All Downloads are FREE. Search and download functionalities are using the official Maven repository.

src.android.os.VibrationEffect Maven / Gradle / Ivy

Go to download

A library jar that provides APIs for Applications written for the Google Android Platform.

There is a newer version: 15-robolectric-12650502
Show newest version
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.os;

import android.annotation.FloatRange;
import android.annotation.IntDef;
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.compat.annotation.UnsupportedAppUsage;
import android.content.ContentResolver;
import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.hardware.vibrator.V1_3.Effect;
import android.net.Uri;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
import android.util.MathUtils;

import com.android.internal.util.Preconditions;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

/**
 * A VibrationEffect describes a haptic effect to be performed by a {@link Vibrator}.
 *
 * These effects may be any number of things, from single shot vibrations to complex waveforms.
 */
public abstract class VibrationEffect implements Parcelable {
    // Stevens' coefficient to scale the perceived vibration intensity.
    private static final float SCALE_GAMMA = 0.65f;


    /**
     * The default vibration strength of the device.
     */
    public static final int DEFAULT_AMPLITUDE = -1;

    /**
     * The maximum amplitude value
     * @hide
     */
    public static final int MAX_AMPLITUDE = 255;

    /**
     * A click effect. Use this effect as a baseline, as it's the most common type of click effect.
     */
    public static final int EFFECT_CLICK = Effect.CLICK;

    /**
     * A double click effect.
     */
    public static final int EFFECT_DOUBLE_CLICK = Effect.DOUBLE_CLICK;

    /**
     * A tick effect. This effect is less strong compared to {@link #EFFECT_CLICK}.
     */
    public static final int EFFECT_TICK = Effect.TICK;

    /**
     * A thud effect.
     * @see #get(int)
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @TestApi
    public static final int EFFECT_THUD = Effect.THUD;

    /**
     * A pop effect.
     * @see #get(int)
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @TestApi
    public static final int EFFECT_POP = Effect.POP;

    /**
     * A heavy click effect. This effect is stronger than {@link #EFFECT_CLICK}.
     */
    public static final int EFFECT_HEAVY_CLICK = Effect.HEAVY_CLICK;

    /**
     * A texture effect meant to replicate soft ticks.
     *
     * Unlike normal effects, texture effects are meant to be called repeatedly, generally in
     * response to some motion, in order to replicate the feeling of some texture underneath the
     * user's fingers.
     *
     * @see #get(int)
     * @hide
     */
    @TestApi
    public static final int EFFECT_TEXTURE_TICK = Effect.TEXTURE_TICK;

    /** {@hide} */
    @TestApi
    public static final int EFFECT_STRENGTH_LIGHT = EffectStrength.LIGHT;

    /** {@hide} */
    @TestApi
    public static final int EFFECT_STRENGTH_MEDIUM = EffectStrength.MEDIUM;

    /** {@hide} */
    @TestApi
    public static final int EFFECT_STRENGTH_STRONG = EffectStrength.STRONG;

    /**
     * Ringtone patterns. They may correspond with the device's ringtone audio, or may just be a
     * pattern that can be played as a ringtone with any audio, depending on the device.
     *
     * @see #get(Uri, Context)
     * @hide
     */
    @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
    @TestApi
    public static final int[] RINGTONES = {
        Effect.RINGTONE_1,
        Effect.RINGTONE_2,
        Effect.RINGTONE_3,
        Effect.RINGTONE_4,
        Effect.RINGTONE_5,
        Effect.RINGTONE_6,
        Effect.RINGTONE_7,
        Effect.RINGTONE_8,
        Effect.RINGTONE_9,
        Effect.RINGTONE_10,
        Effect.RINGTONE_11,
        Effect.RINGTONE_12,
        Effect.RINGTONE_13,
        Effect.RINGTONE_14,
        Effect.RINGTONE_15
    };

    /** @hide */
    @IntDef(prefix = { "EFFECT_" }, value = {
            EFFECT_TICK,
            EFFECT_CLICK,
            EFFECT_HEAVY_CLICK,
            EFFECT_DOUBLE_CLICK,
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface EffectType {}

    /** @hide to prevent subclassing from outside of the framework */
    public VibrationEffect() { }

    /**
     * Create a one shot vibration.
     *
     * One shot vibrations will vibrate constantly for the specified period of time at the
     * specified amplitude, and then stop.
     *
     * @param milliseconds The number of milliseconds to vibrate. This must be a positive number.
     * @param amplitude The strength of the vibration. This must be a value between 1 and 255, or
     * {@link #DEFAULT_AMPLITUDE}.
     *
     * @return The desired effect.
     */
    public static VibrationEffect createOneShot(long milliseconds, int amplitude) {
        if (amplitude == 0) {
            throw new IllegalArgumentException(
                    "amplitude must either be DEFAULT_AMPLITUDE, "
                            + "or between 1 and 255 inclusive (amplitude=" + amplitude + ")");
        }
        return createWaveform(new long[]{milliseconds}, new int[]{amplitude}, -1 /* repeat */);
    }

    /**
     * Create a waveform vibration.
     *
     * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For
     * each pair, the value in the amplitude array determines the strength of the vibration and the
     * value in the timing array determines how long it vibrates for. An amplitude of 0 implies no
     * vibration (i.e. off), and any pairs with a timing value of 0 will be ignored.
     * 

* The amplitude array of the generated waveform will be the same size as the given * timing array with alternating values of 0 (i.e. off) and {@link #DEFAULT_AMPLITUDE}, * starting with 0. Therefore the first timing value will be the period to wait before turning * the vibrator on, the second value will be how long to vibrate at {@link #DEFAULT_AMPLITUDE} * strength, etc. *

* To cause the pattern to repeat, pass the index into the timings array at which to start the * repetition, or -1 to disable repeating. *

* * @param timings The pattern of alternating on-off timings, starting with off. Timing values * of 0 will cause the timing / amplitude pair to be ignored. * @param repeat The index into the timings array at which to repeat, or -1 if you you don't * want to repeat. * * @return The desired effect. */ public static VibrationEffect createWaveform(long[] timings, int repeat) { int[] amplitudes = new int[timings.length]; for (int i = 0; i < (timings.length / 2); i++) { amplitudes[i*2 + 1] = VibrationEffect.DEFAULT_AMPLITUDE; } return createWaveform(timings, amplitudes, repeat); } /** * Create a waveform vibration. * * Waveform vibrations are a potentially repeating series of timing and amplitude pairs. For * each pair, the value in the amplitude array determines the strength of the vibration and the * value in the timing array determines how long it vibrates for, in milliseconds. Amplitude * values must be between 0 and 255, and an amplitude of 0 implies no vibration (i.e. off). Any * pairs with a timing value of 0 will be ignored. *

* To cause the pattern to repeat, pass the index into the timings array at which to start the * repetition, or -1 to disable repeating. *

* * @param timings The timing values, in milliseconds, of the timing / amplitude pairs. Timing * values of 0 will cause the pair to be ignored. * @param amplitudes The amplitude values of the timing / amplitude pairs. Amplitude values * must be between 0 and 255, or equal to {@link #DEFAULT_AMPLITUDE}. An * amplitude value of 0 implies the motor is off. * @param repeat The index into the timings array at which to repeat, or -1 if you you don't * want to repeat. * * @return The desired effect. */ public static VibrationEffect createWaveform(long[] timings, int[] amplitudes, int repeat) { if (timings.length != amplitudes.length) { throw new IllegalArgumentException( "timing and amplitude arrays must be of equal length" + " (timings.length=" + timings.length + ", amplitudes.length=" + amplitudes.length + ")"); } List segments = new ArrayList<>(); for (int i = 0; i < timings.length; i++) { float parsedAmplitude = amplitudes[i] == DEFAULT_AMPLITUDE ? DEFAULT_AMPLITUDE : (float) amplitudes[i] / MAX_AMPLITUDE; segments.add(new StepSegment(parsedAmplitude, /* frequency= */ 0, (int) timings[i])); } VibrationEffect effect = new Composed(segments, repeat); effect.validate(); return effect; } /** * Create a predefined vibration effect. * * Predefined effects are a set of common vibration effects that should be identical, regardless * of the app they come from, in order to provide a cohesive experience for users across * the entire device. They also may be custom tailored to the device hardware in order to * provide a better experience than you could otherwise build using the generic building * blocks. * * This will fallback to a generic pattern if one exists and there does not exist a * hardware-specific implementation of the effect. * * @param effectId The ID of the effect to perform: * {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK} * * @return The desired effect. */ @NonNull public static VibrationEffect createPredefined(@EffectType int effectId) { return get(effectId, true); } /** * Get a predefined vibration effect. * * Predefined effects are a set of common vibration effects that should be identical, regardless * of the app they come from, in order to provide a cohesive experience for users across * the entire device. They also may be custom tailored to the device hardware in order to * provide a better experience than you could otherwise build using the generic building * blocks. * * This will fallback to a generic pattern if one exists and there does not exist a * hardware-specific implementation of the effect. * * @param effectId The ID of the effect to perform: * {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK} * * @return The desired effect. * @hide */ @TestApi public static VibrationEffect get(int effectId) { return get(effectId, true); } /** * Get a predefined vibration effect. * * Predefined effects are a set of common vibration effects that should be identical, regardless * of the app they come from, in order to provide a cohesive experience for users across * the entire device. They also may be custom tailored to the device hardware in order to * provide a better experience than you could otherwise build using the generic building * blocks. * * Some effects you may only want to play if there's a hardware specific implementation because * they may, for example, be too disruptive to the user without tuning. The {@code fallback} * parameter allows you to decide whether you want to fallback to the generic implementation or * only play if there's a tuned, hardware specific one available. * * @param effectId The ID of the effect to perform: * {@link #EFFECT_CLICK}, {@link #EFFECT_DOUBLE_CLICK}, {@link #EFFECT_TICK} * @param fallback Whether to fallback to a generic pattern if a hardware specific * implementation doesn't exist. * * @return The desired effect. * @hide */ @TestApi public static VibrationEffect get(int effectId, boolean fallback) { VibrationEffect effect = new Composed( new PrebakedSegment(effectId, fallback, EffectStrength.MEDIUM)); effect.validate(); return effect; } /** * Get a predefined vibration effect associated with a given URI. * * Predefined effects are a set of common vibration effects that should be identical, regardless * of the app they come from, in order to provide a cohesive experience for users across * the entire device. They also may be custom tailored to the device hardware in order to * provide a better experience than you could otherwise build using the generic building * blocks. * * @param uri The URI associated with the haptic effect. * @param context The context used to get the URI to haptic effect association. * * @return The desired effect, or {@code null} if there's no associated effect. * * @hide */ @TestApi @Nullable public static VibrationEffect get(Uri uri, Context context) { String[] uris = context.getResources().getStringArray( com.android.internal.R.array.config_ringtoneEffectUris); // Skip doing any IPC if we don't have any effects configured. if (uris.length == 0) { return null; } final ContentResolver cr = context.getContentResolver(); Uri uncanonicalUri = cr.uncanonicalize(uri); if (uncanonicalUri == null) { // If we already had an uncanonical URI, it's possible we'll get null back here. In // this case, just use the URI as passed in since it wasn't canonicalized in the first // place. uncanonicalUri = uri; } for (int i = 0; i < uris.length && i < RINGTONES.length; i++) { if (uris[i] == null) { continue; } Uri mappedUri = cr.uncanonicalize(Uri.parse(uris[i])); if (mappedUri == null) { continue; } if (mappedUri.equals(uncanonicalUri)) { return get(RINGTONES[i]); } } return null; } /** * Start composing a haptic effect. * * @see VibrationEffect.Composition */ @NonNull public static Composition startComposition() { return new Composition(); } /** * Start building a waveform vibration. * *

The waveform builder offers more flexibility for creating waveform vibrations, allowing * control over vibration frequency and ramping up or down the vibration amplitude, frequency or * both. * *

For simpler waveform patterns see {@link #createWaveform} methods. * * @hide * @see VibrationEffect.WaveformBuilder */ @TestApi @NonNull public static WaveformBuilder startWaveform() { return new WaveformBuilder(); } @Override public int describeContents() { return 0; } /** @hide */ public abstract void validate(); /** * Gets the estimated duration of the vibration in milliseconds. * * For effects without a defined end (e.g. a Waveform with a non-negative repeat index), this * returns Long.MAX_VALUE. For effects with an unknown duration (e.g. Prebaked effects where * the length is device and potentially run-time dependent), this returns -1. * * @hide */ @TestApi public abstract long getDuration(); /** * Resolve default values into integer amplitude numbers. * * @param defaultAmplitude the default amplitude to apply, must be between 0 and * MAX_AMPLITUDE * @return this if amplitude value is already set, or a copy of this effect with given default * amplitude otherwise * * @hide */ public abstract T resolve(int defaultAmplitude); /** * Scale the vibration effect intensity with the given constraints. * * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will * scale down the intensity, values larger than 1 will scale up * @return this if there is no scaling to be done, or a copy of this effect with scaled * vibration intensity otherwise * * @hide */ public abstract T scale(float scaleFactor); /** * Applies given effect strength to prebaked effects represented by one of * VibrationEffect.EFFECT_*. * * @param effectStrength new effect strength to be applied, one of * VibrationEffect.EFFECT_STRENGTH_*. * @return this if there is no change to this effect, or a copy of this effect with applied * effect strength otherwise. * @hide */ public T applyEffectStrength(int effectStrength) { return (T) this; } /** * Scale given vibration intensity by the given factor. * * @param intensity relative intensity of the effect, must be between 0 and 1 * @param scaleFactor scale factor to be applied to the intensity. Values within [0,1) will * scale down the intensity, values larger than 1 will scale up * @hide */ public static float scale(float intensity, float scaleFactor) { // Applying gamma correction to the scale factor, which is the same as encoding the input // value, scaling it, then decoding the scaled value. float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA); if (scaleFactor <= 1) { // Scale down is simply a gamma corrected application of scaleFactor to the intensity. // Scale up requires a different curve to ensure the intensity will not become > 1. return intensity * scale; } // Apply the scale factor a few more times to make the ramp curve closer to the raw scale. float extraScale = MathUtils.pow(scaleFactor, 4f - scaleFactor); float x = intensity * scale * extraScale; float maxX = scale * extraScale; // scaled x for intensity == 1 float expX = MathUtils.exp(x); float expMaxX = MathUtils.exp(maxX); // Using f = tanh as the scale up function so the max value will converge. // a = 1/f(maxX), used to scale f so that a*f(maxX) = 1 (the value will converge to 1). float a = (expMaxX + 1f) / (expMaxX - 1f); float fx = (expX - 1f) / (expX + 1f); return MathUtils.constrain(a * fx, 0f, 1f); } /** @hide */ public static String effectIdToString(int effectId) { switch (effectId) { case EFFECT_CLICK: return "CLICK"; case EFFECT_TICK: return "TICK"; case EFFECT_HEAVY_CLICK: return "HEAVY_CLICK"; case EFFECT_DOUBLE_CLICK: return "DOUBLE_CLICK"; case EFFECT_POP: return "POP"; case EFFECT_THUD: return "THUD"; case EFFECT_TEXTURE_TICK: return "TEXTURE_TICK"; default: return Integer.toString(effectId); } } /** @hide */ public static String effectStrengthToString(int effectStrength) { switch (effectStrength) { case EFFECT_STRENGTH_LIGHT: return "LIGHT"; case EFFECT_STRENGTH_MEDIUM: return "MEDIUM"; case EFFECT_STRENGTH_STRONG: return "STRONG"; default: return Integer.toString(effectStrength); } } /** * Implementation of {@link VibrationEffect} described by a composition of one or more * {@link VibrationEffectSegment}, with an optional index to represent repeating effects. * * @hide */ @TestApi public static final class Composed extends VibrationEffect { private final ArrayList mSegments; private final int mRepeatIndex; Composed(@NonNull Parcel in) { this(in.readArrayList(VibrationEffectSegment.class.getClassLoader()), in.readInt()); } Composed(@NonNull VibrationEffectSegment segment) { this(Arrays.asList(segment), /* repeatIndex= */ -1); } /** @hide */ public Composed(@NonNull List segments, int repeatIndex) { super(); mSegments = new ArrayList<>(segments); mRepeatIndex = repeatIndex; } @NonNull public List getSegments() { return mSegments; } public int getRepeatIndex() { return mRepeatIndex; } @Override public void validate() { int segmentCount = mSegments.size(); boolean hasNonZeroDuration = false; for (int i = 0; i < segmentCount; i++) { VibrationEffectSegment segment = mSegments.get(i); segment.validate(); // A segment with unknown duration = -1 still counts as a non-zero duration. hasNonZeroDuration |= segment.getDuration() != 0; } if (!hasNonZeroDuration) { throw new IllegalArgumentException("at least one timing must be non-zero" + " (segments=" + mSegments + ")"); } if (mRepeatIndex != -1) { Preconditions.checkArgumentInRange(mRepeatIndex, 0, segmentCount - 1, "repeat index must be within the bounds of the segments (segments.length=" + segmentCount + ", index=" + mRepeatIndex + ")"); } } @Override public long getDuration() { if (mRepeatIndex >= 0) { return Long.MAX_VALUE; } int segmentCount = mSegments.size(); long totalDuration = 0; for (int i = 0; i < segmentCount; i++) { long segmentDuration = mSegments.get(i).getDuration(); if (segmentDuration < 0) { return segmentDuration; } totalDuration += segmentDuration; } return totalDuration; } @NonNull @Override public Composed resolve(int defaultAmplitude) { int segmentCount = mSegments.size(); ArrayList resolvedSegments = new ArrayList<>(segmentCount); for (int i = 0; i < segmentCount; i++) { resolvedSegments.add(mSegments.get(i).resolve(defaultAmplitude)); } if (resolvedSegments.equals(mSegments)) { return this; } Composed resolved = new Composed(resolvedSegments, mRepeatIndex); resolved.validate(); return resolved; } @NonNull @Override public Composed scale(float scaleFactor) { int segmentCount = mSegments.size(); ArrayList scaledSegments = new ArrayList<>(segmentCount); for (int i = 0; i < segmentCount; i++) { scaledSegments.add(mSegments.get(i).scale(scaleFactor)); } if (scaledSegments.equals(mSegments)) { return this; } Composed scaled = new Composed(scaledSegments, mRepeatIndex); scaled.validate(); return scaled; } @NonNull @Override public Composed applyEffectStrength(int effectStrength) { int segmentCount = mSegments.size(); ArrayList scaledSegments = new ArrayList<>(segmentCount); for (int i = 0; i < segmentCount; i++) { scaledSegments.add(mSegments.get(i).applyEffectStrength(effectStrength)); } if (scaledSegments.equals(mSegments)) { return this; } Composed scaled = new Composed(scaledSegments, mRepeatIndex); scaled.validate(); return scaled; } @Override public boolean equals(@Nullable Object o) { if (!(o instanceof Composed)) { return false; } Composed other = (Composed) o; return mSegments.equals(other.mSegments) && mRepeatIndex == other.mRepeatIndex; } @Override public int hashCode() { return Objects.hash(mSegments, mRepeatIndex); } @Override public String toString() { return "Composed{segments=" + mSegments + ", repeat=" + mRepeatIndex + "}"; } @Override public int describeContents() { return 0; } @Override public void writeToParcel(@NonNull Parcel out, int flags) { out.writeList(mSegments); out.writeInt(mRepeatIndex); } @NonNull public static final Creator CREATOR = new Creator() { @Override public Composed createFromParcel(Parcel in) { return new Composed(in); } @Override public Composed[] newArray(int size) { return new Composed[size]; } }; } /** * A composition of haptic primitives that, when combined, create a single haptic effect. * * @see VibrationEffect#startComposition() */ public static final class Composition { /** @hide */ @IntDef(prefix = { "PRIMITIVE_" }, value = { PRIMITIVE_CLICK, PRIMITIVE_THUD, PRIMITIVE_SPIN, PRIMITIVE_QUICK_RISE, PRIMITIVE_SLOW_RISE, PRIMITIVE_QUICK_FALL, PRIMITIVE_TICK, PRIMITIVE_LOW_TICK, }) @Retention(RetentionPolicy.SOURCE) public @interface PrimitiveType {} /** * No haptic effect. Used to generate extended delays between primitives. * @hide */ public static final int PRIMITIVE_NOOP = 0; /** * This effect should produce a sharp, crisp click sensation. */ public static final int PRIMITIVE_CLICK = 1; /** * A haptic effect that simulates downwards movement with gravity. Often * followed by extra energy of hitting and reverberation to augment * physicality. */ public static final int PRIMITIVE_THUD = 2; /** * A haptic effect that simulates spinning momentum. */ public static final int PRIMITIVE_SPIN = 3; /** * A haptic effect that simulates quick upward movement against gravity. */ public static final int PRIMITIVE_QUICK_RISE = 4; /** * A haptic effect that simulates slow upward movement against gravity. */ public static final int PRIMITIVE_SLOW_RISE = 5; /** * A haptic effect that simulates quick downwards movement with gravity. */ public static final int PRIMITIVE_QUICK_FALL = 6; /** * This very short effect should produce a light crisp sensation intended * to be used repetitively for dynamic feedback. */ // Internally this maps to the HAL constant CompositePrimitive::LIGHT_TICK public static final int PRIMITIVE_TICK = 7; /** * This very short low frequency effect should produce a light crisp sensation * intended to be used repetitively for dynamic feedback. */ // Internally this maps to the HAL constant CompositePrimitive::LOW_TICK public static final int PRIMITIVE_LOW_TICK = 8; private final ArrayList mSegments = new ArrayList<>(); private int mRepeatIndex = -1; Composition() {} /** * Add a haptic effect to the end of the current composition. * *

Similar to {@link #addEffect(VibrationEffect, int)} , but with no delay applied. * * @param effect The effect to add to this composition as a primitive * @return The {@link Composition} object to enable adding multiple primitives in one chain. * @hide */ @TestApi @NonNull public Composition addEffect(@NonNull VibrationEffect effect) { return addEffect(effect, /* delay= */ 0); } /** * Add a haptic effect to the end of the current composition. * * @param effect The effect to add to this composition as a primitive * @param delay The amount of time in milliseconds to wait before playing this primitive * @return The {@link Composition} object to enable adding multiple primitives in one chain. * @hide */ @TestApi @NonNull public Composition addEffect(@NonNull VibrationEffect effect, @IntRange(from = 0) int delay) { Preconditions.checkArgumentNonnegative(delay); if (delay > 0) { // Created a segment sustaining the zero amplitude to represent the delay. addSegment(new StepSegment(/* amplitude= */ 0, /* frequency= */ 0, /* duration= */ delay)); } return addSegments(effect); } /** * Add a haptic primitive to the end of the current composition. * * Similar to {@link #addPrimitive(int, float, int)}, but with no delay and a * default scale applied. * * @param primitiveId The primitive to add * * @return The {@link Composition} object to enable adding multiple primitives in one chain. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId) { return addPrimitive(primitiveId, /*scale*/ 1.0f, /*delay*/ 0); } /** * Add a haptic primitive to the end of the current composition. * * Similar to {@link #addPrimitive(int, float, int)}, but with no delay. * * @param primitiveId The primitive to add * @param scale The scale to apply to the intensity of the primitive. * * @return The {@link Composition} object to enable adding multiple primitives in one chain. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId, @FloatRange(from = 0f, to = 1f) float scale) { return addPrimitive(primitiveId, scale, /*delay*/ 0); } /** * Add a haptic primitive to the end of the current composition. * * @param primitiveId The primitive to add * @param scale The scale to apply to the intensity of the primitive. * @param delay The amount of time in milliseconds to wait before playing this primitive, * starting at the time the previous element in this composition is finished. * @return The {@link Composition} object to enable adding multiple primitives in one chain. */ @NonNull public Composition addPrimitive(@PrimitiveType int primitiveId, @FloatRange(from = 0f, to = 1f) float scale, @IntRange(from = 0) int delay) { PrimitiveSegment primitive = new PrimitiveSegment(primitiveId, scale, delay); primitive.validate(); return addSegment(primitive); } private Composition addSegment(VibrationEffectSegment segment) { if (mRepeatIndex >= 0) { throw new IllegalStateException( "Composition already have a repeating effect so any new primitive would be" + " unreachable."); } mSegments.add(segment); return this; } private Composition addSegments(VibrationEffect effect) { if (mRepeatIndex >= 0) { throw new IllegalStateException( "Composition already have a repeating effect so any new primitive would be" + " unreachable."); } Composed composed = (Composed) effect; if (composed.getRepeatIndex() >= 0) { // Start repeating from the index relative to the composed waveform. mRepeatIndex = mSegments.size() + composed.getRepeatIndex(); } mSegments.addAll(composed.getSegments()); return this; } /** * Compose all of the added primitives together into a single {@link VibrationEffect}. * * The {@link Composition} object is still valid after this call, so you can continue adding * more primitives to it and generating more {@link VibrationEffect}s by calling this method * again. * * @return The {@link VibrationEffect} resulting from the composition of the primitives. */ @NonNull public VibrationEffect compose() { if (mSegments.isEmpty()) { throw new IllegalStateException( "Composition must have at least one element to compose."); } VibrationEffect effect = new Composed(mSegments, mRepeatIndex); effect.validate(); return effect; } /** * Convert the primitive ID to a human readable string for debugging * @param id The ID to convert * @return The ID in a human readable format. * @hide */ public static String primitiveToString(@PrimitiveType int id) { switch (id) { case PRIMITIVE_NOOP: return "PRIMITIVE_NOOP"; case PRIMITIVE_CLICK: return "PRIMITIVE_CLICK"; case PRIMITIVE_THUD: return "PRIMITIVE_THUD"; case PRIMITIVE_SPIN: return "PRIMITIVE_SPIN"; case PRIMITIVE_QUICK_RISE: return "PRIMITIVE_QUICK_RISE"; case PRIMITIVE_SLOW_RISE: return "PRIMITIVE_SLOW_RISE"; case PRIMITIVE_QUICK_FALL: return "PRIMITIVE_QUICK_FALL"; case PRIMITIVE_TICK: return "PRIMITIVE_TICK"; case PRIMITIVE_LOW_TICK: return "PRIMITIVE_LOW_TICK"; default: return Integer.toString(id); } } } /** * A builder for waveform haptic effects. * *

Waveform vibrations constitute of one or more timed segments where the vibration * amplitude, frequency or both can linearly ramp to new values. * *

Waveform segments may have zero duration, which represent a jump to new vibration * amplitude and/or frequency values. * *

Waveform segments may have the same start and end vibration amplitude and frequency, * which represent a step where the amplitude and frequency are maintained for that duration. * * @hide * @see VibrationEffect#startWaveform() */ @TestApi public static final class WaveformBuilder { private ArrayList mSegments = new ArrayList<>(); WaveformBuilder() {} /** * Vibrate with given amplitude for the given duration, in millis, keeping the previous * frequency the same. * *

If the duration is zero the vibrator will jump to new amplitude. * * @param amplitude The amplitude for this step * @param duration The duration of this step in milliseconds * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. */ @SuppressLint("MissingGetterMatchingBuilder") @NonNull public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, @IntRange(from = 0) int duration) { return addStep(amplitude, getPreviousFrequency(), duration); } /** * Vibrate with given amplitude for the given duration, in millis, keeping the previous * vibration frequency the same. * *

If the duration is zero the vibrator will jump to new amplitude. * * @param amplitude The amplitude for this step * @param frequency The frequency for this step * @param duration The duration of this step in milliseconds * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. */ @SuppressLint("MissingGetterMatchingBuilder") @NonNull public WaveformBuilder addStep(@FloatRange(from = 0f, to = 1f) float amplitude, @FloatRange(from = -1f, to = 1f) float frequency, @IntRange(from = 0) int duration) { mSegments.add(new StepSegment(amplitude, frequency, duration)); return this; } /** * Ramp vibration linearly for the given duration, in millis, from previous amplitude value * to the given one, keeping previous frequency. * *

If the duration is zero the vibrator will jump to new amplitude. * * @param amplitude The final amplitude this ramp should reach * @param duration The duration of this ramp in milliseconds * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. */ @SuppressLint("MissingGetterMatchingBuilder") @NonNull public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, @IntRange(from = 0) int duration) { return addRamp(amplitude, getPreviousFrequency(), duration); } /** * Ramp vibration linearly for the given duration, in millis, from previous amplitude and * frequency values to the given ones. * *

If the duration is zero the vibrator will jump to new amplitude and frequency. * * @param amplitude The final amplitude this ramp should reach * @param frequency The final frequency this ramp should reach * @param duration The duration of this ramp in milliseconds * @return The {@link WaveformBuilder} object to enable adding multiple steps in chain. */ @SuppressLint("MissingGetterMatchingBuilder") @NonNull public WaveformBuilder addRamp(@FloatRange(from = 0f, to = 1f) float amplitude, @FloatRange(from = -1f, to = 1f) float frequency, @IntRange(from = 0) int duration) { mSegments.add(new RampSegment(getPreviousAmplitude(), amplitude, getPreviousFrequency(), frequency, duration)); return this; } /** * Compose all of the steps together into a single {@link VibrationEffect}. * * The {@link WaveformBuilder} object is still valid after this call, so you can * continue adding more primitives to it and generating more {@link VibrationEffect}s by * calling this method again. * * @return The {@link VibrationEffect} resulting from the composition of the steps. */ @NonNull public VibrationEffect build() { return build(/* repeat= */ -1); } /** * Compose all of the steps together into a single {@link VibrationEffect}. * *

To cause the pattern to repeat, pass the index at which to start the repetition * (starting at 0), or -1 to disable repeating. * *

The {@link WaveformBuilder} object is still valid after this call, so you can * continue adding more primitives to it and generating more {@link VibrationEffect}s by * calling this method again. * * @return The {@link VibrationEffect} resulting from the composition of the steps. */ @NonNull public VibrationEffect build(int repeat) { if (mSegments.isEmpty()) { throw new IllegalStateException( "WaveformBuilder must have at least one element to build."); } VibrationEffect effect = new Composed(mSegments, repeat); effect.validate(); return effect; } private float getPreviousFrequency() { if (!mSegments.isEmpty()) { VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); if (segment instanceof StepSegment) { return ((StepSegment) segment).getFrequency(); } else if (segment instanceof RampSegment) { return ((RampSegment) segment).getEndFrequency(); } } return 0; } private float getPreviousAmplitude() { if (!mSegments.isEmpty()) { VibrationEffectSegment segment = mSegments.get(mSegments.size() - 1); if (segment instanceof StepSegment) { return ((StepSegment) segment).getAmplitude(); } else if (segment instanceof RampSegment) { return ((RampSegment) segment).getEndAmplitude(); } } return 0; } } @NonNull public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public VibrationEffect createFromParcel(Parcel in) { return new Composed(in); } @Override public VibrationEffect[] newArray(int size) { return new VibrationEffect[size]; } }; }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy