Özel dokunsal efektler oluştur

Android'de özel dokunsal efektler oluşturma konusunda örnekler ve rehberlik sunun. Bu kapsamda, VibrationEffect.Builder ile zaman çizelgesine sabitlenmiş kompozisyonlar, özel titreşim kalıpları ve gelişmiş dalga formu zarfları yer alır. keywords_public: > Android, haptics, custom effects, vibration, haptic APIs, vibration patterns, VibrationEffect.Builder, compositions, haptic primitives, waveform envelopes, UI

Bu sayfada, Android uygulamasında standart titreşim dalga biçimlerinin ötesinde özel efektler oluşturmak için farklı dokunma teknolojisi API'lerinin nasıl kullanılacağına dair örnekler verilmektedir.

Bu sayfada aşağıdaki örnekler yer alır:

Ek örnekler için Etkinliklere dokunsal geribildirim ekleme başlıklı makaleyi inceleyin ve her zaman dokunma teknolojisi tasarım ilkelerine uyun.

Cihaz uyumluluğunu yönetmek için yedekleri kullanma

Özel dokunsal efektler uygularken cihaz uyumluluğu ve yedek davranış, seçtiğiniz API yüzeyine bağlıdır:

  • VibrationEffect.Builder (Önerilir): Android 16'dan (26Q4) itibaren, VibrationEffect.Builder kullanılarak oluşturulan efektler otomatik çerçeve düzeyi geri dönüş içerir. Bir cihaz, istenen Preset veya temel Envelope'yi yerel olarak desteklemiyorsa çerçeve, oynatma sırasında en iyi çabayı göstererek bunu otomatik olarak uygun bir alternatife çevirir. VibrationEffect.Builder ile oluşturulan efektleri oynatmadan önce cihazın her bir öğe için desteklediği özellikleri manuel olarak kontrol etmeniz gerekmez.
    • İstisna: WaveformEnvelopeBuilder ile oluşturulan gelişmiş dalga biçimi zarfları, belirli donanım frekansı eşlemelerine (FOAM) bağlı oldukları için otomatik geri dönüşü desteklemez. Desteklenmiyorsa oynatılmaz.
  • VibrationEffect.Composition: startComposition() API ile oluşturulan kompozisyonlarda otomatik yedekler yoktur. Bir kompozisyon desteklenmeyen tek bir öğe içerse bile titreşimin tamamı oynatılamaz. vibrator.arePrimitivesSupported() kullanarak özellikleri manuel olarak kontrol etmeniz gerekir.
  • Genlik kontrolü olan dalga biçimleri: Genlik kontrolü olmayan cihazlarda sıfır olmayan genlikler% 100'e yuvarlanır. vibrator.hasAmplitudeControl() seçeneğini işaretleyin ve gerekirse açıkça tasarlanmış bir AÇIK/KAPALI desenine geri dönün.

Dokunsal temel öğelerin kullanımı

Android, hem genlik hem de frekans açısından farklılık gösteren çeşitli dokunma teknolojisi temel öğeleri içerir. Zengin dokunsal efektler elde etmek için tek bir temel öğe kullanabilir veya birden fazla temel öğeyi birleştirebilirsiniz.

  • İki öğe arasında fark edilebilir boşluklar için 50 ms veya daha uzun gecikmeler kullanın. Mümkünse öğe süresini de göz önünde bulundurun.
  • Yoğunluk farkının daha iyi algılanması için 1,4 veya daha fazla oranda farklılık gösteren ölçekler kullanın.
  • Bir öğenin düşük, orta ve yüksek yoğunluklu versiyonunu oluşturmak için 0,5, 0,7 ve 1,0 ölçeklerini kullanın.

Özel titreşim modelleri oluşturma

Titreşim desenleri genellikle bildirimler ve zil sesleri gibi dikkat çekici dokunma teknolojilerinde kullanılır. Vibrator hizmeti, titreşim genliğini zaman içinde değiştiren uzun titreşim desenleri oynatabilir. Bu tür efektlere dalga biçimleri denir.

Dalga formu efektleri genellikle fark edilebilir ancak sessiz bir ortamda oynatıldığında ani ve uzun titreşimler kullanıcıyı ürkütebilir. Hedef genliğe çok hızlı bir şekilde ulaşmak da duyulabilir vızıltı seslerine neden olabilir. Yükselme ve alçalma efektleri oluşturmak için genlik geçişlerini yumuşatacak dalga biçimi desenleri tasarlayın.

Titreşim deseni örnekleri

Aşağıdaki bölümlerde çeşitli titreşim desenleri örnekleri verilmiştir:

Artış deseni

Dalga biçimleri, üç parametreyle VibrationEffect olarak gösterilir:

  1. Zamanlamalar: Her dalga biçimi segmenti için milisaniye cinsinden süreler dizisi.
  2. Amplitüdler: İlk bağımsız değişkende belirtilen her süre için istenen titreşim amplitüdü. 0 ile 255 arasında bir tam sayı değeriyle gösterilir. 0, titreşimin "kapalı durumunu", 255 ise cihazın maksimum amplitüdünü temsil eder.
  3. Tekrar dizini: Dalga formunun tekrar etmeye başlaması için ilk bağımsız değişkende belirtilen dizindeki dizin veya kalıbın yalnızca bir kez oynatılması gerekiyorsa -1.

Aşağıda, iki kez titreşen ve titreşimler arasında 350 ms'lik bir duraklama olan örnek bir dalga biçimi verilmiştir. İlk darbe, maksimum genliğe kadar düzgün bir artış, ikincisi ise maksimum genliği korumak için hızlı bir artıştır. Sonunda durma, negatif tekrar dizini değeriyle tanımlanır.

Kotlin

val timings: LongArray = longArrayOf(
    50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
    33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.

vibrator.vibrate(VibrationEffect.createWaveform(
    timings, amplitudes, repeatIndex))

Java

long[] timings = new long[] {
    50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
    33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.

vibrator.vibrate(VibrationEffect.createWaveform(
    timings, amplitudes, repeatIndex));

Tekrarlanan desen

Dalga biçimleri, iptal edilene kadar tekrar tekrar oynatılabilir. Tekrarlayan dalga formu oluşturmak için negatif olmayan bir repeat parametresi ayarlamanız gerekir. Tekrarlayan bir dalga formu oynattığınızda titreşim, hizmette açıkça iptal edilene kadar devam eder:

Kotlin

void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
    timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.

vibrator.vibrate(repeatingEffect)
}

void stopVibrating() {
vibrator.cancel()
}

Java

void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
    timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.

vibrator.vibrate(repeatingEffect);
}

void stopVibrating() {
vibrator.cancel();
}

Bu, kullanıcının onaylamasını gerektiren aralıklı etkinlikler için çok yararlıdır. Gelen telefon aramaları ve tetiklenen alarmlar bu tür etkinliklere örnek olarak verilebilir.

Yedekli desen

Titreşimin genliğini kontrol etmek donanıma bağlı bir özelliktir. Bu özellik olmadan düşük seviyeli bir cihazda dalga formu oynatmak, cihazın genlik dizisindeki her pozitif giriş için maksimum genlikte titreşmesine neden olur. Uygulamanızın bu tür cihazları desteklemesi gerekiyorsa bu durumda çalındığında vızıltı efekti oluşturmayan bir desen kullanın veya bunun yerine yedek olarak çalınabilecek daha basit bir AÇMA/KAPAMA deseni tasarlayın.

Kotlin

if (vibrator.hasAmplitudeControl()) {
  vibrator.vibrate(VibrationEffect.createWaveform(
    smoothTimings, amplitudes, smoothRepeatIdx))
} else {
  vibrator.vibrate(VibrationEffect.createWaveform(
    onOffTimings, onOffRepeatIdx))
}

Java

if (vibrator.hasAmplitudeControl()) {
  vibrator.vibrate(VibrationEffect.createWaveform(
    smoothTimings, amplitudes, smoothRepeatIdx));
} else {
  vibrator.vibrate(VibrationEffect.createWaveform(
    onOffTimings, onOffRepeatIdx));
}

VibrationEffect.Builder ile zaman çizelgesine sabitlenmiş kompozisyonlar

Android 16 (26Q4) sürümünden itibaren, karmaşık titreşim efektleri ve kompozisyonları oluşturmak için VibrationEffect.Builder tercih edilen API'dir. startTimeMillis ile mutlak bir zaman çizelgesi boyunca ayrı dokunsal öğeleri sıralayarak etkileyici dokunsal hisler oluşturmanıza olanak tanır.

VibrationEffect.Builder birden fazla titreşim türünün birleştirilmesini destekler:

VibrationEffect.Builder ile oluşturulan titreşimler, öğelerinde (hazır ayarlar, temel zarflar ve birleştirilmiş titreşim efektleri dahil) yerleşik, çerçeve düzeyinde otomatik yedeklemeye sahiptir. Bu sayede, manuel capability nesnesi kontrolleri gerektirmeden farklı Cihazlarda tutarlı bir kullanıcı deneyimi sağlanır. (WaveformEnvelopeBuilder ile oluşturulan gelişmiş dalga formu zarfları donanım desteği gerektirir ve otomatik yedekleme desteği yoktur.)

Hazır ayarlarla beste oluşturma

Bir kompozisyona yaygın, önceden tanımlanmış kısa dokunsal titreşimler (PRESET_CLICK, PRESET_TICK veya PRESET_LOW_TICK gibi) eklemek için VibrationEffect.Preset simgesini kullanın. Hazır ayarlar, cihaz üreticisi tarafından net dokunsal geri bildirimler için dokunma teknolojisi ilkelerine uygun, kısa ve hoş bir titreşim sağlamak üzere uygulanır. Bu özellikler ve işleyiş şekilleri hakkında daha fazla bilgi için Titreşim aktüatörleri hakkında temel bilgiler başlıklı makaleyi inceleyin.

Hazır ayarlar, VibrationEffect.Composition API'sindeki kısa temel öğelerin yerini alır. Gücü artırma ve azaltma gibi daha uzun veya sürekli dokunsal hisler için bunun yerine zarf dalga biçimlerini (PWLE) kullanın.

Her hazır ayara 0.0f ile 1.0f arasında isteğe bağlı bir ölçek atanabilir ve kompozisyonun başlangıcından itibaren belirli bir başlangıç zamanına (milisaniye cinsinden) yerleştirilir.

Kotlin

val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)

val effect = VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build()

vibrator.vibrate(effect)

Java

VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);

VibrationEffect effect = new VibrationEffect.Builder()
    .addPreset(/* startTimeMillis= */ 0L, clickPreset)
    .addPreset(/* startTimeMillis= */ 100L, tickPreset)
    .build();

vibrator.vibrate(effect);

Zarflar ve hazır ayarlar ile beste yapma

Zengin ve çok segmentli dokunsal desenler oluşturmak için VibrationEffect.Envelope örneklerini (BasicEnvelopeBuilder veya WaveformEnvelopeBuilder kullanılarak oluşturulmuş) hazır ayarlarla sorunsuz bir şekilde birleştirebilirsiniz.

Aşağıda, yumuşak bir şekilde artan ve azalan zarfın ardından keskin bir tıklama ön ayarının çalındığı bir örnek verilmiştir:

Kotlin

val basicEnvelope = VibrationEffect.Envelope.create(
    VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
)

val mixedEffect = VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build()

vibrator.vibrate(mixedEffect)

Java

VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
    new VibrationEffect.BasicEnvelopeBuilder()
        .setInitialSharpness(0.0f)
        .addControlPoint(1.0f, 1.0f, 300L)
        .addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
    VibrationEffect.Preset.PRESET_CLICK, 0.9f
);

VibrationEffect mixedEffect = new VibrationEffect.Builder()
    .addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
    .addPreset(/* startTimeMillis= */ 450L, clickPreset)
    .build();

vibrator.vibrate(mixedEffect);

Mevcut etkinlikleri yeniden kullanma ve taşıma

Mevcut bir VibrationEffect öğesini (VibrationEffect.Composition dahil) yeniden kullanmak veya birleştirmek için getEvents() kullanarak VibrationEffect.Event nesnelerinin listesini alın ve addEvents(startTimeShiftMillis, events) kullanarak bir ofsetle ekleyin (veya efekti doğrudan VibrationEffect.Builder(effect) oluşturucusuna iletin). Bu şekilde VibrationEffect.Composition örnekleri içe aktarırken çerçeve, temel öğelerini otomatik olarak hazır ayarlara dönüştürür ve çalışma zamanı geri dönüş desteğini etkinleştirir.

Kotlin

val existingEffect = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build()

// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
    .build()

vibrator.vibrate(combinedEffect)

Java

VibrationEffect existingEffect = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
    )
    .addPreset(
        80L,
        VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
    )
    .build();

// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
    .addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
    .build();

vibrator.vibrate(combinedEffect);

Tekrarlanan kompozisyonlar oluşturma

Bir kompozisyona tekrarlayan desen eklemek için setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) tuşunu kullanın:

Kotlin

val repeatingPattern = VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build()

val repeatingEffect = VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build()

vibrator.vibrate(repeatingEffect)

Java

VibrationEffect repeatingPattern = new VibrationEffect.Builder()
    .addPreset(
        0L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_CLICK, 1.0f
        )
    )
    .addPreset(
        150L,
        VibrationEffect.Preset.create(
            VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
        )
    )
    .build();

VibrationEffect repeatingEffect = new VibrationEffect.Builder()
    .setRepeatingEffect(
        /* startTimeMillis= */ 0L,
        /* effect= */ repeatingPattern,
        /* durationMillis= */ 300L
    )
    .build();

vibrator.vibrate(repeatingEffect);

Zamanlama, doğrulama ve sapma yönetimi

VibrationEffect.Builder ile kompozisyon tasarlarken aşağıdaki zamanlama ve doğrulama kurallarını göz önünde bulundurun:

  • Başlangıç zamanları kesinlikle artırılmalıdır: Oluşturucuya eklenen her öğenin, önceki öğenin başlangıç zamanından kesinlikle büyük veya ona eşit bir startTimeMillis değeri olmalıdır.
  • Derleme zamanı doğrulama: Oluşturucu, bilinen öğe sürelerini (veya hazır ayarlar için minimum 1 ms) kullanarak derleme build() zamanında en iyi çabayı göstererek doğrulama gerçekleştirir. İmkansız bir çakışma tespit edilirse IllegalArgumentException oluşturulur.
  • Sıralı oynatma kaydırma: Bir önceki titreşim öğesi, sonraki öğenin başlangıç zamanı geldiğinde fiziksel olarak oynatılmaya devam ediyorsa çerçeve, sonraki öğeyi otomatik olarak mevcut en erken zaman aralığına kaydırır. Bu sayede etkinliklerin çakışması ve titreşimlerin düşmesi önlenir. Ancak etkinlikler birbirine çok yakın zamanlarda planlanırsa hafif bir zamanlama kayması oluşabilir. Sapmayı en aza indirmek için art arda gelen dokunsal etkinlikler arasında yeterli süre (ör. 50 ms veya daha fazla) bırakın.

Titreşim temel öğeleri kompozisyonları oluşturma

Bu bölümde, VibrationEffect.Composition kullanarak titreşimlerin nasıl oluşturulacağı açıklanmaktadır. Bu sayfanın önceki bölümlerinde açıklanan kompozisyon temelleri, cihaz üreticisi tarafından uygulanır. Net dokunsal geri bildirim için dokunma teknolojisi ilkelerine uygun, kısa ve hoş bir titreşim sağlar. Bu özellikler ve işleyiş şekilleri hakkında daha fazla bilgi için Titreşim aktüatörleri hakkında temel bilgiler başlıklı makaleyi inceleyin.

VibrationEffect.Builder'nın aksine, VibrationEffect.Composition API'sinde desteklenmeyen temel öğeler için otomatik yedekler yoktur. Bu nedenle:

  1. Gelişmiş dokunma teknolojinizi etkinleştirmeden önce, belirli bir cihazın kullandığınız tüm temel öğeleri desteklediğini kontrol edin.

  2. Desteklenmeyen tutarlı deneyimler grubunu devre dışı bırakın. Yalnızca bir öğesi eksik olan efektleri devre dışı bırakmayın.

  • Kotlin: val fallbackEffect = VibrationEffect.Builder(compositionEffect).build() (veya .addEvents(0L, compositionEffect.events))
  • Java: VibrationEffect fallbackEffect = new VibrationEffect.Builder(compositionEffect).build(); (veya .addEvents(0L, compositionEffect.getEvents()))

VibrationEffect.Builder ile oluşturulduğunda çerçeve, kompozisyon temel öğelerini hazır ayarlara dönüştürür ve herhangi bir temel öğe kullanıcının cihazında desteklenmiyorsa otomatik olarak çalışma zamanı geri dönüşü sağlar.

Birleştirilmiş titreşim efektleri

VibrationEffect.Composition ile birleşik titreşim efektleri oluşturabilirsiniz. Aşağıda, yavaş yavaş yükselen bir efektin ardından gelen keskin bir tıklama efektine ilişkin örnek verilmiştir:

Kotlin

vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_CLICK
    ).compose()
)

Java

vibrator.vibrate(
    VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
        .compose());

Bir beste, sırayla çalınacak temel öğeler eklenerek oluşturulur. Her birim ölçeklenebilir olduğundan her birinin oluşturduğu titreşimin genliğini kontrol edebilirsiniz. Ölçek, 0 ile 1 arasında bir değer olarak tanımlanır. 0, bu öğenin kullanıcı tarafından (zar zor) hissedilebileceği minimum genliğe karşılık gelir.

Titreşim temel öğelerinde varyant oluşturma

Aynı öğenin zayıf ve güçlü bir sürümünü oluşturmak istiyorsanız harcanan efor düzeyi farkının kolayca algılanabilmesi için 1,4 veya daha yüksek güç oranları oluşturun. Aynı öğenin üçten fazla yoğunluk seviyesini oluşturmaya çalışmayın. Çünkü bunlar algısal olarak farklı değildir. Örneğin, bir öğenin düşük, orta ve yüksek yoğunluklu sürümlerini oluşturmak için 0,5, 0,7 ve 1,0 ölçeklerini kullanın.

Titreşim öğeleri arasına boşluk ekleme

Kompozisyon, ardışık öğeler arasına eklenecek gecikmeleri de belirtebilir. Bu gecikme, önceki öğenin sonundan itibaren milisaniye cinsinden ifade edilir. Genel olarak, iki öğe arasındaki 5-10 ms'lik boşluk, algılanamayacak kadar kısadır. İki temel öğe arasında fark edilebilir bir boşluk oluşturmak istiyorsanız 50 ms veya daha uzun bir boşluk kullanın. Aşağıda, gecikmeler içeren bir beste örneği verilmiştir:

Kotlin

val delayMs = 100
vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
    ).compose()
)

Java

int delayMs = 100;
vibrator.vibrate(
    VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
        .addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
        .compose());

Hangi temel öğelerin desteklendiğini kontrol etme

Belirli temel öğeler için cihaz desteğini doğrulamak üzere aşağıdaki API'ler kullanılabilir:

Kotlin

val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition()
        .addPrimitive(primitive).compose())
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

Java

int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition()
        .addPrimitive(primitive).compose());
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

Ayrıca, birden fazla temel öğeyi kontrol edip cihaz destek düzeyine göre hangi öğeleri oluşturacağınıza karar verebilirsiniz:

Kotlin

val effects: IntArray = intArrayOf(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives)

Java

int[] primitives = new int[] {
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);

Titreşim kompozisyonu örnekleri

Aşağıdaki bölümlerde, GitHub'daki haptics örnek uygulamasından alınan titreşim kompozisyonlarıyla ilgili çeşitli örnekler verilmektedir.

Direnç (düşük tiklerle)

Devam eden bir işlemle ilgili faydalı geri bildirimler vermek için temel titreşimin genliğini kontrol edebilirsiniz. Yakın aralıklı ölçek değerleri, bir öğenin yumuşak bir crescendo efekti oluşturmak için kullanılabilir. Ardışık ilkel öğeler arasındaki gecikme de kullanıcı etkileşimine göre dinamik olarak ayarlanabilir. Bu durum, sürükleme hareketiyle kontrol edilen ve dokunsal geri bildirimlerle zenginleştirilmiş bir görünüm animasyonu örneğinde gösterilmektedir.

Aşağı sürüklenen bir dairenin animasyonu.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 1. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini gösterir.

Kotlin

@Composable
fun ResistScreen() {
    // Control variables for the dragging of the indicator.
    var isDragging by remember { mutableStateOf(false) }
    var dragOffset by remember { mutableStateOf(0f) }

    // Only vibrates while the user is dragging
    if (isDragging) {
        LaunchedEffect(Unit) {
        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        while (true) {
            // Calculate the interval inversely proportional to the drag offset.
            val vibrationInterval = calculateVibrationInterval(dragOffset)
            // Calculate the scale directly proportional to the drag offset.
            val vibrationScale = calculateVibrationScale(dragOffset)

            delay(vibrationInterval)
            vibrator.vibrate(
            VibrationEffect.startComposition().addPrimitive(
                VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                vibrationScale
            ).compose()
            )
        }
        }
    }

    Screen() {
        Column(
        Modifier
            .draggable(
            orientation = Orientation.Vertical,
            onDragStarted = {
                isDragging = true
            },
            onDragStopped = {
                isDragging = false
            },
            state = rememberDraggableState { delta ->
                dragOffset += delta
            }
            )
        ) {
        // Build the indicator UI based on how much the user has dragged it.
        ResistIndicator(dragOffset)
        }
    }
}

Java

class DragListener implements View.OnTouchListener {
    // Control variables for the dragging of the indicator.
    private int startY;
    private int vibrationInterval;
    private float vibrationScale;

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startY = event.getRawY();
            vibrationInterval = calculateVibrationInterval(0);
            vibrationScale = calculateVibrationScale(0);
            startVibration();
            break;
        case MotionEvent.ACTION_MOVE:
            float dragOffset = event.getRawY() - startY;
            // Calculate the interval inversely proportional to the drag offset.
            vibrationInterval = calculateVibrationInterval(dragOffset);
            // Calculate the scale directly proportional to the drag offset.
            vibrationScale = calculateVibrationScale(dragOffset);
            // Build the indicator UI based on how much the user has dragged it.
            updateIndicator(dragOffset);
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            // Only vibrates while the user is dragging
            cancelVibration();
            break;
        }
        return true;
    }

    private void startVibration() {
        vibrator.vibrate(
            VibrationEffect.startComposition()
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                        vibrationScale)
                .compose());

        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        handler.postDelayed(this::startVibration, vibrationInterval);
    }

    private void cancelVibration() {
        handler.removeCallbacksAndMessages(null);
    }
}

Genişletme (yükselme ve düşmeyle)

Algılanan titreşim yoğunluğunu artırmak için iki ilkel vardır: PRIMITIVE_QUICK_RISE ve PRIMITIVE_SLOW_RISE. Her ikisi de aynı hedefe farklı sürelerde ulaşır. Yavaşlatma için yalnızca bir temel öğe vardır: PRIMITIVE_QUICK_FALL. Bu temel öğeler, harcanan efor düzeyi artan ve sonra azalan bir dalga formu segmenti oluşturmak için birlikte daha iyi çalışır. Ölçeklendirilmiş temel öğeleri, aralarındaki genlikte ani sıçramaları önleyecek şekilde hizalayabilirsiniz. Bu, genel efekt süresini uzatmak için de iyi bir yöntemdir. İnsanlar, yükselen kısmı düşen kısımdan daha çok fark eder. Bu nedenle, yükselen kısmı düşen kısımdan daha kısa yapmak, vurguyu düşen kısma kaydırmak için kullanılabilir.

Bu kompozisyonun bir daireyi genişletme ve daraltma için uygulanmasına dair örneği aşağıda bulabilirsiniz. Yükselme efekti, animasyon sırasında genişleme hissini artırabilir. Yükselme ve düşme efektlerinin birleşimi, animasyonun sonundaki daralmayı vurgulamaya yardımcı olur.

Genişleyen daire animasyonu.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 2. Bu dalga biçimi, bir cihazdaki titreşimin çıkış ivmesini gösterir.

Kotlin

enum class ExpandShapeState {
    Collapsed,
    Expanded
}

@Composable
fun ExpandScreen() {
    // Control variable for the state of the indicator.
    var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }

    // Animation between expanded and collapsed states.
    val transitionData = updateTransitionData(currentState)

    Screen() {
        Column(
        Modifier
            .clickable(
            {
                if (currentState == ExpandShapeState.Collapsed) {
                currentState = ExpandShapeState.Expanded
                vibrator.vibrate(
                    VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
                    0.3f
                    ).addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                    0.3f
                    ).compose()
                )
                } else {
                currentState = ExpandShapeState.Collapsed
                vibrator.vibrate(
                    VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
                    ).compose()
                )
            }
            )
        ) {
        // Build the indicator UI based on the current state.
        ExpandIndicator(transitionData)
        }
    }
}

Java

class ClickListener implements View.OnClickListener {
    private final Animation expandAnimation;
    private final Animation collapseAnimation;
    private boolean isExpanded;

    ClickListener(Context context) {
        expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
        expandAnimation.setAnimationListener(new Animation.AnimationListener() {

        @Override
        public void onAnimationStart(Animation animation) {
            vibrator.vibrate(
            VibrationEffect.startComposition()
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
                .compose());
        }
        });

        collapseAnimation = AnimationUtils
                .loadAnimation(context, R.anim.collapse);
        collapseAnimation.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {
                vibrator.vibrate(
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
                    .compose());
            }
        });
    }

    @Override
    public void onClick(View view) {
        view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
        isExpanded = !isExpanded;
    }
}

Sallanma (dönerek)

Dokunma teknolojisi ilkelerinden biri, kullanıcıları memnun etmektir. Hoş bir beklenmedik titreşim efektini tanıtmanın eğlenceli bir yolu PRIMITIVE_SPIN kullanmaktır. Bu temel, birden fazla kez çağrıldığında en etkili sonucu verir. Birbirine eklenen birden fazla döndürme, titrek ve dengesiz bir efekt oluşturabilir. Bu efekt, her bir öğeye rastgele bir ölçeklendirme uygulanarak daha da geliştirilebilir. Ayrıca, ardışık döndürme temelleri arasındaki boşlukla da denemeler yapabilirsiniz. Arada boşluk olmadan (0 ms) iki dönüş, sıkı bir dönüş hissi yaratır. Dönüşler arasındaki boşluğu 10 ms'den 50 ms'ye çıkarmak daha rahat bir dönüş hissi sağlar ve video ya da animasyon süresine uyacak şekilde kullanılabilir.

Ardışık dönüşler artık iyi bir şekilde entegre olmadığından ve ayrı efektler gibi görünmeye başladığından 100 ms'den uzun bir boşluk kullanmayın.

Aşağıda, aşağı çekilip bırakıldıktan sonra geri seken esnek bir şekil örneği verilmiştir. Animasyon, zıplama yer değiştirmesiyle orantılı olarak değişen yoğunluklarda oynatılan bir çift döndürme efektiyle geliştirilir.

Zıplayan esnek şekil animasyonu
Giriş titreşim dalga biçiminin grafiği

Şekil 3. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini gösterir.

Kotlin

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableStateOf(0f) }
    var isWobbling by remember { mutableStateOf(false) }

    // Use drag distance to create an animated float value behaving like a spring.
    val dragDistanceAnimated by animateFloatAsState(
        targetValue = if (dragDistance > 0f) dragDistance else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioHighBouncy,
            stiffness = Spring.StiffnessMedium
        ),
    )

    if (isWobbling) {
        LaunchedEffect(Unit) {
            while (true) {
                val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                // Use some sort of minimum displacement so the final few frames
                // of animation don't generate a vibration.
                if (displacement > SPIN_MIN_DISPLACEMENT) {
                    vibrator.vibrate(
                        VibrationEffect.startComposition().addPrimitive(
                            VibrationEffect.Composition.PRIMITIVE_SPIN,
                            nextSpinScale(displacement)
                        ).addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SPIN,
                        nextSpinScale(displacement)
                        ).compose()
                    )
                }
                // Delay the next check for a sufficient duration until the
                // current composition finishes. Note that you can use
                // Vibrator.getPrimitiveDurations API to calculcate the delay.
                delay(VIBRATION_DURATION)
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .draggable(
                onDragStopped = {
                    isWobbling = true
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value.
        WobbleShape(dragDistanceAnimated)
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
    // Generate a random offset in the range [-0.1, +0.1] to be added to the
    // vibration scale so the spin effects have slightly different values.
    val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
    return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}

Java

class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
    private final Random vibrationRandom = new Random(seed);
    private final long lastVibrationUptime;

    @Override
    public void onAnimationUpdate(
        DynamicAnimation animation, float value, float velocity) {
        // Delay the next check for a sufficient duration until the current
        // composition finishes. Note that you can use
        // Vibrator.getPrimitiveDurations API to calculcate the delay.
        if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
            return;
        }

        float displacement = calculateRelativeDisplacement(value);

        // Use some sort of minimum displacement so the final few frames
        // of animation don't generate a vibration.
        if (displacement < SPIN_MIN_DISPLACEMENT) {
            return;
        }

        lastVibrationUptime = SystemClock.uptimeMillis();
        vibrator.vibrate(
        VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
            nextSpinScale(displacement))
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
            nextSpinScale(displacement))
            .compose());
    }

    // Calculate a random scale for each spin to vary the full effect.
    float nextSpinScale(float displacement) {
        // Generate a random offset in the range [-0.1,+0.1] to be added to
        // the vibration scale so the spin effects have slightly different
        // values.
        float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
        return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
    }
}

Zıplama (gürültüyle)

Titreşim efektlerinin bir diğer gelişmiş uygulaması da fiziksel etkileşimleri simüle etmektir. PRIMITIVE_THUD, güçlü ve yankı uyandıran bir etki yaratabilir. Bu etki, genel deneyimi zenginleştirmek için örneğin bir video veya animasyonda etki görselleştirmesiyle birlikte kullanılabilir.

Aşağıda, topun ekranın alt kısmına her çarpışında bir gümbürtü efektiyle zenginleştirilmiş bir top düşürme animasyonu örneği verilmiştir:

Düşürülen bir topun ekranın alt kısmından sekmesi animasyonu.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 4. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini gösterir.

Kotlin

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
    // Control variable for the state of the ball.
    var ballPosition by remember { mutableStateOf(BallPosition.Start) }
    var bounceCount by remember { mutableStateOf(0) }

    // Animation for the bouncing ball.
    var transitionData = updateTransitionData(ballPosition)
    val collisionData = updateCollisionData(transitionData)

    // Ball is about to contact floor, only vibrating once per collision.
    var hasVibratedForBallContact by remember { mutableStateOf(false) }
    if (collisionData.collisionWithFloor) {
        if (!hasVibratedForBallContact) {
        val vibrationScale = 0.7.pow(bounceCount++).toFloat()
        vibrator.vibrate(
            VibrationEffect.startComposition().addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_THUD,
            vibrationScale
            ).compose()
        )
        hasVibratedForBallContact = true
        }
    } else {
        // Reset for next contact with floor.
        hasVibratedForBallContact = false
    }

    Screen() {
        Box(
        Modifier
            .fillMaxSize()
            .clickable {
            if (transitionData.isAtStart) {
                ballPosition = BallPosition.End
            } else {
                ballPosition = BallPosition.Start
                bounceCount = 0
            }
            },
        ) {
        // Build the ball UI based on the current state.
        BouncingBall(transitionData)
        }
    }
}

Java

class ClickListener implements View.OnClickListener {
    @Override
    public void onClick(View view) {
        view.animate()
        .translationY(targetY)
        .setDuration(3000)
        .setInterpolator(new BounceInterpolator())
        .setUpdateListener(new AnimatorUpdateListener() {

            boolean hasVibratedForBallContact = false;
            int bounceCount = 0;

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
            boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
            if (valueBeyondThreshold) {
                if (!hasVibratedForBallContact) {
                float vibrationScale = (float) Math.pow(0.7, bounceCount++);
                vibrator.vibrate(
                    VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_THUD,
                        vibrationScale)
                    .compose());
                hasVibratedForBallContact = true;
                }
            } else {
                // Reset for next contact with floor.
                hasVibratedForBallContact = false;
            }
            }
        });
    }
}

Zarflı titreşim dalga biçimi

Özel titreşim modelleri oluşturma işlemi, titreşim genliğini kontrol etmenize olanak tanıyarak titreşimin kademeli olarak artıp azalmasını sağlar. Bu bölümde, titreşim genliği ve sıklığı üzerinde zaman içinde hassas kontrol sağlayan dalga formu zarflarını kullanarak dinamik dokunsal efektlerin nasıl oluşturulacağı açıklanmaktadır. Bu sayede daha zengin ve ayrıntılı dokunsal deneyimler oluşturabilirsiniz.

Android 16'dan (API düzeyi 36) itibaren sistem, bir kontrol noktası dizisi tanımlayarak titreşim dalga formu zarfı oluşturmak için aşağıdaki API'leri sağlar:

  • BasicEnvelopeBuilder: Donanımdan bağımsız dokunsal efektler oluşturmak için erişilebilir bir yaklaşım.
  • WaveformEnvelopeBuilder: Dokunsal efekt oluşturmaya yönelik daha gelişmiş bir yaklaşımdır. Dokunma teknolojisi donanımı hakkında bilgi sahibi olmayı gerektirir.

Android, zarf efektleri için yedekler sağlamaz. Bu desteğe ihtiyacınız varsa aşağıdaki adımları tamamlayın:

  1. Vibrator.areEnvelopeEffectsSupported() kullanarak belirli bir cihazın zarf efektlerini destekleyip desteklemediğini kontrol edin.
  2. Desteklenmeyen tutarlı deneyimler grubunu devre dışı bırakın veya yedek alternatif olarak özel titreşim desenleri ya da kompozisyonlar kullanın.

Daha fazla temel zarf efekti oluşturmak için aşağıdaki parametrelerle BasicEnvelopeBuilder simgesini kullanın:

  • Titreşimin algılanan gücünü temsil eden \( [0, 1] \)aralığında bir yoğunluk değeri. Örneğin, \( 0.5 \)değeri, cihazın ulaşabileceği küresel maksimum yoğunluğun yarısı olarak algılanır.
  • Titreşimin netliğini temsil eden, \( [0, 1] \)aralığında bir keskinlik değeri. Düşük değerler daha yumuşak titreşimlere, yüksek değerler ise daha keskin bir hisse neden olur.

  • Son kontrol noktasından (yani yoğunluk ve keskinlik çifti) yenisine geçişin ne kadar sürdüğünü milisaniye cinsinden gösteren bir süre değeri.

Aşağıda, 500 ms boyunca düşük perdeden yüksek perdeye, maksimum güce kadar titreşim yoğunluğunu artıran ve ardından 100 ms boyunca tekrar\( 0 \) (kapalı) konumuna getiren bir örnek dalga formu verilmiştir.

vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
    .setInitialSharpness(0.0f)
    .addControlPoint(1.0f, 1.0f, 500)
    .addControlPoint(0.0f, 1.0f, 100)
    .build()
)

Dokunma teknolojisi hakkında daha ileri düzeyde bilginiz varsa WaveformEnvelopeBuilder kullanarak zarf efektlerini tanımlayabilirsiniz. Bu nesneyi kullanırken VibratorFrequencyProfile üzerinden frekans-çıkış hızlandırma eşlemesine (FOAM) erişebilirsiniz.

  • Cihaz FOAM'ı tarafından belirlenen, belirli bir frekansta elde edilebilecek titreşim gücünü temsil eden \( [0, 1] \)aralığındaki bir genlik değeri. Örneğin, \( 0.5 \) değeri, belirli bir frekansta elde edilebilecek maksimum çıkış hızının yarısını oluşturur.
  • Hertz cinsinden belirtilen bir frekans değeri.

  • Son kontrol noktasından yeni kontrol noktasına geçişin ne kadar sürdüğünü milisaniye cinsinden gösteren bir süre değeri.

Aşağıdaki kodda, 400 ms'lik bir titreşim efektini tanımlayan örnek bir dalga formu gösterilmektedir. Bu işlem, 60 Hz'lik sabit bir frekansta, kapalıdan tam güce kadar 50 ms'lik bir genlik artışıyla başlar. Ardından, frekans sonraki 100 ms içinde 120 Hz'e yükselir ve 200 ms boyunca bu seviyede kalır. Son olarak, genlik \( 0 \)'ya düşer ve frekans son 50 ms içinde 60 Hz'e geri döner:

vibrator.vibrate(VibrationEffect.WaveformEnvelopeBuilder()
    .addControlPoint(1.0f, 60f, 50)
    .addControlPoint(1.0f, 120f, 100)
    .addControlPoint(1.0f, 120f, 200)
    .addControlPoint(0.0f, 60f, 50)
    .build()
)

Aşağıdaki bölümlerde, zarflı titreşim dalga biçimleriyle ilgili çeşitli örnekler verilmiştir.

Zıplayan yay

Önceki bir örnekte, fiziksel sıçrama etkileşimlerini simüle etmek için PRIMITIVE_THUD kullanılıyor. Temel zarf API'si, titreşim yoğunluğunu ve keskinliğini hassas bir şekilde ayarlamanıza olanak tanıyarak çok daha ayrıntılı kontrol sunar. Bu sayede, animasyonlu etkinlikleri daha doğru şekilde takip eden dokunsal geribildirimler elde edilir.

Animasyonu, yay ekranın alt kısmından her sıçradığında oynatılan temel bir zarf efektiyle geliştirilmiş, serbest düşen bir yay örneği:

Düşen bir yayın ekranın alt kısmından sekmesini gösteren animasyon.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 5. Zıplayan bir yayı simüle eden titreşim için çıkış hızlanma dalga biçimi grafiği.

@Composable
fun BouncingSpringAnimation() {
  var springX by remember { mutableStateOf(SPRING_WIDTH) }
  var springY by remember { mutableStateOf(SPRING_HEIGHT) }
  var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
  var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
  var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
  var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
  var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
  var bottomBounceCount by remember { mutableIntStateOf(0) }
  var animationStartTime by remember { mutableLongStateOf(0L) }
  var isAnimating by remember { mutableStateOf(false) }

  val (screenHeight, screenWidth) = getScreenDimensions(context)

  LaunchedEffect(isAnimating) {
    animationStartTime = System.currentTimeMillis()
    isAnimating = true

    while (isAnimating) {
      velocityY += GRAVITY
      springX += velocityX.dp
      springY += velocityY.dp

      // Handle bottom collision
      if (springY > screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2) {
        // Set the spring's y-position to the bottom bounce point, to keep it
        // above the floor.
        springY = screenHeight - FLOOR_HEIGHT - SPRING_HEIGHT / 2

        // Reverse the vertical velocity and apply damping to simulate a bounce.
        velocityY *= -BOUNCE_DAMPING
        bottomBounceCount++

        // Calculate the fade-out duration of the vibration based on the
        // vertical velocity.
        val fadeOutDuration =
            ((abs(velocityY) / GRAVITY) * FRAME_DELAY_MS).toLong()

        // Create a "boing" envelope vibration effect that fades out.
        vibrator.vibrate(
            VibrationEffect.BasicEnvelopeBuilder()
                // Starting from zero sharpness here, will simulate a smoother
                // "boing" effect.
                .setInitialSharpness(0f)

                // Add a control point to reach the target intensity and
                // sharpness very quickly.
                .addControlPoint(intensity, sharpness, 20L)

                // Add a control point to fade out the vibration intensity while
                // maintaining sharpness.
                .addControlPoint(0f, sharpness, fadeOutDuration)
                .build()
        )

        // Decrease the intensity and sharpness of the vibration for subsequent
        // bounces, and reduce the multiplier to create a fading effect.
        intensity *= multiplier
        sharpness *= multiplier
        multiplier -= 0.1f
      }

      if (springX > screenWidth - SPRING_WIDTH / 2) {
        // Prevent the spring from moving beyond the right edge of the screen.
        springX = screenWidth - SPRING_WIDTH / 2
      }

      // Check for 3 bottom bounces and then slow down.
      if (bottomBounceCount >= MAX_BOTTOM_BOUNCE &&
            System.currentTimeMillis() - animationStartTime > 1000) {
        velocityX *= 0.9f
        velocityY *= 0.9f
      }

      delay(FRAME_DELAY_MS) // Control animation speed.

      // Determine if the animation should continue based on the spring's
      // position and velocity.
      isAnimating = (springY < screenHeight + SPRING_HEIGHT ||
            springX < screenWidth + SPRING_WIDTH)
        && (velocityX >= 0.1f || velocityY >= 0.1f)
    }
  }

  Box(
    modifier = Modifier
      .fillMaxSize()
      .noRippleClickable {
        if (!isAnimating) {
          resetAnimation()
        }
      }
      .width(screenWidth)
      .height(screenHeight)
  ) {
    DrawSpring(mutableStateOf(springX), mutableStateOf(springY))
    DrawFloor()
    if (!isAnimating) {
      DrawText("Tap to restart")
    }
  }
}

Roket kalkışı

Önceki bir örnekte, zıplayan yay tepkisini simüle etmek için temel zarf API'sinin nasıl kullanılacağı gösterilmektedir. WaveformEnvelopeBuilder, cihazın tam frekans aralığı üzerinde hassas kontrol sağlar ve son derece özelleştirilmiş dokunsal efektlere olanak tanır. Bu verileri FOAM verileriyle birleştirerek titreşimleri belirli frekans özelliklerine göre ayarlayabilirsiniz.

Dinamik titreşim modeli kullanılarak yapılan bir roket fırlatma simülasyonunu gösteren örneği aşağıda bulabilirsiniz. Efekt, desteklenen minimum frekans hızlanma çıkışı olan 0,1 G'den rezonans frekansına kadar gider ve her zaman %10 genlik girişi korunur. Bu sayede, sürüş genliği aynı olsa da efekt makul ölçüde güçlü bir çıkışla başlayabilir ve algılanan yoğunluk ile keskinlik artırılabilir. Rezonansa ulaşıldığında, efekt sıklığı minimuma geri döner. Bu durum, harcanan efor düzeyi ve Keskinliğin azalması olarak algılanır. Bu, uzaya fırlatılmayı taklit ederek önce direnç, ardından serbest bırakılma hissi yaratır.

Temel zarf API'si, rezonans frekansı ve çıkış ivmesi eğrisiyle ilgili cihaza özgü bilgileri soyutladığı için bu efekt mümkün değildir. Keskinliği artırmak, eşdeğer frekansı rezonansın ötesine iterek istenmeyen bir hızlanma düşüşüne neden olabilir.

Ekranın alt kısmından havalanan roket gemi animasyonu.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 6. Bir roket fırlatılışını simüle eden titreşim için çıkış hızlanma dalga biçimi grafiği.

@Composable
fun RocketLaunchAnimation() {
  val context = LocalContext.current
  val screenHeight = remember { mutableFloatStateOf(0f) }
  var rocketPositionY by remember { mutableFloatStateOf(0f) }
  var isLaunched by remember { mutableStateOf(false) }
  val animation = remember { Animatable(0f) }

  val animationDuration = 3000
  LaunchedEffect(isLaunched) {
    if (isLaunched) {
      animation.animateTo(
        1.2f, // Overshoot so that the rocket goes off the screen.
        animationSpec = tween(
          durationMillis = animationDuration,
          // Applies an easing curve with a slow start and rapid acceleration
          // towards the end.
          easing = CubicBezierEasing(1f, 0f, 0.75f, 1f)
        )
      ) {
        rocketPositionY = screenHeight.floatValue * value
      }
      animation.snapTo(0f)
      rocketPositionY = 0f;
      isLaunched = false;
    }
  }

  Box(
    modifier = Modifier
      .fillMaxSize()
      .noRippleClickable {
        if (!isLaunched) {
          // Play vibration with same duration as the animation, using 70% of
          // the time for the rise of the vibration, to match the easing curve
          // defined previously.
          playVibration(vibrator, animationDuration, 0.7f)
          isLaunched = true
        }
      }
      .background(Color(context.getColor(R.color.background)))
      .onSizeChanged { screenHeight.floatValue = it.height.toFloat() }
  ) {
    drawRocket(rocketPositionY)
  }
}

private fun playVibration(
  vibrator: Vibrator,
  totalDurationMs: Long,
  riseBias: Float,
  minOutputAccelerationGs: Float = 0.1f,
) {
  require(riseBias in 0f..1f) { "Rise bias must be between 0 and 1." }

  if (!vibrator.areEnvelopeEffectsSupported()) {
    return
  }

  val resonantFrequency = vibrator.resonantFrequency
  if (resonantFrequency.isNaN()) {
    // Device doesn't have or expose a resonant frequency.
    return
  }

  val startFrequency = vibrator.frequencyProfile?.getFrequencyRange(minOutputAccelerationGs)?.lower ?: return

  if (startFrequency >= resonantFrequency) {
    // Vibrator can't generate the minimum required output at lower frequencies.
    return
  }

  val minDurationMs = vibrator.envelopeEffectInfo.minControlPointDurationMillis
  val rampUpDurationMs = (riseBias * totalDurationMs).toLong() - minDurationMs
  val rampDownDurationMs = totalDurationMs - rampUpDuration - minDurationMs

  vibrator.vibrate(
    VibrationEffect.WaveformEnvelopeBuilder()
      // Quickly reach the target output at the start frequency
      .addControlPoint(0.1f, startFrequency, minDurationMs)
      .addControlPoint(0.1f, resonantFrequency, rampUpDurationMs)
      .addControlPoint(0.1f, startFrequency, rampDownDurationMs)

      // Controlled ramp down to zero to avoid ringing after the vibration.
      .addControlPoint(0.0f, startFrequency, minDurationMs)
      .build()
  )
}

LavaBeats

Roket Kalkışı örneğinde olduğu gibi, WaveformEnvelopeBuilder API, titreşimdeki genlik ve frekans segmentlerinin kontrolünü sağlayarak birçok karmaşık dokunsal efektin tasarlanmasına olanak tanır. Bu tür bir tasarımın bir başka örneği de "canlılık" gibi daha soyut fiziksel duyumların taklit edilmesidir.

Bu, tipik bir elektrokardiyogram (EKG) sinyalinin biyolojik belirteçlerini belirli genlik ve frekanslardaki titreşim segmentleriyle temsil ederek gerçekleştirilebilir. LavaBeats, bir EKG kaydının iki karakteristik segmentinin, zaman gecikmesiyle ayrılmış iki darbe olarak gösterildiği bir örnektir. İlk karakteristik nabız, yüksek genlikli ve kısa süreli keskin bir tepe olarak görünen QRS kompleksidir. İkinci atım, daha düşük genlikli, daha uzun süreli ve daha düzgün şekilli olan T dalgasıdır (Şekil 7'ye bakın).

WaveformEnvelopeBuilder kullanarak, sabit bir birinci-ikinci darbe gecikmesiyle ayrılmış bu iki darbenin çeşitli tekrarlarını oluşturun. İlk darbe, kısa bir süre içinde düşük frekansta başlayıp daha yüksek bir frekansta sona eren bir cıvıltı sinyali olabilir. İkinci darbe, düşük frekanslı bir sinüs dalgasının tek bir periyodu olarak gösterilebilir. İki vuruşu bir ritme dönüştürebilir ve aralarında gecikme olacak şekilde, dakikadaki vuruş (bpm) oranıyla birkaç kez tekrarlayabiliriz. Sonuç, atan bir kalbi andıran bir dokunsal efekt olur.

GitHub'daki dokunsal örnek uygulamamızda LavaBeats özelliğini deneyebilir ve dokunsal efektle aynı ritimde atan bir lav lambası görselleştirmesiyle birlikte efekti hissedebilirsiniz. Ayrıca, iki titreşimin genliklerini, frekanslarını, sürelerini ve gecikmelerini değiştirerek farklı titreşim hisleri oluşturmak için efektin ayarlarını da değiştirebilirsiniz.

Dokunsal kalp atışı deseniyle atan lav lambası animasyonu.
EKG segmentinin ve karakteristik dalga biçimlerinin grafiği

Şekil 7. QRS kompleksi ve T dalgası içeren bir EKG kaydı segmenti

@RequiresApi(Build.VERSION_CODES.BAKLAVA)
private fun createEnvelopeEffect(
beatParameters: List<BeatParameter>
):VibrationEffect =
    VibrationEffect.WaveformEnvelopeBuilder()
        .apply {
            repeat(beatParameters.getNumBeats()) {
                // First pulse chirp
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseStartFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getFirstPulseAmplitude(),
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstPulseDurationMillis().toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )

                // Delay between first and second pulse
                addControlPoint(
                    0f,
                    beatParameters.getFirstPulseEndFreq(),
                    beatParameters.getFirstToSecondPulseDelayMillis().toLong(),
                )

                // Second pulse
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    beatParameters.getSecondPulseAmplitude(),
                    beatParameters.getSecondPulseFreq(),
                    (1_000 / (2f * beatParameters.getSecondPulseFreq())).toLong(),
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    ENVELOPE_RAMP_DURATION_MILLIS,
                )
                addControlPoint(
                    0f,
                    beatParameters.getSecondPulseFreq(),
                    beatParameters.getBeatDelayMillis().toLong(),
                )
            }
        }
        .build()

/** A parameter of a haptic beat effect that represents an ECG signal parameter */
@Stable
data class BeatParameter(
    val description: String = "",
    val value: Float = 0f,
    val range: ClosedFloatingPointRange<Float> = 0f..1f,
    val steps: Int = 0,
    val isFrequencyType: Boolean = false,
)