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

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

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

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

از جایگزین‌ها برای مدیریت سازگاری دستگاه استفاده کنید

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

  • VibrationEffect.Builder (توصیه‌شده): از Android 16 (26Q4) شروع می‌شود، جلوه‌های ایجادشده بااستفاده از VibrationEffect.Builder شامل بازگشت خودکار در سطح چارچوب می‌شود. اگر دستگاهی به‌طور بومی از Preset درخواستی یا Envelope پایه پشتیبانی نکند، چارچوب به‌طور خودکار آن را در زمان بازپخش با بهترین تلاش به جایگزین مناسبی تبدیل می‌کند. پیش‌از پخش جلوه‌های ساخته‌شده با VibrationEffect.Builder، نیازی نیست قابلیت‌های هر دستگاه ابتدایی را به‌صورت دستی بررسی کنید.
    • استثنا: پوشش‌های پیشرفته شکل موج ایجادشده با WaveformEnvelopeBuilder از بازگشت خودکار پشتیبانی نمی‌کنند زیرا به نگاشت‌های فرکانس سخت‌افزاری خاصی وابسته هستند (FOAM). اگر پشتیبانی نشوند، پخش نخواهند شد.
  • VibrationEffect.Composition: قطعات موسیقی ساخته‌شده با میانای برنامه‌سازی کاربردی startComposition() دارای جایگزین‌های خودکار نیستند. اگر یک ترکیب حتی یک عنصر اولیه پشتیبانی‌نشده داشته باشد، کل لرزش پخش نمی‌شود. باید قابلیت‌ها را بااستفاده از 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))

جاوا

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

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

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

کاتلین

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

جاوا

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

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

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

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

کاتلین

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

جاوا

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

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

از Android 16 (26Q4) شروع می‌شود، VibrationEffect.Builder رابط برنامه‌نویسی نرم‌افزار ترجیحی برای ایجاد جلوه‌های لرزش و ترکیب‌های پیچیده است. این API به شما امکان می‌دهد با توالی‌بندی عناصر لمسی مجزا در یک خط زمان مطلق با 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))
  • Java: 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()
)

جاوا

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

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

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

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

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

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

کاتلین

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

جاوا

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

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

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

کاتلین

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

جاوا

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

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

کاتلین

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

جاوا

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

نمونه‌هایی از قطعات لرزشی

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

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

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

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

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

کاتلین

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

جاوا

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. این عناصر اولیه با هم بهتر کار می‌کنند تا یک بخش شکل موج ایجاد کنند که شدت آن افزایش می‌یابد و سپس کاهش می‌یابد. می‌توانید عناصر اولیه مقیاس‌بندی‌شده را تراز کنید تا از جهش‌های ناگهانی در دامنه بین آن‌ها جلوگیری کنید، که برای افزایش مدت زمان کلی اثر نیز خوب عمل می‌کند. ازنظر ادراکی، افراد همیشه بخش صعودی را بیشتر از بخش نزولی متوجه می‌شوند، بنابراین کوتاه‌تر کردن بخش صعودی نسبت‌به بخش نزولی می‌تواند برای انتقال تأکید به بخش نزولی استفاده شود.

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

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

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

کاتلین

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

جاوا

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

رقص Wobble (با چرخش)

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

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

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

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

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

کاتلین

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

جاوا

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

جهش (با صدای ضربه)

کاربرد پیشرفته دیگر جلوه‌های لرزشی شبیه‌سازی تعاملات فیزیکی است. 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) }
    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)
        }
    }
}

جاوا

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

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

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

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

  • 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() {
  var springX by remember { mutableStateOf(SPRING_WIDTH) }
  var springY by remember { mutableStateOf(SPRING_HEIGHT) }
  var velocityX by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
  var velocityY by remember { mutableFloatStateOf(INITIAL_VELOCITY) }
  var sharpness by remember { mutableFloatStateOf(INITIAL_SHARPNESS) }
  var intensity by remember { mutableFloatStateOf(INITIAL_INTENSITY) }
  var multiplier by remember { mutableFloatStateOf(INITIAL_MULTIPLIER) }
  var bottomBounceCount by remember { mutableIntStateOf(0) }
  var animationStartTime by remember { mutableLongStateOf(0L) }
  var isAnimating by remember { mutableStateOf(false) }

  val (screenHeight, screenWidth) = getScreenDimensions(context)

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

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

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

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

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

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

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

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

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

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

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

      delay(FRAME_DELAY_MS) // Control animation speed.

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

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

پرتاب موشک

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

LavaBeats

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

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

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

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

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

شکل ۷. بخشی از نوار قلب با مجموعه QRS و موج T

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

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

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

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