ایجاد جلوه‌های لمسی سفارشی

این صفحه نمونه‌هایی از نحوه استفاده از میاناهای برنامه‌سازی کاربردی لمسی مختلف را برای ایجاد جلوه‌های سفارشی فراتر از شکل موج‌های لرزش استاندارد در برنامه Android پوشش می‌دهد.

این صفحه شامل مثال‌های زیر است:

برای مثال‌های بیشتر، افزودن بازخورد لمسی به رویدادها را ببینید و همیشه اصول طراحی لمسی را دنبال کنید.

استفاده از بازگشت‌ها برای مدیریت سازگاری دستگاه

هنگام پیاده‌سازی جلوه‌های لمسی سفارشی، سازگاری دستگاه و رفتار بازگشتی به سطح API که انتخاب می‌کنید بستگی دارد:

  • VibrationEffect.Builder (توصیه‌شده): از Android 16 (سه‌ماهه چهارم ۲۰۲۶)، جلوه‌های ایجادشده بااستفاده از VibrationEffect.Builder شامل جایگزین خودکار در سطح چارچوب است. اگر دستگاهی به‌طور پیش‌فرض از Preset درخواستی یا Envelope پایه پشتیبانی نکند، چارچوب به‌طور خودکار آن را در زمان بازپخش به جایگزین مناسبی ترجمه می‌کند و بهترین تلاش را برای این کار انجام می‌دهد. نیازی نیست قبل‌از پخش جلوه‌های ساخته‌شده با VibrationEffect.Builder، قابلیت‌های دستگاه را به‌صورت دستی برای هر عنصر پایه بررسی کنید.
    • استثنا: پوش‌های شکل موج پیشرفته ایجادشده با WaveformEnvelopeBuilder از بازگشت خودکار پشتیبانی نمی‌کنند زیرا به نگاشت‌های فرکانس سخت‌افزاری خاصی وابسته هستند (FOAM). اگر پشتیبانی نشود، پخش نخواهد شد.
  • VibrationEffect.Composition: ترکیب‌بندی‌های ایجادشده با startComposition() API جایگزین خودکار ندارند. اگر یک ترکیب‌بندی حتی یک عنصر پایه پشتیبانی‌نشده داشته باشد، کل لرزش پخش نمی‌شود. باید قابلیت‌ها را به‌صورت دستی بااستفاده از vibrator.arePrimitivesSupported() بررسی کنید.
  • شکل موج با کنترل دامنه: دامنه‌های غیرصفر در دستگاه‌هایی که کنترل دامنه ندارند، به ۱۰۰٪ گرد می‌شوند. vibrator.hasAmplitudeControl() را بررسی کنید و درصورت نیاز به الگوی «روشن/خاموش» که به‌طور صریح طراحی شده است برگردید.

استفاده از اصول لمسی

Android شامل چندین عنصر پایه لمسی است که هم ازنظر دامنه و هم ازنظر فرکانس متفاوت هستند. می‌توانید از یک عنصر پایه به‌تنهایی یا از چند عنصر پایه به‌صورت ترکیبی استفاده کنید تا به جلوه‌های لمسی غنی دست پیدا کنید.

  • برای ایجاد فاصله قابل‌تشخیص بین دو عنصر پایه، از تأخیر ۵۰ میلی‌ثانیه یا بیشتر استفاده کنید، همچنین درصورت امکان مدت عنصر پایه را نیز درنظر بگیرید.
  • از مقیاس‌هایی استفاده کنید که نسبت تفاوت آن‌ها ۱٫۴ یا بیشتر باشد تا تفاوت در شدت بهتر درک شود.
  • از مقیاس‌های ۰٫۵، ۰٫۷، و ۱٫۰ برای ایجاد نسخه با شدت کم، متوسط، و زیاد از یک عنصر پایه استفاده کنید.

ایجاد الگوهای لرزش سفارشی

الگوهای لرزش اغلب در فناوری لمسی جلب‌توجه، مانند اعلان‌ها و آهنگ‌های زنگ، استفاده می‌شوند. سرویس Vibrator می‌تواند الگوهای لرزش طولانی را پخش کند که دامنه لرزش را درطول زمان تغییر می‌دهد. به این جلوه‌ها شکل موج می‌گویند.

جلوه‌های شکل موج معمولاً قابل‌درک هستند، اما لرزش‌های طولانی ناگهانی اگر در محیطی آرام پخش شوند، می‌توانند کاربر را غافلگیر کنند. همچنین، افزایش سریع دامنه هدف ممکن است صداهای وزوز قابل‌شنیدن تولید کند. الگوهای شکل موج را طوری طراحی کنید که انتقال‌های دامنه را هموار کند تا جلوه‌های افزایش و کاهش تدریجی ایجاد شود.

نمونه‌هایی از الگوهای لرزش

بخش‌های زیر چند نمونه از الگوهای لرزش را ارائه می‌دهند:

الگوی افزایش تدریجی

شکل موج‌ها به‌صورت VibrationEffect با سه پارامتر نشان داده می‌شوند:

  1. زمان‌بندی‌ها: آرایه‌ای از مدت‌زمان‌ها، برحسب میلی‌ثانیه، برای هر بخش شکل موج.
  2. دامنه‌ها: دامنه لرزش موردنظر برای هر مدت‌زمان مشخص‌شده در آرگومان اول، که با مقدار عدد صحیح از ۰ تا ۲۵۵ نشان داده می‌شود، که ۰ نشان‌دهنده «حالت خاموش» لرزاننده و ۲۵۵ حداکثر دامنه دستگاه است.
  3. شاخص تکرار: شاخص در آرایه مشخص‌شده در اولین شناسه برای شروع تکرار شکل موج، یا ۱- اگر الگو فقط یک‌بار پخش شود.

در اینجا نمونه‌ای از شکل موجی آورده شده است که دو بار با مکث ۳۵۰ میلی‌ثانیه‌ای بین پالس‌ها، پالس می‌زند. پالس اول یک شیب ملایم تا حداکثر دامنه است و پالس دوم یک شیب سریع برای حفظ حداکثر دامنه است. توقف در پایان با مقدار منفی شاخص تکرار تعریف می‌شود.

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

الگوی تکرارشونده

شکل موج‌ها را می‌توان به‌طور مکرر پخش کرد تا زمانی که لغو شوند. راه ایجاد شکل موج تکرارشونده، تنظیم پارامتر غیرمنفی repeat است. وقتی شکل موج تکرارشونده‌ای را پخش می‌کنید، لرزش تا زمانی که صریحاً در سرویس لغو شود ادامه می‌یابد:

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

این ویژگی برای رویدادهای متناوب که برای تأیید آن به کنش کاربر نیاز است بسیار مفید است. نمونه‌هایی از این رویدادها شامل تماس‌های تلفنی ورودی و زنگ‌های ساعت فعال‌شده است.

الگو با جایگزین

کنترل دامنه لرزش قابلیتی وابسته به سخت‌افزار است. پخش شکل موج در دستگاهی با مشخصات پایین بدون این قابلیت باعث می‌شود دستگاه با حداکثر دامنه برای هر ورودی مثبت در آرایه دامنه بلرزد. اگر برنامه‌تان باید با چنین دستگاه‌هایی سازگار باشد، یا از الگویی استفاده کنید که هنگام پخش در آن شرایط صدای وزوز ایجاد نکند، یا الگوی روشن/خاموش ساده‌تری طراحی کنید که بتواند به‌عنوان جایگزین پخش شود.

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

ترکیب‌بندی‌های لنگرانداخته خط زمان با VibrationEffect.Builder

از Android 16 (سه‌ماهه چهارم ۲۰۲۶)، VibrationEffect.Builder رابط برنامه‌نویسی کاربردی ترجیحی برای ایجاد ترکیب‌ها و جلوه‌های لرزش پیچیده است. این ابزار به شما امکان می‌دهد با توالی عناصر لمسی مجزا درطول یک خط زمانی مطلق با startTimeMillis، احساسات لمسی بیانگر بسازید.

‫VibrationEffect.Builder از ترکیب چند نوع لرزش پشتیبانی می‌کند:

  • پیش‌تنظیم‌ها: پالس‌های لمسی ازپیش تعریف‌شده (VibrationEffect.Preset) مانند کلیک و تیک.
  • پوش‌ها: شکل موج‌های پیوسته پویا، ازجمله پوش‌های پایه مستقل از سخت‌افزار (BasicEnvelopeBuilder) و پوش‌های پیشرفته با فرکانس مدوله‌شده (WaveformEnvelopeBuilder).
  • VibrationEffects موجود: ترکیب‌های اولیه (VibrationEffect.Composition)، شکل موج‌های گام (VibrationEffect.createWaveform)، جلوه‌های ازپیش تعریف‌شده (VibrationEffect.createPredefined)، و جلوه‌های تک‌ضرب (VibrationEffect.createOneShot) واردشده بااستفاده از addEvents() یا کپی کردن سازنده.
  • توالی‌های تکرارشونده: الگوهای پیوسته‌ای که با setRepeatingEffect() پیکربندی شده‌اند.

لرزش‌های ساخته‌شده با VibrationEffect.Builder دارای جایگزین خودکار داخلی در سطح چارچوب در سراسر عناصر خود (ازجمله پیش‌تنظیم‌ها، پوش‌های پایه، و جلوه‌های لرزش ترکیبی) هستند که تجربه کاربری ثابتی را در دستگاه‌های مختلف بدون نیاز به بررسی دستی قابلیت‌ها تضمین می‌کند. (پوش‌های شکل موج پیشرفته ایجادشده با WaveformEnvelopeBuilder به پشتیبانی سخت‌افزاری نیاز دارند و پشتیبانی جایگزین خودکار ندارند.)

نوشتن با پیش‌تنظیم‌ها

از VibrationEffect.Preset برای افزودن پالس‌های لمسی کوتاه رایج و ازپیش تعریف‌شده (مانند PRESET_CLICK، PRESET_TICK، یا PRESET_LOW_TICK) به ترکیب‌بندی استفاده کنید. سازنده دستگاه پیش‌تنظیم‌ها را برای ارائه لرزش واضح، کوتاه، و خوشایند که با اصول لمسی برای لمس واضح مطابقت دارد، پیاده‌سازی می‌کند. برای جزئیات بیشتر درباره این قابلیت‌ها و نحوه عملکرد آن‌ها، کتاب راهنمای محرک‌های لرزش را ببینید.

پیش‌تنظیم‌ها جایگزین دستورهای اولیه کوتاه از VibrationEffect.Composition API می‌شوند. برای حس‌های لمسی طولانی‌تر یا مداوم (مثل افزایش و کاهش تدریجی)، به‌جای آن از شکل موج‌های پوش (PWLE) استفاده کنید.

هر پیش‌تنظیم می‌تواند مقیاس اختیاری بین 0.0f و 1.0f داشته باشد و در زمان شروع صریح (به میلی‌ثانیه) از شروع ترکیب‌بندی قرار می‌گیرد.

کاتلین

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)

جاوا

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

آهنگ‌سازی با پاکت‌ها و پیش‌تنظیم‌ها

می‌توانید به‌طور یکپارچه نمونه‌های VibrationEffect.Envelope (ایجادشده بااستفاده از BasicEnvelopeBuilder یا WaveformEnvelopeBuilder) را با پیش‌تنظیم‌ها ترکیب کنید تا الگوهای لمسی غنی و چندبخشی ایجاد کنید.

در اینجا مثالی آورده شده است که یک پوشش افزایش تدریجی و محو شدن تدریجی ملایم را پخش می‌کند و به‌دنبال آن یک پیش‌تنظیم کلیک تند:

کاتلین

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)

جاوا

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

استفاده مجدد و جابه‌جایی رویدادهای موجود

برای استفاده مجدد یا الحاق کردن VibrationEffect موجود (ازجمله VibrationEffect.Composition)، فهرست اشیاء VibrationEffect.Event آن را بااستفاده از getEvents() بازیابی کنید و آن‌ها را با یک افست بااستفاده از addEvents(startTimeShiftMillis, events) الحاق کنید (یا جلوه را مستقیماً به سازنده VibrationEffect.Builder(effect) ارسال کنید). هنگام وارد کردن VibrationEffect.Composition نمونه به این روش، چارچوب به‌طور خودکار اصلی‌های آن‌ها را به پیش‌تنظیمات تبدیل می‌کند و پشتیبانی از بازگشت زمان اجرا را فعال می‌کند.

کاتلین

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)

جاوا

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

ایجاد ترکیب‌های تکراری

از setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis) برای افزودن الگوی تکرارشونده به ترکیب‌بندی استفاده کنید:

کاتلین

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)

جاوا

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

زمان‌بندی، اعتبارسنجی، و مدیریت انحراف

هنگام طراحی ترکیب‌بندی‌ها با VibrationEffect.Builder، قوانین زمان‌بندی و اعتبارسنجی زیر را درنظر داشته باشید:

  • زمان‌های شروع کاملاً صعودی: هر عنصری که به سازنده اضافه می‌شود باید startTimeMillis داشته باشد که کاملاً بزرگ‌تر یا مساوی با زمان شروع عنصر قبلی باشد.
  • اعتبارسنجی زمان ساخت: سازنده اعتبارسنجی را با بهترین تلاش در build() زمان بااستفاده از مدت‌های عنصر شناخته‌شده (یا حداقل ۱ میلی‌ثانیه برای پیش‌تنظیم‌ها) انجام می‌دهد. اگر هم‌پوشانی غیرممکنی شناسایی شود، IllegalArgumentException را برمی‌گرداند.
  • تغییر متوالی بازپخش: اگر عنصر لرزش قبلی هنوز به‌طور فیزیکی درحال پخش باشد و زمان شروع عنصر بعدی فرا برسد، چارچوب به‌طور خودکار عنصر بعدی را به اولین بازه زمانی دردسترس منتقل می‌کند. این کار تضمین می‌کند که رویدادها هم‌پوشانی نداشته باشند و هیچ لرزشی ازقلم نیفتد، هرچند اگر رویدادها خیلی نزدیک به هم زمان‌بندی شده باشند، ممکن است باعث انحراف جزئی در زمان‌بندی شود. برای به‌حداقل رساندن رانش، زمان کافی (مثلاً ۵۰ میلی‌ثانیه یا بیشتر) بین رویدادهای لمسی متوالی درنظر بگیرید.

ساختن ترکیب‌های لرزش اولیه

این بخش نحوه ترکیب کردن لرزش‌ها را بااستفاده از VibrationEffect.Composition نشان می‌دهد. عناصر اولیه ترکیب‌بندی که قبلاً در این صفحه شرح داده شد، توسط سازنده دستگاه پیاده‌سازی می‌شود. این موتورها لرزشی واضح، کوتاه، و خوشایند ارائه می‌دهند که با اصول لمسی برای لمسی‌های واضح مطابقت دارد. برای جزئیات بیشتر درباره این قابلیت‌ها و نحوه عملکرد آن‌ها، مقدمه‌ای بر محرک‌های لرزش را ببینید.

برخلاف VibrationEffect.Builder، میانای برنامه‌سازی کاربردی VibrationEffect.Composition برای موارد اولیه پشتیبانی‌نشده، بازگشت خودکار ندارد. بنابراین:

  1. قبل‌از فعال کردن لمس پیشرفته، بررسی کنید که دستگاه موردنظر از همه عناصر اولیه‌ای که استفاده می‌کنید پشتیبانی کند.

  2. مجموعه یکپارچه تجربه‌هایی را که پشتیبانی نمی‌شوند غیرفعال کنید، نه فقط جلوه‌هایی را که فاقد عنصر اولیه هستند.

  • Kotlin: val fallbackEffect = VibrationEffect.Builder(compositionEffect).build() (یا .addEvents(0L, compositionEffect.events))
  • جاوا: VibrationEffect fallbackEffect = new VibrationEffect.Builder(compositionEffect).build(); (یا .addEvents(0L, compositionEffect.getEvents()))

وقتی با VibrationEffect.Builder ساخته می‌شود، چارچوب، ترکیب‌بندی‌های اولیه را به پیش‌تنظیم‌ها تبدیل می‌کند و اگر هرکدام از ترکیب‌بندی‌های اولیه در دستگاه کاربر پشتیبانی نشود، به‌طور خودکار بازگشت زمان اجرا را ارائه می‌دهد.

جلوه‌های لرزش ترکیبی

می‌توانید با VibrationEffect.Composition جلوه‌های لرزش ترکیبی ایجاد کنید. در اینجا مثالی از جلوه افزایش تدریجی و به‌دنبال آن جلوه کلیک تند آورده شده است:

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

ترکیب‌بندی با افزودن عناصر اولیه برای پخش به‌صورت متوالی ایجاد می‌شود. هر الگوی اولیه مقیاس‌پذیر نیز هست، بنابراین می‌توانید دامنه ارتعاش تولیدشده توسط هرکدام را کنترل کنید. مقیاس به‌عنوان مقداری بین ۰ و ۱ تعریف می‌شود، که در آن ۰ درواقع به حداقل دامنه‌ای نگاشت می‌شود که در آن این عنصر پایه می‌تواند (به‌سختی) توسط کاربر احساس شود.

ایجاد گونه‌های دیگر در لرزش‌های اولیه

اگر می‌خواهید نسخه ضعیف و قوی از یک عنصر پایه ایجاد کنید، نسبت‌های قدرت ۱٫۴ یا بیشتر ایجاد کنید تا تفاوت در شدت به‌راحتی قابل‌درک باشد. سعی نکنید بیش‌از سه سطح شدت برای یک عنصر پایه ایجاد کنید، زیرا ازنظر ادراکی متمایز نیستند. برای مثال، از مقیاس‌های 0.5،‏ 0.7، و 1.0 برای ایجاد نسخه‌های با شدت کم، متوسط، و زیاد از یک عنصر اولیه استفاده کنید.

افزودن فاصله بین لرزش‌های اولیه

ترکیب‌بندی همچنین می‌تواند تأخیرهایی را برای اضافه شدن بین عناصر اولیه متوالی مشخص کند. این تأخیر برحسب میلی‌ثانیه از پایان ابتدایی قبلی بیان می‌شود. به‌طور کلی، فاصله ۵ تا ۱۰ میلی‌ثانیه‌ای بین دو عنصر پایه بسیار کوتاه است و قابل‌تشخیص نیست. اگر می‌خواهید بین دو عنصر پایه فاصله قابل‌تشخیصی ایجاد کنید، از فاصله‌ای در حدود ۵۰ میلی‌ثانیه یا بیشتر استفاده کنید. در اینجا نمونه‌ای از ترکیب‌بندی با تأخیر آورده شده است:

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

بررسی کنید کدام‌یک از انواع داده‌های اولیه پشتیبانی می‌شوند

از APIهای زیر می‌توان برای تأیید پشتیبانی دستگاه از موارد اولیه خاص استفاده کرد:

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

همچنین می‌توان چندین دستور اولیه را بررسی کرد و سپس تصمیم گرفت که کدام‌یک را براساس سطح پشتیبانی دستگاه ترکیب کرد:

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

نمونه‌هایی از ترکیب‌های لرزش

بخش‌های زیر چند نمونه از ترکیب‌های لرزش را ارائه می‌دهند که از برنامه نمونه لمسی در GitHub گرفته شده‌اند.

مقاومت (با تیک‌های کم)

می‌توانید دامنه لرزش اولیه را کنترل کنید تا بازخورد مفیدی برای کنش درحال انجام ارائه دهید. از مقادیر مقیاس با فاصله نزدیک می‌توان برای ایجاد جلوه کرشندوی هموار در یک عنصر اولیه استفاده کرد. تأخیر بین اولیههای متوالی را نیز می‌توان به‌طور پویا براساس تعامل کاربر تنظیم کرد. این در مثال زیر از پویانمایی نمای کنترل‌شده با اشاره کشیدن و تقویت‌شده با لمس نشان داده شده است.

پویانمایی دایره‌ای که به پایین کشیده می‌شود.
نمودار شکل موج لرزش ورودی.

شکل ۱. این شکل موج نشان‌دهنده شتاب خروجی لرزش در دستگاه است.

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

ازهم باز کردن (با بالا و پایین رفتن)

دو عنصر پایه برای افزایش شدت لرزش درک‌شده وجود دارد: PRIMITIVE_QUICK_RISE و PRIMITIVE_SLOW_RISE. هر دو به یک هدف می‌رسند، اما با مدت زمان‌های متفاوت. فقط یک دستور اولیه برای کاهش تدریجی وجود دارد، PRIMITIVE_QUICK_FALL. این عناصر اولیه باهم بهتر کار می‌کنند تا بخش شکل موجی ایجاد کنند که شدت آن افزایش می‌یابد و سپس ازبین می‌رود. می‌توانید عناصر اولیه مقیاس‌بندی‌شده را تراز کنید تا از پرش‌های ناگهانی در دامنه بین آن‌ها جلوگیری کنید، که این کار برای افزایش مدت‌زمان کلی جلوه نیز مفید است. ازنظر ادراکی، افراد همیشه به بخش صعودی بیشتر از بخش نزولی توجه می‌کنند، بنابراین کوتاه‌تر کردن بخش صعودی نسبت به بخش نزولی می‌تواند برای انتقال تأکید به بخش نزولی استفاده شود.

در اینجا نمونه‌ای از کاربرد این ترکیب‌بندی برای گسترش و جمع کردن دایره آورده شده است. جلوه صعودی می‌تواند احساس گسترش را درطول پویانمایی تقویت کند. ترکیب جلوه‌های صعودی و نزولی به تأکید بر فروپاشی در پایان پویانمایی کمک می‌کند.

پویانمایی دایره‌ای درحال گسترش.
نمودار شکل موج لرزش ورودی.

شکل ۲.این شکل موج نشان‌دهنده شتاب خروجی لرزش در دستگاه است.

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

لرزش (با چرخش)

یکی از اصول کلیدی لمسی، خوشحال کردن کاربران است. یک روش جالب برای معرفی یک جلوه لرزش غیرمنتظره دلپذیر، استفاده از PRIMITIVE_SPIN است. این دستور اولیه زمانی بیشترین تأثیر را دارد که بیش‌از یک بار فراخوانی شود. چرخش‌های متعدد متصل‌شده می‌توانند جلوه‌ای لرزان و ناپایدار ایجاد کنند که با اعمال مقیاس‌بندی تا حدودی تصادفی روی هر عنصر پایه، می‌توان آن را بیشتر تقویت کرد. همچنین می‌توانید فاصله بین اشکال پایه چرخش متوالی را امتحان کنید. دو چرخش بدون هیچ فاصله‌ای (۰ میلی‌ثانیه بین آن‌ها) حس چرخش تنگی ایجاد می‌کند. افزایش فاصله بین چرخش‌ها از ۱۰ به ۵۰ میلی‌ثانیه منجر به احساس چرخش شل‌تر می‌شود و می‌توان از آن برای مطابقت با مدت‌زمان ویدیو یا پویانمایی استفاده کرد.

از وقفه‌ای طولانی‌تر از ۱۰۰ میلی‌ثانیه استفاده نکنید، زیرا چرخش‌های متوالی دیگر به‌خوبی ادغام نمی‌شوند و مانند جلوه‌های جداگانه احساس می‌شوند.

در اینجا نمونه‌ای از شکل ارتجاعی آورده شده است که پس‌از کشیده شدن به پایین و سپس رها شدن، به حالت اولیه برمی‌گردد. پویانمایی با یک جفت جلوه چرخش بهبود یافته است که با شدت‌های مختلفی که متناسب با جابه‌جایی پرش است پخش می‌شود.

پویانمایی شکل کشسانی که بالا و پایین می‌پرد
نمودار شکل موج لرزش ورودی

شکل ۳. این شکل موج نشان‌دهنده شتاب خروجی لرزش در دستگاه است.

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

جهش (با صدای تالاپ)

یکی دیگر از کاربردهای پیشرفته جلوه‌های لرزش، شبیه‌سازی تعاملات فیزیکی است. PRIMITIVE_THUD می‌تواند جلوه‌ای قوی و طنین‌انداز ایجاد کند که می‌توان آن را با تجسم یک ضربه، مثلاً در یک ویدیو یا پویانمایی، جفت کرد تا تجربه کلی را تقویت کند.

در اینجا نمونه‌ای از پویانمایی افتادن توپ را می‌بینید که با جلوه صدای تالاپ تقویت شده است و هر بار که توپ از پایین صفحه‌نمایش برمی‌گردد پخش می‌شود:

پویانمایی توپی که رها شده است و از پایین صفحه‌نمایش بالا می‌جهد.
نمودار شکل موج لرزش ورودی.

شکل ۴. این شکل موج نشان‌دهنده شتاب خروجی لرزش در دستگاه است.

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

شکل‌موج لرزش با پوشش

فرایند ایجاد الگوهای لرزش سفارشی به شما امکان می‌دهد دامنه لرزش را کنترل کنید تا جلوه‌های همواری از افزایش و کاهش تدریجی ایجاد کنید. این بخش توضیح می‌دهد که چگونه بااستفاده از پوش‌های شکل موج، جلوه‌های لمسی پویا ایجاد کنید که امکان کنترل دقیق دامنه و فرکانس لرزش را درطول زمان فراهم می‌کند. این کار به شما امکان می‌دهد تجربه‌های لمسی غنی‌تر و دقیق‌تری ایجاد کنید.

از Android 16 (سطح API‏ ۳۶)، سیستم APIهای زیر را برای ایجاد پوش شکل موج لرزش با تعریف توالی نقاط کنترل ارائه می‌دهد:

  • BasicEnvelopeBuilder: رویکردی دردسترس برای ایجاد جلوه‌های لمسی مستقل از سخت‌افزار.
  • WaveformEnvelopeBuilder: رویکردی پیشرفته‌تر برای ایجاد جلوه‌های لمسی؛ به آشنایی با سخت‌افزار لمسی نیاز دارد.

Android برای جلوه‌های پوششی، جایگزین ارائه نمی‌دهد. اگر به این پشتیبانی نیاز دارید، مراحل زیر را تکمیل کنید:

  1. بررسی کنید که دستگاه موردنظر از جلوه‌های پوششی بااستفاده از Vibrator.areEnvelopeEffectsSupported() پشتیبانی می‌کند یا نه.
  2. مجموعه یکپارچه تجربه‌هایی را که پشتیبانی نمی‌شوند غیرفعال کنید، یا از الگوهای لرزش سفارشی یا ترکیب‌بندی‌ها به‌عنوان جایگزین‌های احتیاطی استفاده کنید.

برای ایجاد جلوه‌های پوششی پایه‌ای بیشتر، از BasicEnvelopeBuilder با این پارامترها استفاده کنید:

  • مقدار شدت در محدوده \( [0, 1] \)که نشان‌دهنده قدرت درک‌شده لرزش است. برای مثال، مقدار \( 0.5 \) به‌عنوان نصف حداکثر شدت جهانی که دستگاه می‌تواند به آن دست یابد درنظر گرفته می‌شود.
  • مقدار وضوح در محدوده \( [0, 1] \)که نشان‌دهنده وضوح لرزش است. مقادیر کمتر به لرزش‌های ملایم‌تر ترجمه می‌شود، درحالی‌که مقادیر بیشتر حس شدیدتری ایجاد می‌کند.

  • مقدار مدت که نشان‌دهنده زمان (به میلی‌ثانیه) برای انتقال از آخرین نقطه کنترل (یعنی جفت شدت و وضوح) به نقطه جدید است.

در اینجا نمونه‌ای از شکل موجی ارائه شده است که شدت را از لرزش با گام پایین به لرزش با گام بالا و حداکثر قدرت در مدت ۵۰۰ میلی‌ثانیه افزایش می‌دهد و سپس در مدت ۱۰۰ میلی‌ثانیه دوباره به\( 0 \) (خاموش) کاهش می‌دهد.

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

اگر دانش پیشرفته‌تری درباره لمس‌شناسی دارید، می‌توانید جلوه‌های پوششی را بااستفاده از WaveformEnvelopeBuilder تعریف کنید. هنگام استفاده از این شیء، می‌توانید ازطریق VibratorFrequencyProfile به نگاشت فرکانس به شتاب خروجی (FOAM) دسترسی پیدا کنید.

  • مقدار دامنه در محدوده \( [0, 1] \)که نشان‌دهنده قدرت لرزش قابل‌دستیابی در فرکانس مشخص‌شده است، همان‌طور که توسط FOAM دستگاه تعیین شده است. برای مثال، مقدار \( 0.5 \) نیمی از حداکثر شتاب خروجی را که می‌توان در فرکانس داده‌شده به‌دست آورد تولید می‌کند.
  • مقدار بسامد، مشخص‌شده به هرتز.

  • مقدار مدت که نشان‌دهنده زمان (به میلی‌ثانیه) است که برای انتقال از آخرین نقطه کنترل به نقطه جدید لازم است.

کد زیر نمونه‌ای از شکل موج را نشان می‌دهد که اثر لرزش ۴۰۰ میلی‌ثانیه‌ای را تعریف می‌کند. این الگو با یک شیب دامنه ۵۰ میلی‌ثانیه‌ای، از خاموش تا کامل، در فرکانس ثابت ۶۰ هرتز شروع می‌شود. سپس، فرکانس در ۱۰۰ میلی‌ثانیه بعدی به ۱۲۰ هرتز افزایش می‌یابد و به‌مدت ۲۰۰ میلی‌ثانیه در آن سطح باقی می‌ماند. درنهایت، دامنه به \( 0 \)کاهش می‌یابد و فرکانس در ۵۰ میلی‌ثانیه آخر به ۶۰ هرتز بازمی‌گردد:

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

بخش‌های زیر چند نمونه از شکل موج‌های لرزش با پوش را ارائه می‌دهند.

فنر جهنده

نمونه قبلی از PRIMITIVE_THUD برای شبیه‌سازی تعاملات پرش فیزیکی استفاده می‌کند. میانای برنامه‌سازی کاربردی پوششی پایه کنترل بسیار دقیق‌تری ارائه می‌دهد و به شما امکان می‌دهد شدت و وضوح لرزش را دقیقاً سفارشی‌سازی کنید. این کار منجر به بازخورد لمسی‌ای می‌شود که رویدادهای پویانمایی‌شده را دقیق‌تر دنبال می‌کند.

در اینجا نمونه‌ای از یک فنر درحال سقوط آزاد با پویانمایی بهبودیافته با یک جلوه پوششی پایه که هربار که فنر از پایین صفحه برمی‌گردد پخش می‌شود، آورده شده است:

پویانمایی فنری که افتاده است و از پایین صفحه‌نمایش بالا می‌پرد.
نمودار شکل موج لرزش ورودی.

شکل ۵. نمودار شکل موج شتاب خروجی برای لرزشی که پرش فنر را شبیه‌سازی می‌کند.

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

پرتاب موشک

نمونه قبلی نشان می‌دهد که چگونه از API پوششی پایه برای شبیه‌سازی واکنش فنر ارتجاعی استفاده کنید. WaveformEnvelopeBuilder کنترل دقیقی بر کل محدوده فرکانس دستگاه فراهم می‌کند و امکان ایجاد جلوه‌های لمسی بسیار سفارشی‌شده را فراهم می‌سازد. با ترکیب این مورد با داده‌های FOAM، می‌توانید لرزش‌ها را برای قابلیت‌های فرکانس خاصی سفارشی کنید.

در اینجا مثالی آورده شده است که شبیه‌سازی پرتاب موشک را بااستفاده از الگوی لرزش پویا نشان می‌دهد. این جلوه از حداقل خروجی شتاب فرکانس پشتیبانی‌شده، ۰٫۱ G، به فرکانس تشدید می‌رود و همیشه ورودی دامنه ۱۰٪ را حفظ می‌کند. این کار باعث می‌شود جلوه با خروجی نسبتاً قوی شروع شود و شدت و وضوح درک‌شده را افزایش دهد، حتی اگر دامنه محرک یکسان باشد. با رسیدن به تشدید، فرکانس جلوه دوباره به حداقل کاهش می‌یابد که به‌عنوان کاهش شدت و وضوح درک می‌شود. این کار حس مقاومت اولیه و سپس رها شدن را ایجاد می‌کند، که شبیه به پرتاب شدن به فضا است.

این اثر با API پوششی پایه امکان‌پذیر نیست، زیرا اطلاعات خاص دستگاه درباره فرکانس تشدید و منحنی شتاب خروجی آن را انتزاعی می‌کند. افزایش وضوح می‌تواند فرکانس معادل را فراتر از تشدید ببرد، که ممکن است باعث افت شتاب ناخواسته شود.

پویانمایی سفینه موشکی که از پایین صفحه‌نمایش بلند می‌شود.
نمودار شکل موج لرزش ورودی.

شکل ۶. نمودار شکل موج شتاب خروجی برای لرزشی که پرتاب موشک را شبیه‌سازی می‌کند.

@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

همان‌طور که در مثال پرتاب موشک دیدیم، WaveformEnvelopeBuilder API طراحی بسیاری از جلوه‌های لمسی پیچیده را با امکان کنترل بخش‌های دامنه و فرکانس در لرزش فراهم می‌کند. نمونه دیگری از چنین طراحی‌ای، شبیه‌سازی احساسات فیزیکی انتزاعی‌تر، مانند «سرزندگی» است.

این کار را می‌توان با نشان دادن نشانگرهای زیستی سیگنال الکتروکاردیوگرام (ECG) معمولی با بخش‌های ارتعاشی با دامنه‌ها و فرکانس‌های خاص انجام داد. LavaBeats مثالی است که در آن دو بخش مشخصه از یک ضبط ECG به‌عنوان دو پالس جداشده با تأخیر زمانی نشان داده شده است. اولین پالس مشخصه، کمپلکس QRS است که به‌صورت یک قله تیز با دامنه بالا و مدت‌زمان کوتاه نشان داده می‌شود. پالس دوم موج T است که دامنه کمتر، مدت طولانی‌تر، و شکل هموارتری دارد (شکل ۷ را ببینید).

از WaveformEnvelopeBuilder برای ساختن تکرارهای مختلف این دو پالس که با تأخیر ثابت پالس اول به دوم از هم جدا شده‌اند استفاده کنید. پالس اول می‌تواند سیگنال جیرجیر باشد که از فرکانس پایین شروع می‌شود و در مدت کوتاهی به فرکانس بالاتر می‌رسد. پالس دوم را می‌توان به‌عنوان یک دوره واحد از یک موج سینوسی با فرکانس پایین نشان داد. می‌توانیم دو پالس را در یک ضرب آهنگ ترکیب کنیم، و ترکیب را چندین بار با تأخیر در بین آن‌ها تکرار کنیم، با پیروی از یک نرخ معمول ضربه در دقیقه (bpm). نتیجه جلوه‌ای لمسی است که شبیه ضربان قلب است.

می‌توانید LavaBeats را در برنامه نمونه لمسی ما در GitHub امتحان کنید و تأثیر آن را همراه با تجسم چراغ گدازه‌ای که با همان ریتم تأثیر لمسی می‌تپد احساس کنید. همچنین می‌توانید تنظیمات جلوه را تغییر دهید تا با اصلاح دامنه‌ها، فرکانس‌ها، مدت‌ها، و تأخیرهای دو پالس، حس‌های ضربان متفاوتی ایجاد کنید.

پویانمایی چراغ گدازه‌ای که با الگوی ضربان قلب لمسی می‌تپد.
نمودار قطعه نوار قلب و شکل موج‌های مشخصه آن

شکل ۷. بخشی از ضبط نوار قلب با کمپلکس QRS و موج T

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