Özel dokunsal efektler oluştur

Bu sayfada, bir Android uygulamasında standart titreşim dalga formlarının ötesinde özel efektler oluşturmak için farklı dokunma API'lerinin nasıl kullanılacağına dair örnekler yer almaktadır.

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

Daha fazla örnek 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 dokunma efektleri 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üzeyinde yedek içerir. Bir cihaz, istenen Preset veya temel Envelope için yerel destek sunmuyorsa çerçeve, oynatma sırasında bunu otomatik olarak uygun bir alternatife çevirir. VibrationEffect.Builder ile oluşturulan efektleri oynatmadan önce her bir temel öğenin cihaz özelliklerini manuel olarak kontrol etmeniz gerekmez.
    • İstisna: WaveformEnvelopeBuilder ile oluşturulan gelişmiş dalga formu zarfları, belirli donanım frekansı eşlemelerine (FOAM) bağlı olduğundan otomatik yedeklemeyi desteklemez. Desteklenmiyorsa oynatılmaz.
  • VibrationEffect.Composition: startComposition() API ile oluşturulan kompozisyonlarda otomatik yedekler yoktur. Bir kompozisyon, desteklenmeyen tek bir temel öğe bile içeriyorsa titreşimin tamamı oynatılamaz. vibrator.arePrimitivesSupported() kullanarak özellikleri manuel olarak kontrol etmeniz gerekir.
  • Genlik kontrollü dalga formları: Genlik kontrolü olmayan cihazlarda sıfır olmayan genlikler% 100'e yuvarlanır. vibrator.hasAmplitudeControl() sayfasını inceleyin ve gerekirse açıkça tasarlanmış bir AÇIK/KAPALI desenine geri dönün.

Dokunma temellerinin kullanımı

Android'de hem genlik hem de frekans açısından farklılık gösteren çeşitli dokunma temelleri bulunur. Zengin dokunma efektleri elde etmek için tek bir temel öğeyi tek başına veya birden fazla temel öğeyi birlikte kullanabilirsiniz.

  • İki temel öğe arasında fark edilebilir boşluklar olması için 50 ms veya daha uzun gecikmeler kullanın. Mümkünse temel öğe süresini de hesaba katın.
  • Yoğunluk farkının daha iyi algılanması için 1,4 veya daha yüksek bir oranla farklılık gösteren ölçekler kullanın.
  • Bir primitifin 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 kalıpları 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 kalıpları çalabilir. Bu tür efektlere dalga formu denir.

Dalga formu efektleri genellikle algılanabilir ancak sessiz bir ortamda oynatılırsa ani uzun titreşimler kullanıcıyı ürkütebilir. Hedef genliğe çok hızlı yükselmek de duyulabilir vızıltı seslerine neden olabilir. Genlik geçişlerini yumuşatarak artış ve azalış efektleri oluşturmak için dalga formu desenleri tasarlayın.

Titreşim modeli örnekleri

Aşağıdaki bölümlerde titreşim kalıplarıyla ilgili birkaç örnek verilmiştir:

Artış modeli

Dalga formları, üç parametreyle VibrationEffect olarak temsil edilir:

  1. Zamanlamalar: Her dalga formu segmenti için milisaniye cinsinden süre dizisi.
  2. Genlikler: İlk bağımsız değişkende belirtilen her süre için istenen titreşim genliği. 0 ile 255 arasında bir tam sayı değeriyle temsil edilir. 0, titreşimin "kapalı durumunu", 255 ise cihazın maksimum genliğini temsil eder.
  3. Tekrar dizini: Dalga formunu tekrarlamaya başlamak için ilk bağımsız değişkende belirtilen dizideki dizin veya kalıbı yalnızca bir kez oynatması gerekiyorsa -1.

Aşağıda, iki kez titreşen ve titreşimler arasında 350 ms duraklama olan örnek bir dalga formu verilmiştir. İlk darbe, maksimum genliğe yumuşak bir şekilde yükselir ve ikinci darbe, maksimum genliği korumak için hızlı bir şekilde yükselir. Sonunda durma, negatif tekrar dizini değeriyle tanımlanır.

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
    )
)

Yinelenen desen

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

fun 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.
    val repeatingEffect: VibrationEffect = VibrationEffect.createWaveform(
        timings, amplitudes, repeat
    )
    // repeatingEffect can be used in multiple places.

    vibrator.vibrate(repeatingEffect)
}

fun stopVibrating() {
    vibrator.cancel()
}

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

Yedekli kalıp

Titreşimin genliğini kontrol etmek donanıma bağlı bir özelliktir. Bu özelliğe sahip olmayan düşük kaliteli bir cihazda dalga formu oynatıldığında cihaz, genlik dizisindeki her pozitif giriş için maksimum genlikte titreşir. 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ÇIK/KAPALI deseni tasarlayın.

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'dan (26Q4) itibaren karmaşık titreşim efektleri ve kompozisyonlar oluşturmak için tercih edilen API VibrationEffect.Builder'dir. startTimeMillis ile mutlak bir zaman çizelgesi boyunca ayrı dokunma öğelerini sıralayarak etkileyici dokunma hisleri 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, öğeleri (ön ayarlar, temel zarflar ve birleşik titreşim efektleri dahil) genelinde yerleşik, çerçeve düzeyinde otomatik yedek özelliğine sahiptir. Bu sayede, manuel özellik kontrolleri gerekmeden 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 yedek desteği yoktur.)

Hazır ayarlarla oluşturma

Bir oluşturmaya yaygın, önceden tanımlanmış kısa dokunma titreşimleri (PRESET_CLICK, PRESET_TICK veya PRESET_LOW_TICK gibi) eklemek için VibrationEffect.Preset kullanın. Hazır ayarlar, cihaz üreticisi tarafından dokunma ilkelerine uygun, net, kısa ve hoş bir titreşim sağlamak için 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'deki kısa temel öğelerin yerini alır. Daha uzun veya sürekli dokunma hissi (ör. artırma ve azaltma) için zarf dalga formlarını (PWLE) kullanın.

Her hazır ayara 0.0f ile 1.0f arasında isteğe bağlı bir ölçek atanabilir ve hazır ayar, kompozisyonun başlangıcından itibaren açık 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 ayarlarla oluşturma

Zengin, çok segmentli dokunma desenleri oluşturmak için VibrationEffect.Envelope örneklerini (BasicEnvelopeBuilder veya WaveformEnvelopeBuilder kullanılarak oluşturulur) hazır ayarlarla sorunsuz bir şekilde birleştirebilirsiniz.

Aşağıda, yumuşak bir artış ve azalma zarfı ve ardından keskin bir tıklama ön ayarı çalan 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 kaydırma

Mevcut bir VibrationEffect (VibrationEffect.Composition dahil) nesnesini yeniden kullanmak veya birleştirmek için getEvents() kullanarak VibrationEffect.Event nesnelerinin listesini alın ve addEvents(startTimeShiftMillis, events) kullanarak bunları bir ofsetle ekleyin (veya efekti doğrudan VibrationEffect.Builder(effect) oluşturucuya iletin). VibrationEffect.Composition örneklerini bu şekilde içe aktarırken çerçeve, temel öğelerini otomatik olarak hazır ayarlara dönüştürerek çalışma zamanı yedek 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 yinelenen bir desen eklemek için setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) 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:

  • Kesin olarak artan başlangıç zamanları: Oluşturucuya eklenen her öğenin startTimeMillis değeri, önceki öğenin başlangıç zamanından kesin olarak daha büyük veya ona eşit olmalıdır.
  • Derleme zamanı doğrulaması: Oluşturucu, bilinen öğe sürelerini (veya hazır ayarlar için minimum 1 ms) kullanarak build() zamanında en iyi sonuç doğrulamasını gerçekleştirir. İmkansız bir çakışma tespit edilirse IllegalArgumentException hatası verir.
  • Oynatma sıralı kaydırma: Bir sonraki öğenin başlangıç zamanı geldiğinde önceki bir titreşim öğesi fiziksel olarak hâlâ oynatılıyorsa çerçeve, sonraki öğeyi otomatik olarak mevcut en erken zaman dilimine kaydırır. Bu sayede etkinlikler çakışmaz ve hiçbir titreşim atlanmaz. Ancak etkinlikler birbirine çok yakın planlanırsa hafif zamanlama kayması olabilir. Sapmayı en aza indirmek için ardışık dokunma etkinlikleri arasında yeterli süre (ör. 50 ms veya daha fazla) bırakın.

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

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

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

  1. Gelişmiş dokunma özelliklerinizi etkinleştirmeden önce, cihazın kullandığınız tüm temel öğeleri desteklediğinden emin olun.

  2. Yalnızca temel öğesi eksik olan efektleri değil, desteklenmeyen tutarlı deneyim grubunu devre dışı bırakı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, oluşturma 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ı yedeği sağlar.

Oluşturulan titreşim efektleri

VibrationEffect.Composition ile birleşik titreşim efektleri oluşturabilirsiniz. Aşağıda, yavaşça yükselen bir efektin ardından keskin bir tıklama efektinin örneği verilmiştir:

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

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

Titreşim temel öğelerinde varyant oluşturma

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

Titreşim temelleri arasına boşluk ekleme

Bileşim, ardışık temel öğeler arasına eklenecek gecikmeleri de belirtebilir. Bu gecikme, önceki temel öğenin sonundan itibaren milisaniye cinsinden ifade edilir. Genel olarak, iki temel öğe arasındaki 5 ila 10 ms'lik bir 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. Gecikmeler içeren bir kompozisyon örneği:

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()
)

Hangi temel öğelerin desteklendiğini kontrol etme

Aşağıdaki API'ler, cihazın belirli temel öğeleri destekleyip desteklemediğini doğrulamak için kullanılabilir:

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.
}

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

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

Titreşim kompozisyonu örnekleri

Aşağıdaki bölümlerde, GitHub'daki dokunma örnek uygulamasından alınan titreşim oluşturma örnekleri verilmiştir.

Direnç (düşük tiklerle)

Devam eden bir işlemle ilgili faydalı geri bildirimler iletmek için temel titreşimin genliğini kontrol edebilirsiniz. Birbirine yakın ölçek değerleri, bir primitifin yumuşak bir kreşendo efekti oluşturmak için kullanılabilir. Ardışık temel öğeler arasındaki gecikme de kullanıcı etkileşimine göre dinamik olarak ayarlanabilir. Bu durum, sürükleme hareketiyle kontrol edilen ve dokunma ile desteklenen bir görünüm animasyonunun aşağıdaki örneğinde gösterilmiştir.

Aşağı sürüklenen bir daire animasyonu.
Giriş titreşim dalga formunun grafiği.

Şekil 1. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini temsil eder.

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

    // Only vibrates while the user is dragging
    LaunchedEffect(isDragging) {
        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        while (isDragging) {
            // 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()
            )
        }
    }

    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)
    }
}

Genişletme (yükselme ve alçalma ile)

Algılanan titreşim yoğunluğunu artırmak için iki temel öğe vardır: PRIMITIVE_QUICK_RISE ve PRIMITIVE_SLOW_RISE. Her ikisi de aynı hedefe ulaşır ancak süreleri farklıdır. Azaltma için yalnızca bir temel öğe vardır: PRIMITIVE_QUICK_FALL. Bu temel öğeler, yoğunluğu artıp azalan bir dalga formu segmenti oluşturmak için birlikte daha iyi çalışır. Ölçeklendirilmiş temel öğeleri, aralarındaki genlikte ani sıçramaları önlemek için hizalayabilirsiniz. Bu, genel efekt süresini uzatmak için de iyi bir yöntemdir. İnsanlar algısal olarak her zaman 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 yaparak vurguyu düşen kısma kaydırabilirsiniz.

Bu bileşimin bir daireyi genişletmek ve daraltmak için nasıl kullanıldığına dair bir örneği aşağıda bulabilirsiniz. Yükselme efekti, animasyon sırasında genişleme hissini artırabilir. Yükselme ve düşme efektlerinin kombinasyonu, animasyonun sonundaki çöküşü vurgulamaya yardımcı olur.

Genişleyen bir daire animasyonu.
Giriş titreşim dalga formunun grafiği.

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

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)

    Column(
        Modifier
            .clickable(
                onClick = {
                    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)
    }
}

Sallanma (dönüşlerle)

Temel dokunma ilkelerinden biri, kullanıcıları memnun etmektir. Hoş ve beklenmedik bir titreşim efekti sunmanın eğlenceli bir yolu, PRIMITIVE_SPIN kullanmaktır. Bu temel öğe, birden fazla kez çağrıldığında en etkili şekilde çalışır. Birbirine eklenen birden fazla dönüş, sallantılı ve dengesiz bir etki yaratabilir. Bu etki, her bir temel öğeye rastgele ölçeklendirme uygulanarak daha da artırılabilir. Ayrıca, birbirini takip eden dönüş primitifleri 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 gevşek bir dönüş hissi verir ve bir videonun veya animasyonun süresini eşleştirmek için kullanılabilir.

100 ms'den uzun bir boşluk kullanmayın. Aksi takdirde, art arda gelen dönüşler artık iyi bir şekilde entegre olmaz ve ayrı efektler gibi hissedilmeye başlar.

Aşağıda, aşağı sürüklenip bırakıldıktan sonra geri sıçrayan esnek bir şekil örneği verilmiştir. Animasyon, sıçrama yer değiştirmesiyle orantılı olarak farklı yoğunluklarda oynatılan bir çift döndürme efektiyle geliştirilir.

Zıplayan esnek şekil animasyonu
Giriş titreşim dalga formunun grafiği

Şekil 3. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini temsil eder.

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableFloatStateOf(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
        ),
        // The spring ends on its target. An end value of 0f means that the
        // shape is back at rest, so the wobble is over.
        finishedListener = { endValue -> if (endValue == 0f) isWobbling = false },
    )

    LaunchedEffect(isWobbling) {
        while (isWobbling) {
            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 = {
                    // Wobble only if the shape is far enough from rest to vibrate.
                    // It then springs back to 0f, and the finishedListener ends
                    // the wobble.
                    val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                    isWobbling = displacement > SPIN_MIN_DISPLACEMENT
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value. The
        // lambda defers the read to the draw phase, so the animation doesn't
        // recompose this screen on every frame.
        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)
}

Zıplama (tok seslerle)

Titreşim efektlerinin bir diğer gelişmiş uygulaması da fiziksel etkileşimleri simüle etmektir. PRIMITIVE_THUD, güçlü ve yankılanan bir etki yaratabilir. Bu etki, genel deneyimi zenginleştirmek için bir videoda veya animasyonda bir etkinin görselleştirilmesiyle birleştirilebilir.

Aşağıda, topun ekranın alt kısmından her sekmesinde çalınan bir gümleme efektiyle geliştirilmiş bir top düşürme animasyonu örneği verilmiştir:

Düşen bir topun ekranın alt kısmından sekme animasyonu.
Giriş titreşim dalga formunun grafiği.

Şekil 4. Bu dalga formu, cihazdaki titreşimin çıkış ivmesini temsil eder.

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
    // Control variable for the state of the ball.
    var ballPosition by remember { mutableStateOf(BallPosition.Start) }
    // Plain holder for the bounce count. It's read only in the effect and in
    // the click handler, never in composition, so it needs no snapshot state.
    val bounceCount = remember { intArrayOf(0) }

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

    // Ball is about to contact floor. The effect runs only when the collision
    // state changes, so it vibrates once per collision. Nothing here suspends,
    // so a keyed SideEffect does the job without launching a coroutine.
    SideEffect(collisionData.collisionWithFloor) {
        if (collisionData.collisionWithFloor) {
            val vibrationScale = 0.7.pow(bounceCount[0]++).toFloat()
            vibrator.vibrate(
                VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_THUD,
                    vibrationScale
                ).compose()
            )
        }
    }

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

Zarflı titreşim dalga formu

Özel titreşim modelleri oluşturma işlemi, titreşim genliğini kontrol ederek yumuşak bir şekilde artan ve azalan efektler oluşturmanıza olanak tanır. Bu bölümde, titreşim genliği ve frekansının zaman içinde hassas bir şekilde kontrol edilmesini sağlayan dalga formu zarflarını kullanarak dinamik dokunma efektlerinin nasıl oluşturulacağı açıklanmaktadır. Bu sayede daha zengin ve ayrıntılı dokunma deneyimleri 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şturmaya yönelik erişilebilir bir yaklaşım.
  • WaveformEnvelopeBuilder: Dokunsal efektler oluşturmaya yönelik daha gelişmiş bir yaklaşımdır. Dokunsal 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ı uygulayı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 kalıpları ya da oluşturmalar kullanın.

Daha basit zarf efektleri oluşturmak için BasicEnvelopeBuilder öğesini şu parametrelerle kullanın:

  • Titreşimin algılanan gücünü temsil eden, \( [0, 1] \)aralığında bir harcanan efor düzeyi değeri. Örneğin, \( 0.5 \)değeri, cihazın ulaşabileceği global maksimum harcanan efor düzeyinin 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şimler sağlarken yüksek değerler daha keskin bir his yaratır.

  • Son kontrol noktasından (yani bir yoğunluk ve keskinlik çifti) yeni kontrol noktasına geçiş için geçen süreyi milisaniye cinsinden temsil eden bir süre değeri.

Aşağıda, 500 ms boyunca düşük perdeden yüksek perdeye, maksimum güçte titreşime kadar harcanan efor düzeyini artıran ve ardından 100 ms boyunca tekrar\( 0 \) (kapalı) değerine düşüren örnek bir 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 bilgiye sahipseniz WaveformEnvelopeBuilder kullanarak zarf efektleri tanımlayabilirsiniz. Bu nesneyi kullanırken VibratorFrequencyProfile aracılığıyla frekanstan çıkış hızlandırmasına eşleme (FOAM) özelliğine erişebilirsiniz.

  • Cihazın FOAM'u 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ış ivmesinin yarısını üretir.
  • Hertz cinsinden belirtilen bir frekans değeri.

  • Son kontrol noktasından yeni kontrol noktasına geçiş için geçen süreyi milisaniye cinsinden temsil eden 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 efekt, 50 ms'lik bir genlik rampasıyla başlar ve kapalıdan tam seviyeye, sabit 60 Hz'de devam eder. Ardından, frekans sonraki 100 ms boyunca 120 Hz'e yükselir ve 200 ms boyunca bu seviyede kalır. Son olarak, genlik \( 0 \)değerine düşer ve frekans son 50 ms'de 60 Hz'e 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 formlarıyla ilgili birkaç örnek verilmiştir.

Zıplayan yay

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

Aşağıda, serbest düşüş yapan bir yayın örneği verilmiştir. Animasyon, yay ekranın alt kısmından her sektiğinde oynatılan temel bir zarf efektiyle geliştirilmiştir:

Düşen bir yayın ekranın alt kısmından sekme animasyonu.
Giriş titreşim dalga formunun grafiği.

Şekil 5. Zıplayan bir yayı simüle eden titreşimin çıkış ivmesi dalga formu grafiği.

@Composable
fun BouncingSpringAnimation() {
    // The spring's height above the floor: 1f is the drop height, 0f is the floor.
    val springHeight = remember { Animatable(1f) }
    var isAnimating by remember { mutableStateOf(true) }

    LaunchedEffect(isAnimating) {
        if (!isAnimating) return@LaunchedEffect
        springHeight.snapTo(1f)

        // Each bounce reaches a fraction of the previous height.
        var bounceHeight = 1f
        while (bounceHeight > MIN_BOUNCE_HEIGHT) {
            // Fall to the floor, speeding up like a falling object. A fall from
            // a greater height takes longer.
            val fallDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
            springHeight.animateTo(0f, tween(fallDurationMs, easing = EaseInQuad))

            // The spring hits the floor: play a "boing" envelope effect that fades
            // out. A higher bounce gives a stronger and sharper vibration.
            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(bounceHeight, bounceHeight * MAX_SHARPNESS, 20L)
                    // Add a control point to fade out the vibration intensity while
                    // maintaining sharpness.
                    .addControlPoint(0f, bounceHeight * MAX_SHARPNESS, fallDurationMs.toLong())
                    .build()
            )

            // Bounce back up to a lower height, slowing down like a thrown object.
            // When the next bounce is too small, the spring stays on the floor.
            bounceHeight *= BOUNCE_DAMPING
            if (bounceHeight > MIN_BOUNCE_HEIGHT) {
                val riseDurationMs = (DROP_DURATION_MS * sqrt(bounceHeight)).toInt()
                springHeight.animateTo(bounceHeight, tween(riseDurationMs, easing = EaseOutQuad))
            }
        }
        isAnimating = false
    }

    Box(
        modifier = Modifier
            .fillMaxSize()
            .clickable(enabled = !isAnimating) { isAnimating = true }
            .drawBehind {
                // Read the height in the draw phase, and scale it to the distance
                // between the top of the box and the floor.
                val floorTop = size.height - FLOOR_HEIGHT.toPx()
                val dropDistance = floorTop - SPRING_HEIGHT.toPx()
                drawSpring(topY = dropDistance * (1f - springHeight.value))
                drawFloor(top = floorTop)
            }
    ) {
        if (!isAnimating) {
            Text("Tap to restart", Modifier.align(Alignment.Center))
        }
    }
}

Roket fırlatma

Ö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 tüm frekans aralığı üzerinde hassas kontrol sağlayarak son derece özelleştirilmiş dokunma efektleri sunar. Bu verileri FOAM verileriyle birleştirerek titreşimleri belirli frekans özelliklerine göre özelleştirebilirsiniz.

Dinamik titreşim deseni kullanan bir roket fırlatma simülasyonu örneğini burada bulabilirsiniz. Efekt, desteklenen minimum frekans hızlanma çıkışı olan 0,1 G'den rezonans frekansına geçer ve her zaman %10 genlik girişi sağlar. Bu sayede efekt, makul düzeyde güçlü bir çıkışla başlar ve sürüş genliği aynı olsa bile algılanan yoğunluk ve keskinlik artar. Rezonansa ulaşıldığında efekt frekansı tekrar minimuma iner. Bu durum, harcanan efor düzeyinin ve keskinliğin azalması olarak algılanır. Bu, önce bir direnç, ardından serbest kalma hissi yaratarak uzaya fırlatılma hissini taklit eder.

Bu efekt, temel zarf API'si ile mümkün değildir. Çünkü bu API, rezonans frekansı ve çıkış hızlanma eğrisiyle ilgili cihaza özgü bilgileri soyutlar. Keskinliği artırmak, eşdeğer frekansı rezonansın ötesine taşıyarak istenmeyen bir hızlanma düşüşüne neden olabilir.

Ekranın alt kısmından kalkan bir roket gemi animasyonu.
Giriş titreşim dalga formunun grafiği.

Şekil 6. Roket fırlatmayı simüle eden bir titreşimin çıkış ivmesi dalga formu grafiği.

@Composable
fun RocketLaunchAnimation() {
    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)
                )
            )
            animation.snapTo(0f)
            isLaunched = false
        }
    }

    Canvas(
        modifier = Modifier
            .fillMaxSize()
            .clickable {
                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.toLong(), 0.7f)
                    isLaunched = true
                }
            }
            .background(colorResource(R.color.background))
    ) {
        // Read the animation in the draw phase, and scale it to the canvas height.
        drawRocket(size.height * animation.value)
    }
}

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 - rampUpDurationMs - 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 Fırlatma örneğinde olduğu gibi WaveformEnvelopeBuilder API'si, titreşimdeki genlik ve frekans segmentlerinin kontrol edilmesini sağlayarak birçok karmaşık dokunma efektinin tasarlanmasına olanak tanır. Bu tür bir tasarımın başka bir örneği, "canlılık" gibi daha soyut fiziksel duyumların taklit edilmesidir.

Bu, tipik bir elektrokardiyogram (EKG) sinyalinin biyobelirteç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 temsil edildiği bir örnektir. İlk karakteristik nabız, yüksek genlikli ve kısa süreli keskin bir tepe noktası olarak görünen QRS kompleksidir. İkinci dalga ise daha düşük genliğe, daha uzun süreye ve daha pürüzsüz bir şekle sahip olan T dalgasıdır (bkz. Şekil 7).

WaveformEnvelopeBuilder kullanarak bu iki darbenin çeşitli tekrarlarını oluşturun. Bu tekrarlar, sabit bir ilk-ikinci darbe gecikmesiyle ayrılır. İlk darbe, kısa bir süre içinde düşük frekansta başlayıp daha yüksek 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 nabız atışını birleştirip bir ritim oluşturabilir ve bu ritmi, tipik bir dakikadaki vuruş sayısı (bpm) oranını takip ederek aralarda gecikmeyle birkaç kez tekrarlayabiliriz. Sonuç, kalp atışına benzeyen bir dokunma efektidir.

GitHub'daki dokunma örneği uygulamamızda LavaBeats efektini deneyebilir ve dokunma efektiyle aynı ritimde atan lav lambası görselleştirmesiyle birlikte efektin etkisini hissedebilirsiniz. İki darbenin genliklerini, frekanslarını, sürelerini ve gecikmelerini değiştirerek farklı vuruş 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 formlarının grafiği

Şekil 7. QRS kompleksi ve T dalgasıyla bir EKG kaydının segmenti

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 */
data class BeatParameter(
    val description: String = "",
    val value: Float = 0f,
    val range: ClosedFloatingPointRange<Float> = 0f..1f,
    val steps: Int = 0,
    val isFrequencyType: Boolean = false,
)