Özel dokunsal efektler oluştur

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

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

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

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

Özel efektleri uygularken aşağıdakileri göz önünde bulundurun:

  • Efekt için hangi cihaz özellikleri gerekir?
  • Cihaz efekti oynatamadığında ne yapmanız gerekir?

Android dokunsal geri bildirim API referansı, dokunsal geri bildiriminizde yer alan bileşenlerin desteklenip desteklenmediğini nasıl kontrol edeceğinizle ilgili ayrıntılı bilgi sağlar. Böylece uygulamanız genel olarak tutarlı bir deneyim sunabilir.

Kullanım alanınıza bağlı olarak özel efektleri devre dışı bırakmak veya farklı potansiyel özelliklere göre alternatif özel efektler sağlamak isteyebilirsiniz.

Aşağıdaki üst düzey cihaz özelliği sınıflarını planlayın:

  • Haptik ilkel öğeler kullanıyorsanız: Özel efektler için gereken bu ilkel öğeleri destekleyen cihazlar. (Temel öğelerle ilgili ayrıntılar için sonraki bölüme bakın.)

  • Genlik kontrolü olan cihazlar.

  • Temel titreşim desteği olan (açma/kapama) cihazlar. Diğer bir deyişle, genlik kontrolü olmayan cihazlar.

Uygulamanızın dokunsal efekt seçimi bu kategorileri hesaba katıyorsa dokunsal kullanıcı deneyimi, her bir cihaz için tahmin edilebilir olmaya devam etmelidir.

Dokunsal temel öğelerin kullanımı

Android, hem genlik hem de frekans açısından farklılık gösteren çeşitli dokunsal temel öğeler 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 kalıpları 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çimi denir.

Dalga formu efektleri genellikle fark edilebilir ancak sessiz bir ortamda oynatıldığında ani ve uzun titreşimler kullanıcıyı korkutabilir. 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 modeli örnekleri

Aşağıdaki bölümlerde çeşitli titreşim kalıpları ö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ü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 gösterilir. 0, titreşim motorunun "kapalı durumunu", 255 ise cihazın maksimum genliğini temsil eder.
  3. Tekrar dizini: Dalga formunun tekrar etmeye başlayacağı, ilk bağımsız değişkende belirtilen dizideki 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 formu 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 da oynatılabilir. Tekrarlayan dalga formu oluşturmanın yolu, negatif olmayan bir repeat parametresi ayarlamaktır. 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 özellik, kullanıcının onaylaması gereken 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 ö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));
}

Titreşim kompozisyonları oluşturma

Bu bölümde, titreşimleri daha uzun ve karmaşık özel efektler oluşturacak şekilde birleştirmenin yolları açıklanmakta ve daha gelişmiş donanım özelliklerini kullanarak zengin dokunsal deneyimler keşfedilmektedir. Daha geniş bir frekans bant genişliğine sahip dokunsal aktüatörler içeren cihazlarda daha karmaşık dokunsal efektler oluşturmak için genliği ve frekansı değişen efekt kombinasyonlarını kullanabilirsiniz.

Bu sayfada daha önce açıklanan özel titreşim kalıpları oluşturma süreci, titreşim genliğini kontrol ederek kademeli olarak artan ve azalan titreşim efektleri oluşturmayı açıklar. Zengin dokunsal geri bildirim, cihaz titreşim motorunun daha geniş frekans aralığını keşfederek bu konsepti geliştirir ve efekti daha da yumuşak hale getirir. Bu dalga biçimleri, özellikle crescendo veya diminuendo efekti oluşturmada etkilidir.

Bu sayfanın önceki bölümlerinde açıklanan kompozisyon temelleri, cihaz üreticisi tarafından uygulanır. Net dokunsal geri bildirim için dokunsal geri bildirim ilkelerine uygun, net, 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.

Android, desteklenmeyen temel öğeler içeren besteler için yedekler sağlamaz. Bu nedenle, aşağıdaki adımları uygulayın:

  1. Gelişmiş dokunma geri bildiriminizi 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 temel öğesi eksik olan efektleri devre dışı bırakmayın.

Cihazın desteğini kontrol etme hakkında daha fazla bilgiyi aşağıdaki bölümlerde bulabilirsiniz.

Birleşik titreşim efektleri oluşturma

VibrationEffect.Composition ile birleşik titreşim efektleri oluşturabilirsiniz. Aşağıda, yavaşça 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());

Sırayla oynatılacak temel öğeler eklenerek bir beste oluşturulur. Her bir öğe ö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 yoğunluk farkının kolayca algılanabilmesi için 1,4 veya daha fazla güç oranı 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 temel öğeler arasına eklenecek gecikmeleri de belirtebilir. Bu gecikme, önceki öğenin bitişinden itibaren milisaniye cinsinden ifade edilir. Genel olarak, iki temel öğ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 verilmiştir.

Direnç (düşük tiklerle)

Devam eden bir işlemle ilgili faydalı geri bildirimler vermek için temel titreşimin genliğini kontrol edebilirsiniz. Birbirine yakın ölçek değerleri, bir öğenin yumuşak bir crescendo 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 dokunsal geri bildirimlerle zenginleştirilmiş bir görünüm animasyonunun aşağıdaki örneğinde gösterilmektedir.

Aşağı sürüklenen daire 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ı hedef kitleye farklı sürelerde ulaşır. Yalnızca bir tane azaltma primi vardır: PRIMITIVE_QUICK_FALL. Bu temel öğeler, yoğunluğu 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, algısal olarak her zaman yükselen kısımdan daha çok düşen kısmı fark eder. Bu nedenle, yükselen kısmı düşen kısımdan daha kısa hale getirmek, vurguyu düşen kısma kaydırmak için kullanılabilir.

Bu kompozisyonun bir daireyi genişletme ve daraltma için kullanıldığı 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 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 formu, 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önme hareketiyle)

Dokunsal geri bildirim 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 işlev, 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 uygulayarak daha da geliştirilebilir. Ayrıca, ardışık döndürme temelleri arasındaki boşlukla da denemeler yapabilirsiniz. Aralıksız iki dönüş (arada 0 ms) 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 veya animasyon süresine uyacak şekilde kullanılabilir.

Ardışık dönüşümler artık iyi bir şekilde entegre edilmediği ve ayrı efektler gibi görünmeye başladığı için 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, sıçrama yer değiştirmesiyle orantılı olarak değişen yoğunluklarda oynatılan bir çift döndürme efektiyle geliştirilmiştir.

Sekerek ilerleyen 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ından her sektiğinde bir çarpma efektiyle geliştirilmiş bir top düşürme animasyonu örneği verilmiştir:

Düşürülen topun ekranın alt kısmından sekme 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 kalıpları oluşturma işlemi, titreşim genliğini kontrol etmenize olanak tanıyarak titreşimi kademeli olarak artırıp azaltarak yumuşak efektler oluşturmanızı sağlar. 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 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 noktaları dizisi tanımlayarak titreşim dalga biçimi 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 efekt 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 destekten yararlanmak istiyorsanız aşağıdaki adımları tamamlayın:

  1. Belirli bir cihazın zarf efektlerini destekleyip desteklemediğini kontrol etmek için Vibrator.areEnvelopeEffectsSupported() simgesini kullanın.
  2. Desteklenmeyen tutarlı deneyimler grubunu devre dışı bırakın veya yedek alternatif olarak özel titreşim kalıplarını ya da kompozisyonları kullanın.

Daha fazla temel zarf efekti oluşturmak için aşağıdaki parametrelerle birlikte 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şimin yoğunluğunu artıran ve ardından 100 ms boyunca tekrar\( 0 \) (kapalı) konumuna getiren bir dalga biçimi örneği verilmiştir.

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

Dokunsal geri bildirimler hakkında daha ileri düzeyde bilgiye sahipseniz WaveformEnvelopeBuilder kullanarak zarf efektlerini tanımlayabilirsiniz. Bu nesneyi kullanırken VibratorFrequencyProfile aracılığıyla 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ığında bir genlik değeri. Örneğin, \( 0.5 \) değeri, belirli bir sıklıkta 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. Önce 60 Hz'de 50 ms'lik bir genlik artışıyla başlar. Ardından, frekans sonraki 100 ms içinde 120 Hz'ye 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'ye 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 önemli ölçüde daha ayrıntılı kontrol sunar. Bu sayede, animasyonlu etkinlikleri daha doğru şekilde takip eden dokunsal geri bildirimler elde edilir.

Aşağıda, animasyonu geliştirilmiş serbest düşen bir yay örneği verilmiştir. Yay ekranın alt kısmından her sektiğinde temel bir zarf efekti oynatılır:

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

Füze kalkışı

Önceki bir örnekte, zıplayan yay tepkimesini 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 deseni kullanılarak yapılan bir roket fırlatma simülasyonunu gösteren örneği aşağıda bulabilirsiniz. Etki, desteklenen minimum frekans hızlanma çıkışı olan 0,1 G'den rezonans frekansına kadar devam eder ve her zaman %10 genlik girişi korunur. Bu sayede efekt, makul ölçüde güçlü bir çıkışla başlar ve sürüş genliği aynı olmasına rağmen algılanan yoğunluk ve keskinlik artar. Rezonansa ulaşıldığında, efekt sıklığı minimuma geri döner. Bu durum, yoğunluk 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 efektle kullanılamaz. 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 kalkan bir roket gemi animasyonu.
Giriş titreşimi dalga biçiminin grafiği.

Şekil 6. Bir roket fırlatması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 desired 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()
  )
}