src.android.os.VibrationEffect Maven / Gradle / Ivy
Show all versions of android-all Show documentation
/*
* 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 extends VibrationEffectSegment> 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];
}
};
}