पसंद के मुताबिक हैप्टिक इफ़ेक्ट बनाएं

इस पेज पर, Android ऐप्लिकेशन में पसंद के मुताबिक इफ़ेक्ट बनाने के लिए, अलग-अलग हैप्टिक्स एपीआई इस्तेमाल करने के उदाहरण दिए गए हैं. इस पेज पर दी गई ज़्यादातर जानकारी, वाइब्रेशन ऐक्चुएटर के काम करने के तरीके के बारे में अच्छी जानकारी पर आधारित है. इसलिए, हमारा सुझाव है कि आप वाइब्रेशन ऐक्चुएटर प्राइमर पढ़ें.

इस पेज पर ये उदाहरण दिए गए हैं.

ज़्यादा उदाहरणों के लिए, इवेंट में हैप्टिक फ़ीडबैक जोड़ना लेख पढ़ें. साथ ही, हैप्टिक्स डिज़ाइन के सिद्धांतों का हमेशा पालन करें.

डिवाइस के साथ काम करने की सुविधा को मैनेज करने के लिए, फ़ॉलबैक का इस्तेमाल करना

कोई भी कस्टम इफ़ेक्ट लागू करते समय, इन बातों का ध्यान रखें:

  • इफ़ेक्ट के लिए डिवाइस में कौनसी सुविधाएं होनी चाहिए
  • अगर डिवाइस पर इफ़ेक्ट नहीं चल रहा है, तो क्या करें

Android haptics API रेफ़रंस में, haptics से जुड़े कॉम्पोनेंट के साथ काम करने की सुविधा की जांच करने का तरीका बताया गया है. इससे आपके ऐप्लिकेशन को एक जैसा अनुभव देने में मदद मिलती है.

इस्तेमाल के उदाहरण के आधार पर, हो सकता है कि आप कस्टम इफ़ेक्ट बंद करना चाहें या अलग-अलग संभावित क्षमताओं के आधार पर, वैकल्पिक कस्टम इफ़ेक्ट उपलब्ध कराना चाहें.

डिवाइस की सुविधाओं के लिए, इन हाई-लेवल क्लास के लिए प्लान बनाएं:

  • अगर हैप्टिक प्राइमटिव का इस्तेमाल किया जा रहा है, तो ऐसे डिवाइस जिन पर कस्टम इफ़ेक्ट के लिए ज़रूरी प्राइमटिव काम करते हैं. (प्राइमिटिव के बारे में जानकारी के लिए अगला सेक्शन देखें.)

  • ऐम्प्ल्यट्यूड कंट्रोल वाले डिवाइस.

  • ऐसे डिवाइस जिनमें वाइब्रेशन की सामान्य सुविधा (चालू/बंद) मौजूद है. दूसरे शब्दों में, जिन डिवाइसों में वाइब्रेशन के वॉल्यूम को कंट्रोल करने की सुविधा नहीं है.

अगर आपके ऐप्लिकेशन में, वाइब्रेशन के इन इफ़ेक्ट का इस्तेमाल किया जाता है, तो किसी भी डिवाइस पर, वाइब्रेशन के हिसाब से उपयोगकर्ता अनुभव का अनुमान लगाया जा सकता है.

हैप्टिक प्राइमिटिव का इस्तेमाल

Android में कई तरह के हप्टिक प्राइमिटिव शामिल हैं, जो ऐम्प्ल्यट्यूड और फ़्रीक्वेंसी, दोनों में अलग-अलग होते हैं. बेहतरीन हैप्टिक इफ़ेक्ट पाने के लिए, एक प्रिमिटिव का इस्तेमाल किया जा सकता है या एक से ज़्यादा प्रिमिटिव को एक साथ इस्तेमाल किया जा सकता है.

  • दो प्राइमिटिव के बीच के अंतर को साफ़ तौर पर दिखाने के लिए, 50 मिलीसेकंड या उससे ज़्यादा की देरी का इस्तेमाल करें. अगर हो सके, तो प्राइमिटिव के चलने का कुल समय भी ध्यान में रखें.
  • ऐसे स्केल का इस्तेमाल करें जिनका रेशियो 1.4 या उससे ज़्यादा हो, ताकि तीव्रता में अंतर को बेहतर तरीके से समझा जा सके.
  • प्राइमिटिव का कम, मीडियम, और ज़्यादा इंटेंसिटी वाला वर्शन बनाने के लिए, 0.5, 0.7, और 1.0 स्केल का इस्तेमाल करें.

कस्टम वाइब्रेशन पैटर्न बनाना

वाइब्रेशन पैटर्न का इस्तेमाल, अक्सर ध्यान खींचने वाले हैप्टिक्स में किया जाता है. जैसे, सूचनाएं और रिंगटोन. Vibrator सेवा, लंबे समय तक वाइब्रेशन पैटर्न चला सकती है. इससे, समय के साथ वाइब्रेशन का ऐम्प्ल्यफ़िड बदलता है. ऐसे इफ़ेक्ट को वेवफ़ॉर्म कहा जाता है.

वेवफ़ॉर्म इफ़ेक्ट आसानी से समझे जा सकते हैं. हालांकि, अगर शांत माहौल में इन्हें अचानक और लंबे समय तक चलाया जाता है, तो उपयोगकर्ता को डर लग सकता है. टारगेट ऐम्प्ल्यफ़िकेशन पर बहुत तेज़ी से पहुंचने पर, आवाज़ भी हो सकती है. हमारा सुझाव है कि वॉवफ़ॉर्म पैटर्न डिज़ाइन करते समय, ऐम्प्ल्यट्यूड ट्रांज़िशन को धीमा करें, ताकि रैंप अप और रैंप डाउन इफ़ेक्ट बनाए जा सकें.

सैंपल: रैंप-अप पैटर्न

वेवफ़ॉर्म को तीन पैरामीटर के साथ VibrationEffect के तौर पर दिखाया जाता है:

  1. समय: हर वेवफ़ॉर्म सेगमेंट के लिए, मिलीसेकंड में अवधि का कलेक्शन.
  2. ऐम्प्लिटी: पहले आर्ग्युमेंट में बताई गई हर अवधि के लिए, वाइब्रेशन का ऐम्प्लिटी. इसे 0 से 255 के बीच की कोई पूर्णांक वैल्यू दी जाती है. 0 का मतलब है कि वाइब्रेटर "बंद" है और 255 का मतलब है कि डिवाइस का ऐम्प्लिटी सबसे ज़्यादा है.
  3. दोहराने का इंडेक्स: वेवफ़ॉर्म को दोहराने के लिए, पहले आर्ग्युमेंट में बताए गए कलेक्शन में इंडेक्स या -1, अगर पैटर्न को सिर्फ़ एक बार चलाना है.

यहां एक उदाहरण के तौर पर, एक वेवफ़ॉर्म दिया गया है, जिसमें दो बार स्पंदन होते हैं और दोनों स्पंदनों के बीच 350 मिलीसेकंड का विराम होता है. पहला पल्ज़, ऐम्प्ल्यट्यूड के ज़्यादा से ज़्यादा लेवल तक धीरे-धीरे बढ़ता है. वहीं, दूसरा पल्ज़, ऐम्प्ल्यट्यूड के ज़्यादा से ज़्यादा लेवल को बनाए रखने के लिए तेज़ी से बढ़ता है. आखिर में रुकने की प्रक्रिया, नेगेटिव रीपीट इंडेक्स वैल्यू से तय होती है.

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 // Do not 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; // Do not repeat.

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

उदाहरण: बार-बार होने वाला पैटर्न

वेवफ़ॉर्म को तब तक बार-बार चलाया जा सकता है, जब तक कि उन्हें रद्द नहीं किया जाता. दोहराए जाने वाले वेवफ़ॉर्म को बनाने का तरीका यह है कि किसी ऐसे ‘repeat’ पैरामीटर को सेट करें जो शून्य से बड़ा हो. दोहराए जाने वाले वेवफ़ॉर्म को चलाने पर, वाइब्रेशन तब तक जारी रहता है, जब तक कि सेवा में इसे साफ़ तौर पर रद्द नहीं किया जाता:

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

यह उन इवेंट के लिए काफ़ी मददगार है जो समय-समय पर होते हैं और जिन्हें स्वीकार करने के लिए उपयोगकर्ता की कार्रवाई की ज़रूरत होती है. इस तरह के इवेंट के उदाहरणों में, इनकमिंग फ़ोन कॉल और ट्रिगर किए गए अलार्म शामिल हैं.

सैंपल: फ़ॉलबैक वाला पैटर्न

वाइब्रेशन के ऐम्प्ल्यफ़िड को कंट्रोल करने की सुविधा, हार्डवेयर पर निर्भर करती है. इस सुविधा के बिना, कम-एंड डिवाइस पर वेवफ़ॉर्म चलाने पर, ऐम्प्लिटी ऐरे में हर पॉज़िटिव एंट्री के लिए, डिवाइस सबसे ज़्यादा ऐम्प्लिटी पर वाइब्रेट करता है. अगर आपके ऐप्लिकेशन को ऐसे डिवाइसों पर काम करना है, तो हमारा सुझाव है कि आप यह पक्का करें कि उस स्थिति में चलाए जाने पर, आपका पैटर्न बज़िंग इफ़ेक्ट न जनरेट करे. इसके अलावा, आपके पास एक आसान चालू/बंद पैटर्न डिज़ाइन करने का विकल्प भी है, जिसे फ़ॉलबैक के तौर पर चलाया जा सकता है.

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

वाइब्रेशन कंपोज़िशन बनाना

इस सेक्शन में, इन इफ़ेक्ट को ज़्यादा लंबे और जटिल कस्टम इफ़ेक्ट में कंपोज़ करने के तरीके बताए गए हैं. साथ ही, इसमें हार्डवेयर की ज़्यादा बेहतर सुविधाओं का इस्तेमाल करके, बेहतर हैप्टिक्स को एक्सप्लोर करने के बारे में भी बताया गया है. ऐसे डिवाइसों पर ज़्यादा जटिल हैप्टिक इफ़ेक्ट बनाने के लिए, ऐम्प्ल्यट्यूड और फ़्रीक्वेंसी में बदलाव करने वाले इफ़ेक्ट के कॉम्बिनेशन का इस्तेमाल किया जा सकता है जिनमें हैप्टिक ऐक्चुएटर होते हैं और जिनकी फ़्रीक्वेंसी बैंडविड्थ ज़्यादा होती है.

इस पेज पर पहले बताई गई, कस्टम वाइब्रेशन पैटर्न बनाने की प्रोसेस में, वाइब्रेशन के ऐम्प्ल्यफ़िड को कंट्रोल करने का तरीका बताया गया है. इससे, वाइब्रेशन को धीरे-धीरे कम या ज़्यादा करने के बेहतर असर बनाए जा सकते हैं. रिच हैप्टिक्स इस कॉन्सेप्ट को बेहतर बनाता है. इसके लिए, डिवाइस के वाइब्रेटर की ज़्यादा फ़्रीक्वेंसी रेंज का इस्तेमाल किया जाता है, ताकि असर को और भी बेहतर बनाया जा सके. ये वेवफ़ॉर्म, ज़ोर से शुरू होने या धीरे-धीरे कम होने वाले इफ़ेक्ट बनाने में खास तौर पर असरदार होते हैं.

इस पेज पर पहले बताए गए कॉम्पोज़िशन प्राइमिटिव, डिवाइस मैन्युफ़ैक्चरर लागू करता है. ये आपको साफ़, कम अवधि वाला, और सुखद वाइब्रेशन देते हैं. यह वाइब्रेशन, हैप्टिक्स के सिद्धांतों के मुताबिक होता है. इन सुविधाओं और उनके काम करने के तरीके के बारे में ज़्यादा जानकारी के लिए, वाइब्रेशन ऐक्चुएटर के बारे में जानकारी लेख पढ़ें.

Android, काम न करने वाले प्राइमिटिव वाले कंपोज़िशन के लिए फ़ॉलबैक उपलब्ध नहीं कराता. हमारा सुझाव है कि आप यह तरीका अपनाएं:

  1. बेहतर वाइब्रेशन की सुविधा चालू करने से पहले, देख लें कि आपका डिवाइस उन सभी प्राइमिटिव के साथ काम करता है जिनका इस्तेमाल किया जा रहा है.

  2. सिर्फ़ उन इफ़ेक्ट को नहीं, बल्कि उन सभी इफ़ेक्ट को बंद करें जिनके लिए प्राइमिटिव उपलब्ध नहीं है. डिवाइस के लिए सहायता उपलब्ध है या नहीं, यह देखने का तरीका यहां बताया गया है.

VibrationEffect.Composition की मदद से, वाइब्रेशन के कंपोज किए गए इफ़ेक्ट बनाए जा सकते हैं. यहां धीरे-धीरे बढ़ने वाले इफ़ेक्ट के बाद, तेज़ क्लिक इफ़ेक्ट का उदाहरण दिया गया है:

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

कॉम्पोज़िशन बनाने के लिए, क्रम से चलाए जाने वाले प्राइमिटिव जोड़े जाते हैं. हर प्राइमिटिव को स्केल भी किया जा सकता है, ताकि आप उनमें से हर एक से जनरेट होने वाले वाइब्रेशन के ऐम्प्ल्यट्यूड को कंट्रोल कर सकें. स्केल को 0 से 1 के बीच की वैल्यू के तौर पर तय किया जाता है. इसमें 0, असल में उस कम से कम ऐम्प्ल्यट्यूड पर मैप होता है जिस पर उपयोगकर्ता को यह प्राइमिटिव महसूस हो सकता है.

अगर आपको एक ही प्रिमिटिव का कम और ज़्यादा इंटेंसिटी वाला वर्शन बनाना है, तो हमारा सुझाव है कि स्केल का अनुपात 1.4 या उससे ज़्यादा हो. इससे, इंटेंसिटी में अंतर को आसानी से समझा जा सकता है. एक ही प्राइमिटिव के लिए, तीव्रता के तीन से ज़्यादा लेवल बनाने की कोशिश न करें, क्योंकि वे एक-दूसरे से अलग नहीं दिखते. उदाहरण के लिए, किसी प्रिमिटिव का लो, मीडियम, और हाई इंटेंसिटी वाला वर्शन बनाने के लिए, 0.5, 0.7, और 1.0 स्केल का इस्तेमाल करें.

कॉम्पोज़िशन में, एक-दूसरे के बाद आने वाले प्राइमिटिव के बीच में जोड़ी जाने वाली देरी के बारे में भी बताया जा सकता है. यह देरी, पिछले प्रिमिटिव के खत्म होने के बाद मिलीसेकंड में दिखाई जाती है. आम तौर पर, दो प्राइमिटिव के बीच 5 से 10 मिलीसेकंड का अंतर, पता लगाने के लिए बहुत कम होता है. अगर आपको दो प्राइमिटिव के बीच का अंतर साफ़ तौर पर दिखाना है, तो 50 मिलीसेकंड या उससे ज़्यादा के गैप का इस्तेमाल करें. यहां देरी वाले कंपोज़िशन का उदाहरण दिया गया है:

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

किसी खास प्राइमिटिव के लिए डिवाइस पर काम करने की पुष्टि करने के लिए, इन एपीआई का इस्तेमाल किया जा सकता है:

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

एक से ज़्यादा प्राइमिटिव की जांच करके, यह तय किया जा सकता है कि डिवाइस के साथ काम करने के लेवल के आधार पर, किन प्राइमिटिव को कंपोज़ किया जाए:

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

उदाहरण: Resist (कम टिक के साथ)

किसी कार्रवाई के बारे में काम का फ़ीडबैक देने के लिए, प्राइमटिव वाइब्रेशन के ऐम्प्ल्यट्यूड को कंट्रोल किया जा सकता है. स्केल की वैल्यू को एक-दूसरे के करीब रखकर, प्राइमिटिव का स्मूद क्रेस्केंडो इफ़ेक्ट बनाया जा सकता है. उपयोगकर्ता के इंटरैक्शन के आधार पर, एक के बाद एक प्राइमटिव के बीच की देरी को डाइनैमिक तरीके से भी सेट किया जा सकता है. इसकी जानकारी, यहां दिए गए उदाहरण में दी गई है. इसमें, खींचने और छोड़ने वाले जेस्चर से कंट्रोल किए जाने वाले व्यू ऐनिमेशन के साथ-साथ, हैप्टिक्स की सुविधा का इस्तेमाल किया गया है.

सर्कल को नीचे की ओर खींचने का ऐनिमेशन
इनपुट वाइब्रेशन वेवफ़ॉर्म का प्लॉट

पहली इमेज. इस वेवफ़ॉर्म से, किसी डिवाइस पर वाइब्रेशन के आउटपुट ऐक्सेलरेशन के बारे में पता चलता है.

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

उदाहरण: बड़ा करना (उतार-चढ़ाव के साथ)

वाइब्रेशन की इंटेंसिटी बढ़ाने के लिए, दो प्राइमटिव हैं: PRIMITIVE_QUICK_RISE और PRIMITIVE_SLOW_RISE. दोनों एक ही टारगेट तक पहुंचते हैं, लेकिन अलग-अलग अवधि में. रैंप-डाउन के लिए, सिर्फ़ एक प्राइमिटिव है, PRIMITIVE_QUICK_FALL. ये प्राइमिटिव, एक साथ मिलकर ऐसा वेवफ़ॉर्म सेगमेंट बनाते हैं जो पहले धीरे-धीरे बढ़ता है और फिर धीरे-धीरे कम हो जाता है. स्केल किए गए प्राइमिटिव को अलाइन करके, उनके बीच अचानक अम्प्ल्यट्यूड में होने वाली बढ़ोतरी को रोका जा सकता है. यह पूरे इफ़ेक्ट की अवधि को बढ़ाने के लिए भी अच्छा काम करता है. आम तौर पर, लोग वीडियो के आखिर में दिखने वाले हिस्से की तुलना में, शुरुआत में दिखने वाले हिस्से पर ज़्यादा ध्यान देते हैं. इसलिए, वीडियो के आखिर में दिखने वाले हिस्से को छोटा करके, शुरुआत में दिखने वाले हिस्से पर ज़्यादा ध्यान दिया जा सकता है.

यहां सर्कल को बड़ा और छोटा करने के लिए, इस कॉम्पोज़िशन के इस्तेमाल का उदाहरण दिया गया है. ऐनिमेशन के दौरान, 'बढ़ने' वाले इफ़ेक्ट से, आइटम के बड़े होने की भावना बढ़ सकती है. बढ़ने और गिरने वाले इफ़ेक्ट के कॉम्बिनेशन से, ऐनिमेशन के आखिर में घटने पर ज़्यादा असर पड़ता है.

बड़ा होने वाले सर्कल का ऐनिमेशन
इनपुट वाइब्रेशन वेवफ़ॉर्म का प्लॉट

दूसरी इमेज. इस वेवफ़ॉर्म से, किसी डिवाइस पर वाइब्रेशन के आउटपुट ऐक्सेलरेशन के बारे में पता चलता है.

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

सैंपल: वॉबल (स्पिन के साथ)

हैप्टिक्स के सिद्धांतों में से एक यह है कि उपयोगकर्ताओं को खुशी मिले. PRIMITIVE_SPIN का इस्तेमाल करके, वीडियो में अचानक होने वाले वाइब्रेशन इफ़ेक्ट को मज़ेदार तरीके से दिखाया जा सकता है. यह प्राइमिटिव सबसे ज़्यादा असरदार तब होता है, जब इसे एक से ज़्यादा बार कॉल किया जाता है. एक साथ कई स्‍पिन करने पर, वॉबल करने वाला और अस्थिर इफ़ेक्ट दिख सकता है. हर प्राइमिटिव पर थोड़ी-बहुत रैंडम स्केलिंग लागू करके, इस इफ़ेक्ट को और बेहतर बनाया जा सकता है. आपके पास, एक-दूसरे के बाद आने वाले स्पिन प्राइमटिव के बीच के अंतर के साथ भी एक्सपेरिमेंट करने का विकल्प है. बिना किसी गैप (बीच में 0 मि॰से॰) के दो स्पिन करने पर, तेज़ी से घूमने का एहसास होता है. इंटर-स्पिन गैप को 10 से 50 मिलीसेकंड तक बढ़ाने पर, स्पिनिंग की गति धीमी हो जाती है. इसका इस्तेमाल, वीडियो या ऐनिमेशन की अवधि से मैच करने के लिए किया जा सकता है.

हमारा सुझाव है कि 100 मिलीसेकंड से ज़्यादा के गैप का इस्तेमाल न करें. ऐसा करने पर, एक के बाद एक होने वाले स्‍पिन ठीक से इंटिग्रेट नहीं होते और अलग-अलग इफ़ेक्ट की तरह लगने लगते हैं.

यहां ऐसे इलास्टिक आकार का उदाहरण दिया गया है जिसे नीचे की ओर खींचने और फिर छोड़ने पर, वह वापस अपनी जगह पर आ जाता है. इस एनिमेशन को स्पिन इफ़ेक्ट की मदद से बेहतर बनाया गया है. इफ़ेक्ट की तीव्रता, बाउंस डिसप्लेसमेंट के हिसाब से अलग-अलग होती है.

किसी इलास्टिक आकार के उछलने का ऐनिमेशन
इनपुट वाइब्रेशन वेवफ़ॉर्म का प्लॉट

तीसरी इमेज. इस वेवफ़ॉर्म से, किसी डिवाइस पर वाइब्रेशन के आउटपुट ऐक्सेलरेशन के बारे में पता चलता है.

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

सैंपल: बाउंस (थपथप की आवाज़ के साथ)

वाइब्रेशन इफ़ेक्ट का एक और बेहतर इस्तेमाल, किसी गतिविधि को सिम्युलेट करना है. PRIMITIVE_THUD का इस्तेमाल करके, वीडियो में ज़ोरदार और गूंजने वाला इफ़ेक्ट बनाया जा सकता है. उदाहरण के लिए, वीडियो या ऐनिमेशन में किसी असर को विज़ुअलाइज़ करने के साथ-साथ, इस इफ़ेक्ट का इस्तेमाल किया जा सकता है. इससे वीडियो देखने का अनुभव बेहतर बनता है.

यहां गेंद के गिरने के एक सामान्य ऐनिमेशन का उदाहरण दिया गया है. इसमें गेंद के स्क्रीन के नीचे से उछलने पर, थपथप की आवाज़ का इफ़ेक्ट जोड़ा गया है:

स्क्रीन पर, नीचे से ऊपर की ओर उछलती हुई गेंद का ऐनिमेशन
इनपुट वाइब्रेशन वेवफ़ॉर्म का प्लॉट

चौथी इमेज. इस वेवफ़ॉर्म से, किसी डिवाइस पर वाइब्रेशन के आउटपुट ऐक्सेलरेशन के बारे में पता चलता है.

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