تقديم أمثلة وإرشادات حول كيفية إنشاء تأثيرات لمس مخصّصة في Android، بما في ذلك التركيبات المستندة إلى المخطط الزمني باستخدام VibrationEffect.Builder وأنماط الاهتزاز المخصّصة وأغلفة الموجات المتقدّمة keywords_public: > Android, haptics, custom effects, vibration, haptic APIs, vibration patterns, VibrationEffect.Builder, compositions, haptic primitives, waveform envelopes, UI
تتضمّن هذه الصفحة أمثلة على كيفية استخدام واجهات برمجة التطبيقات المختلفة الخاصة باللمس لإنشاء تأثيرات مخصّصة تتجاوز أشكال الموجات الاهتزازية العادية في تطبيق Android.
تتضمّن هذه الصفحة الأمثلة التالية:
- مقاطع صوتية مرتبطة بالمخطط الزمني مع
VibrationEffect.Builder- إنشاء اهتزازات باستخدام الإعدادات المُسبقة: يمكنك ترتيب إحساسات لمسية مُحدّدة مسبقًا.
- إنشاء مقاطع صوتية باستخدام المغلفات والإعدادات المُسبقة: يمكنك دمج المغلفات والإعدادات المُسبقة على طول المخطط الزمني.
- إعادة استخدام الأحداث وتغيير موضعها: يمكنك تغيير موضع أحداث التجميع الحالية وإعادة استخدامها.
- المقاطع المتكررة: يمكنك إنشاء مؤثرات متكررة على المخطط الزمني.
- أنماط الاهتزاز المخصّصة
- نمط الزيادة التدريجية: نمط يبدأ بسلاسة.
- النمط المتكرّر: نمط لا ينتهي.
- النمط مع الإجراء الاحتياطي: عرض توضيحي للإجراء الاحتياطي
- تركيبات عناصر الاهتزاز الأساسية
- شكل موجة الاهتزاز مع الأغلفة
- النابض المرتد: تأثير نابض مرتّد باستخدام تأثيرات أساسية للأظرف.
- إطلاق صاروخ: مؤثر إطلاق صاروخ باستخدام مؤثرات مغلف الموجة الصوتية.
للاطّلاع على أمثلة إضافية، راجِع إضافة تجاوب حسّي إلى الأحداث، واحرص دائمًا على اتّباع مبادئ تصميم تقنية اللمس.
استخدام طرق احتياطية للتعامل مع توافق الأجهزة
عند تنفيذ مؤثرات لمس مخصّصة، يعتمد التوافق مع الأجهزة والسلوك الاحتياطي على مساحة واجهة برمجة التطبيقات التي تختارها:
VibrationEffect.Builder(يُنصح به): بدءًا من Android 16 (الربع الرابع من 2026)، تتضمّن التأثيرات التي تم إنشاؤها باستخدامVibrationEffect.Builderإجراء احتياطي تلقائي على مستوى إطار العمل. إذا كان الجهاز لا يتوافق بشكل أصلي معPresetأوEnvelopeأساسيَين مطلوبَين، يحوّلهما إطار العمل تلقائيًا إلى بديل مناسب أثناء التشغيل مع بذل أقصى الجهود. لست بحاجة إلى التحقّق يدويًا من إمكانات كل جهاز بدائي قبل تشغيل التأثيرات التي تم إنشاؤها باستخدامVibrationEffect.Builder.- استثناء: لا تتوافق مغلفات الأشكال الموجية المتقدّمة التي تم إنشاؤها باستخدام
WaveformEnvelopeBuilderمع ميزة الرجوع التلقائي لأنّها تعتمد على عمليات ربط محدّدة لترددات الأجهزة (FOAM). إذا كانت غير متوافقة، لن يتم تشغيلها.
- استثناء: لا تتوافق مغلفات الأشكال الموجية المتقدّمة التي تم إنشاؤها باستخدام
VibrationEffect.Composition: لا تتضمّن المقاطع الموسيقية التي تم إنشاؤها باستخدام واجهة برمجة التطبيقاتstartComposition()بدائل تلقائية. إذا كان أحد العناصر الأساسية في التركيبة غير متوافق، لن يتم تشغيل الاهتزاز بأكمله. يجب التحقّق من الإمكانات يدويًا باستخدامvibrator.arePrimitivesSupported().- الأشكال الموجية مع التحكّم في السعة: يتم تقريب السعات غير الصفرية إلى 100% على الأجهزة التي لا تتضمّن ميزة التحكّم في السعة. تحقَّق من
vibrator.hasAmplitudeControl()واستخدِم نمطًا مصمّمًا بشكل واضح لتفعيل الميزة أو إيقافها إذا لزم الأمر.
استخدام عناصر اللمس الأساسية
يتضمّن نظام التشغيل Android العديد من العناصر الأساسية التي تختلف في كل من السعة والتردد. يمكنك استخدام عنصر أساسي واحد بمفرده أو عناصر أساسية متعددة معًا لتحقيق تأثيرات لمسية غنية.
- استخدِم تأخيرات تبلغ 50 مللي ثانية أو أكثر لإنشاء فجوات واضحة بين عنصرَين أساسيَين، مع مراعاة مدة العنصر الأساسي إذا أمكن.
- استخدِم مقاييس تختلف بنسبة 1.4 أو أكثر حتى يسهل إدراك الفرق في الشدة.
استخدِم المقاييس 0.5 و0.7 و1.0 لإنشاء نسخة من عنصر أساسي بدرجة منخفضة ومتوسطة وعالية.
إنشاء أنماط اهتزاز مخصّصة
غالبًا ما تُستخدم أنماط الاهتزاز في تقنية اللمس التي تجذب الانتباه، مثل الإشعارات ونغمات الرنين. يمكن لخدمة Vibrator تشغيل أنماط اهتزاز طويلة
تغيّر سعة الاهتزاز بمرور الوقت. تُعرف هذه التأثيرات باسم أشكال الموجات.
عادةً ما تكون تأثيرات شكل الموجة ملموسة، ولكن قد تؤدي الاهتزازات الطويلة المفاجئة إلى إزعاج المستخدم إذا تم تشغيلها في بيئة هادئة. قد يؤدي الانتقال إلى السعة المستهدَفة بسرعة كبيرة أيضًا إلى حدوث ضوضاء طنين مسموعة. صمِّم أنماط الموجات الصوتية لتسوية انتقالات السعة من أجل إنشاء تأثيرات زيادة وخفض مستوى الصوت تدريجيًا.
أمثلة على أنماط الاهتزاز
تقدّم الأقسام التالية عدة أمثلة على أنماط الاهتزاز:
نمط توفير الميزة
يتم تمثيل أشكال الموجات على النحو التالي: VibrationEffect مع ثلاثة مَعلمات:
- التوقيتات: مصفوفة من المدد بالمللي ثانية لكل جزء من شكل الموجة
- السعات: سعة الاهتزاز المطلوبة لكل مدة زمنية محددة في الوسيط الأول، ويتم تمثيلها بقيمة عدد صحيح تتراوح بين 0 و255، حيث يمثّل 0 حالة "إيقاف" المهتز، و255 يمثّل الحد الأقصى للسعة في الجهاز.
- فهرس التكرار: هو الفهرس في الصفيف المحدّد في الوسيطة الأولى لبدء تكرار الشكل الموجي، أو -1 إذا كان يجب تشغيل النمط مرة واحدة فقط.
في ما يلي مثال على شكل موجي ينبض مرتين مع توقّف مؤقت لمدة 350 ملي ثانية بين النبضات. النبضة الأولى هي زيادة سلسة في السعة إلى الحد الأقصى، أما النبضة الثانية فهي زيادة سريعة للحفاظ على السعة القصوى. يتم تحديد التوقف في النهاية من خلال قيمة فهرس التكرار السالبة.
Kotlin
val timings: LongArray = longArrayOf(
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex))
Java
long[] timings = new long[] {
50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.
vibrator.vibrate(VibrationEffect.createWaveform(
timings, amplitudes, repeatIndex));
النمط المتكرّر
يمكن أيضًا تشغيل الأشكال الموجية بشكل متكرر إلى أن يتم إلغاؤها. لإنشاء شكل موجي متكرّر، عليك ضبط المَعلمة repeat على قيمة غير سالبة. عند تشغيل شكل موجي متكرّر، يستمر الاهتزاز إلى أن يتم إلغاؤه بشكل صريح في الخدمة:
Kotlin
void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect)
}
void stopVibrating() {
vibrator.cancel()
}
Java
void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.
vibrator.vibrate(repeatingEffect);
}
void stopVibrating() {
vibrator.cancel();
}
ويكون ذلك مفيدًا جدًا للأحداث المتقطّعة التي تتطلّب من المستخدم اتّخاذ إجراء لتأكيدها. وتشمل الأمثلة على هذه الأحداث المكالمات الهاتفية الواردة والمنبّهات التي تم تفعيلها.
النمط مع الإجراء الاحتياطي
التحكّم في سعة الاهتزاز هو إمكانية تعتمد على الأجهزة. سيؤدي تشغيل شكل موجي على جهاز منخفض المواصفات بدون هذه الإمكانية إلى اهتزاز الجهاز بأقصى سعة لكل إدخال موجب في مصفوفة السعة. إذا كان تطبيقك يحتاج إلى استيعاب هذه الأجهزة، استخدِم نمطًا لا يؤدي إلى إصدار صوت طنين عند تشغيله في هذه الحالة، أو صمِّم نمطًا أبسط للتشغيل/الإيقاف يمكن تشغيله كبديل.
Kotlin
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx))
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx))
}
Java
if (vibrator.hasAmplitudeControl()) {
vibrator.vibrate(VibrationEffect.createWaveform(
smoothTimings, amplitudes, smoothRepeatIdx));
} else {
vibrator.vibrate(VibrationEffect.createWaveform(
onOffTimings, onOffRepeatIdx));
}
مقاطع موسيقية مرتبطة بالمخطط الزمني مع VibrationEffect.Builder
اعتبارًا من Android 16 (الربع الرابع من 2026)، ستكون واجهة برمجة التطبيقات VibrationEffect.Builder هي
واجهة برمجة التطبيقات المفضّلة لإنشاء مؤثرات واهتزازات معقّدة. تتيح لك هذه السمة إنشاء إحساسات لمسية معبّرة من خلال ترتيب عناصر لمسية منفصلة على طول مخطط زمني مطلق باستخدام startTimeMillis.
تتيح السمة VibrationEffect.Builder دمج أنواع متعددة من الاهتزازات:
- الإعدادات المُسبقة: نبضات لمسية محدّدة مسبقًا (
VibrationEffect.Preset) مثل النقرات والعلامات. - المغلفات: موجات ديناميكية مستمرة، بما في ذلك المغلفات الأساسية المستقلة عن الأجهزة (
BasicEnvelopeBuilder) والمغلفات المتقدّمة المعدَّلة التردد (WaveformEnvelopeBuilder). - تأثيرات الاهتزاز الحالية: التركيبات الأساسية
(
VibrationEffect.Composition) وأشكال الموجات المتدرّجة (VibrationEffect.createWaveform) والتأثيرات المحدّدة مسبقًا (VibrationEffect.createPredefined) والتأثيرات الفردية (VibrationEffect.createOneShot) التي تم استيرادها باستخدامaddEvents()أو نسخ الدالة الإنشائية - التسلسلات المتكرّرة: الأنماط المستمرة التي تم إعدادها باستخدام
setRepeatingEffect()
تتضمّن الاهتزازات التي تم إنشاؤها باستخدام VibrationEffect.Builder احتياطيًا تلقائيًا على مستوى إطار العمل في جميع عناصرها (بما في ذلك الإعدادات المُسبقة والأغلفة الأساسية وتأثيرات الاهتزاز المجمّعة)، ما يضمن تجربة مستخدم متسقة على جميع الأجهزة المختلفة بدون الحاجة إلى إجراء عمليات تحقّق يدوية من الإمكانات. (تتطلّب مغلفات الأشكال الموجية المتقدّمة التي تم إنشاؤها باستخدام
WaveformEnvelopeBuilder توافقًا مع الأجهزة، ولا تتوافق مع ميزة التبديل التلقائي إلى إصدار احتياطي.)
إنشاء الأغاني باستخدام الإعدادات المُسبَقة
استخدِم VibrationEffect.Preset لإضافة نبضات لمسية قصيرة وشائعة ومحدّدة مسبقًا
(مثل PRESET_CLICK أو PRESET_TICK أو PRESET_LOW_TICK) إلى مقطوعة موسيقية.
تنفّذ الشركة المصنّعة للجهاز الإعدادات المُسبقة لتوفير اهتزاز واضح وقصير وممتع يتوافق مع مبادئ اللمس للحصول على تجربة لمس واضحة. لمزيد من التفاصيل حول هذه الإمكانات وطريقة عملها، يُرجى الاطّلاع على
مقدمة عن مشغّلات الاهتزاز.
تحلّ الإعدادات المُسبَقة محلّ العناصر الأساسية القصيرة من واجهة برمجة التطبيقات VibrationEffect.Composition.
بالنسبة إلى الإحساسات اللمسية الأطول أو المستمرة، مثل زيادة الشدة تدريجيًا ثم خفضها، استخدِم أشكال الموجات المغلفة (PWLE) بدلاً من ذلك.
يمكن تعيين مقياس اختياري لكل إعداد مُسبَق بين 0.0f و1.0f، ويتم وضعه في وقت بدء محدّد (بالمللي ثانية) من بداية المقطوعة الموسيقية.
Kotlin
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
)
val tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
)
val effect = VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build()
vibrator.vibrate(effect)
Java
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, /* scale= */ 0.8f
);
VibrationEffect.Preset tickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_TICK, /* scale= */ 0.5f
);
VibrationEffect effect = new VibrationEffect.Builder()
.addPreset(/* startTimeMillis= */ 0L, clickPreset)
.addPreset(/* startTimeMillis= */ 100L, tickPreset)
.build();
vibrator.vibrate(effect);
إنشاء الأغاني باستخدام الأغلفة والإعدادات المُسبقة
يمكنك دمج مثيلات VibrationEffect.Envelope (التي تم إنشاؤها باستخدام BasicEnvelopeBuilder أو WaveformEnvelopeBuilder) مع الإعدادات المُسبقة لإنشاء أنماط لمسية غنية ومتعددة الأجزاء.
في ما يلي مثال يعرض تدرّجًا سلسًا في مستوى الصوت ثم انخفاضًا تدريجيًا، يليهما إعداد مسبق لنقرة حادة:
Kotlin
val basicEnvelope = VibrationEffect.Envelope.create(
VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
)
val clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
)
val mixedEffect = VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build()
vibrator.vibrate(mixedEffect)
Java
VibrationEffect.Envelope basicEnvelope = VibrationEffect.Envelope.create(
new VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 300L)
.addControlPoint(0.0f, 0.5f, 100L)
);
VibrationEffect.Preset clickPreset = VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 0.9f
);
VibrationEffect mixedEffect = new VibrationEffect.Builder()
.addEnvelope(/* startTimeMillis= */ 0L, basicEnvelope)
.addPreset(/* startTimeMillis= */ 450L, clickPreset)
.build();
vibrator.vibrate(mixedEffect);
إعادة استخدام الأحداث الحالية ونقلها
لإعادة استخدام VibrationEffect حالي أو ربطه (بما في ذلك VibrationEffect.Composition)، استرجِع قائمة عناصر VibrationEffect.Event باستخدام getEvents() وأضِفها مع إزاحة باستخدام addEvents(startTimeShiftMillis, events) (أو مرِّر التأثير مباشرةً إلى الدالة الإنشائية VibrationEffect.Builder(effect)). عند استيراد مثيلات VibrationEffect.Composition بهذه الطريقة، يحوّل إطار العمل تلقائيًا العناصر الأساسية إلى إعدادات مسبقة، ما يتيح إمكانية الرجوع إلى الإعدادات التلقائية أثناء وقت التشغيل.
Kotlin
val existingEffect = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build()
// Shift and append the existing events 200ms into the new composition.
val combinedEffect = VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.events)
.build()
vibrator.vibrate(combinedEffect)
Java
VibrationEffect existingEffect = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_CLICK)
)
.addPreset(
80L,
VibrationEffect.Preset.create(VibrationEffect.Preset.PRESET_TICK)
)
.build();
// Shift and append the existing events 200ms into the new composition.
VibrationEffect combinedEffect = new VibrationEffect.Builder()
.addEvents(/* startTimeShiftMillis= */ 200L, existingEffect.getEvents())
.build();
vibrator.vibrate(combinedEffect);
إنشاء مقطوعات موسيقية متكررة
استخدِم setRepeatingEffect(startTimeMillis, repeatingEffect, durationMillis)
لإضافة نمط متكرّر إلى مقطوعة موسيقية:
Kotlin
val repeatingPattern = VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build()
val repeatingEffect = VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build()
vibrator.vibrate(repeatingEffect)
Java
VibrationEffect repeatingPattern = new VibrationEffect.Builder()
.addPreset(
0L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_CLICK, 1.0f
)
)
.addPreset(
150L,
VibrationEffect.Preset.create(
VibrationEffect.Preset.PRESET_LOW_TICK, 0.6f
)
)
.build();
VibrationEffect repeatingEffect = new VibrationEffect.Builder()
.setRepeatingEffect(
/* startTimeMillis= */ 0L,
/* effect= */ repeatingPattern,
/* durationMillis= */ 300L
)
.build();
vibrator.vibrate(repeatingEffect);
التوقيت والتحقّق من الصحة وإدارة الانحراف
عند تصميم مقاطع باستخدام VibrationEffect.Builder، يُرجى مراعاة قواعد التوقيت والتحقّق التالية:
- أوقات البدء المتزايدة بدقة: يجب أن يتضمّن كل عنصر تتم إضافته إلى أداة الإنشاء
startTimeMillisأكبر من وقت بدء العنصر السابق أو يساويه. - التحقّق أثناء الإنشاء: ينفّذ أداة الإنشاء عملية تحقّق بأفضل جهد ممكن في وقت
build()الإنشاء باستخدام مدة العناصر المعروفة (أو الحد الأدنى البالغ 1 مللي ثانية للإعدادات المسبقة). في حال رصد تداخل مستحيل، سيتم عرضIllegalArgumentException. - تغيير ترتيب التشغيل: إذا كان عنصر اهتزاز سابق لا يزال قيد التشغيل عندما يحين وقت بدء تشغيل العنصر التالي، سيغيّر إطار العمل تلقائيًا ترتيب العنصر التالي إلى أقرب فترة زمنية متاحة. يضمن ذلك عدم تداخل الأحداث وعدم حذف أي اهتزازات، مع أنّه قد يؤدي إلى اختلاف طفيف في التوقيت إذا تم تحديد مواعيد الأحداث بفواصل زمنية قصيرة جدًا. للحدّ من الانحراف، يجب إتاحة وقت كافٍ (مثل 50 مللي ثانية أو أكثر) بين أحداث اللمس المتتالية.
إنشاء تركيبات بدائية للاهتزاز
يوضّح هذا القسم كيفية إنشاء اهتزازات باستخدام
VibrationEffect.Composition. تتولى الشركة المصنّعة للجهاز تنفيذ العناصر الأساسية الخاصة بالتركيب، والتي تم توضيحها سابقًا في هذه الصفحة. وتوفّر هذه المحركات اهتزازًا واضحًا وموجزًا وممتعًا يتوافق مع مبادئ اللمس للحصول على تجربة لمس واضحة. لمزيد من التفاصيل حول هذه الإمكانات وطريقة عملها،
يُرجى الاطّلاع على مقدمة عن مشغّلات الاهتزاز.
على عكس VibrationEffect.Builder، لا تتضمّن واجهة برمجة التطبيقات VibrationEffect.Composition عمليات احتياطية تلقائية للعناصر الأساسية غير المتوافقة. وبالتالي:
قبل تفعيل ميزة "اللمس المتقدّم"، تأكَّد من أنّ الجهاز المعنيّ يتوافق مع جميع العناصر الأساسية التي تستخدمها.
عليك إيقاف مجموعة التجارب غير المتوافقة بالكامل، وليس فقط التأثيرات التي لا تتضمّن عنصرًا أساسيًا.
- 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. في ما يلي مثال على تأثير ارتفاع تدريجي
متبوع بتأثير نقرة حادة:
Kotlin
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_CLICK
).compose()
)
Java
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_CLICK)
.compose());
يتم إنشاء مقطوعة موسيقية من خلال إضافة عناصر أساسية ليتم تشغيلها بالتسلسل. كل عنصر أساسي قابل للتوسيع أيضًا، لذا يمكنك التحكّم في سعة الاهتزاز الناتج عن كل عنصر منها. يتم تحديد المقياس كقيمة بين 0 و1، حيث يرتبط 0 في الواقع بالحد الأدنى من السعة التي يمكن للمستخدم (بالكاد) الشعور بها.
إنشاء نُسخ مختلفة في عناصر الاهتزاز الأساسية
إذا أردت إنشاء نسخة ضعيفة ونسخة قوية من العنصر الأساسي نفسه، عليك إنشاء نسب قوة تبلغ 1.4 أو أكثر، كي يسهل إدراك الفرق في الشدة. لا تحاول إنشاء أكثر من ثلاثة مستويات شدة للسمة الأساسية نفسها، لأنّها ليست متميزة إدراكيًا. على سبيل المثال، استخدِم مقاييس 0.5 و0.7 و1.0 لإنشاء إصدارات منخفضة ومتوسطة وعالية الكثافة من عنصر أساسي.
إضافة فجوات بين عناصر الاهتزاز الأساسية
يمكن أن تحدّد التركيبة أيضًا فترات تأخير يجب إضافتها بين العناصر الأساسية المتتالية. يتم التعبير عن هذا التأخير بالملي ثانية منذ نهاية العنصر الأساسي السابق. بشكل عام، لا يمكن رصد فجوة تتراوح بين 5 و10 مللي ثانية بين عنصرَين أساسيَّين. استخدِم فجوة تبلغ 50 ملي ثانية أو أكثر إذا أردت إنشاء فجوة واضحة بين عنصرَين أساسيَّين. في ما يلي مثال على تركيبة تتضمّن تأخيرات:
Kotlin
val delayMs = 100
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
).compose()
)
Java
int delayMs = 100;
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
.compose());
التحقّق من العناصر الأساسية المتوافقة
يمكن استخدام واجهات برمجة التطبيقات التالية للتحقّق من توافق الجهاز مع عناصر أساسية معيّنة:
Kotlin
val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose())
} else {
// Play a predefined effect or custom pattern as a fallback.
}
Java
int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;
if (vibrator.areAllPrimitivesSupported(primitive)) {
vibrator.vibrate(VibrationEffect.startComposition()
.addPrimitive(primitive).compose());
} else {
// Play a predefined effect or custom pattern as a fallback.
}
يمكنك أيضًا التحقّق من عدة عناصر أساسية ثم تحديد العناصر التي تريد إنشاءها استنادًا إلى مستوى توافق الجهاز:
Kotlin
val effects: IntArray = intArrayOf(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
)
val supported: BooleanArray = vibrator.arePrimitivesSupported(primitives)
Java
int[] primitives = new int[] {
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
VibrationEffect.Composition.PRIMITIVE_TICK,
VibrationEffect.Composition.PRIMITIVE_CLICK
};
boolean[] supported = vibrator.arePrimitivesSupported(effects);
أمثلة على تركيبات الاهتزاز
تقدّم الأقسام التالية عدة أمثلة على تركيبات الاهتزاز، مأخوذة من نموذج تطبيق تقنية اللمس على GitHub.
مقاومة (مع عدد قليل من العلامات)
يمكنك التحكّم في سعة الاهتزاز الأساسي لنقل ملاحظات مفيدة بشأن إجراء قيد التنفيذ. يمكن استخدام قيم المقياس المتقاربة لإنشاء تأثير تصاعدي سلس لعنصر أساسي. يمكن أيضًا ضبط التأخير بين العناصر الأساسية المتتالية بشكل ديناميكي استنادًا إلى تفاعل المستخدم. يتضح ذلك في المثال التالي الخاص بصورة متحركة يتم التحكم فيها بواسطة إيماءة سحب ويتم تحسينها باستخدام ردود فعل لمسية.
الشكل 1: يمثّل هذا الشكل الموجي تسارع الاهتزاز الناتج على الجهاز.
Kotlin
@Composable
fun ResistScreen() {
// Control variables for the dragging of the indicator.
var isDragging by remember { mutableStateOf(false) }
var dragOffset by remember { mutableStateOf(0f) }
// Only vibrates while the user is dragging
if (isDragging) {
LaunchedEffect(Unit) {
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
while (true) {
// Calculate the interval inversely proportional to the drag offset.
val vibrationInterval = calculateVibrationInterval(dragOffset)
// Calculate the scale directly proportional to the drag offset.
val vibrationScale = calculateVibrationScale(dragOffset)
delay(vibrationInterval)
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale
).compose()
)
}
}
}
Screen() {
Column(
Modifier
.draggable(
orientation = Orientation.Vertical,
onDragStarted = {
isDragging = true
},
onDragStopped = {
isDragging = false
},
state = rememberDraggableState { delta ->
dragOffset += delta
}
)
) {
// Build the indicator UI based on how much the user has dragged it.
ResistIndicator(dragOffset)
}
}
}
Java
class DragListener implements View.OnTouchListener {
// Control variables for the dragging of the indicator.
private int startY;
private int vibrationInterval;
private float vibrationScale;
@Override
public boolean onTouch(View view, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startY = event.getRawY();
vibrationInterval = calculateVibrationInterval(0);
vibrationScale = calculateVibrationScale(0);
startVibration();
break;
case MotionEvent.ACTION_MOVE:
float dragOffset = event.getRawY() - startY;
// Calculate the interval inversely proportional to the drag offset.
vibrationInterval = calculateVibrationInterval(dragOffset);
// Calculate the scale directly proportional to the drag offset.
vibrationScale = calculateVibrationScale(dragOffset);
// Build the indicator UI based on how much the user has dragged it.
updateIndicator(dragOffset);
break;
case MotionEvent.ACTION_CANCEL:
case MotionEvent.ACTION_UP:
// Only vibrates while the user is dragging
cancelVibration();
break;
}
return true;
}
private void startVibration() {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
vibrationScale)
.compose());
// Continuously run the effect for vibration to occur even when the view
// is not being drawn, when user stops dragging midway through gesture.
handler.postDelayed(this::startVibration, vibrationInterval);
}
private void cancelVibration() {
handler.removeCallbacksAndMessages(null);
}
}
توسيع (مع ارتفاع وانخفاض)
هناك عنصران أساسيان لزيادة شدة الاهتزاز المحسوس:
PRIMITIVE_QUICK_RISE وPRIMITIVE_SLOW_RISE. ويصل كلاهما إلى الهدف نفسه، ولكن بمدد مختلفة. لا يتوفّر سوى عنصر أساسي واحد
لخفض السرعة، وهو PRIMITIVE_QUICK_FALL. تعمل هذه العناصر الأساسية بشكل أفضل معًا لإنشاء مقطع موجي يزداد شدةً ثم ينخفض. ويمكنك محاذاة العناصر الأساسية التي تم تغيير حجمها لمنع حدوث ارتفاعات مفاجئة في السعة بينها، وهو ما يساعد أيضًا في إطالة مدة التأثير الإجمالية.
من الناحية الإدراكية، يلاحظ الأشخاص دائمًا الجزء الصاعد أكثر من الجزء الهابط، لذا يمكن استخدام الجزء الصاعد أقصر من الجزء الهابط لتوجيه التركيز نحو الجزء الهابط.
في ما يلي مثال على تطبيق هذه التركيبة لتوسيع دائرة وتصغيرها. يمكن أن يعزّز تأثير الارتفاع الشعور بالتوسّع أثناء الصورة المتحركة. يساعد الجمع بين تأثيرَي الارتفاع والانخفاض في التأكيد على عملية التصغير في نهاية الصورة المتحركة.
الشكل 2: يمثّل هذا الشكل الموجي تسارع الاهتزاز الناتج على الجهاز.
Kotlin
enum class ExpandShapeState {
Collapsed,
Expanded
}
@Composable
fun ExpandScreen() {
// Control variable for the state of the indicator.
var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }
// Animation between expanded and collapsed states.
val transitionData = updateTransitionData(currentState)
Screen() {
Column(
Modifier
.clickable(
{
if (currentState == ExpandShapeState.Collapsed) {
currentState = ExpandShapeState.Expanded
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
0.3f
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
0.3f
).compose()
)
} else {
currentState = ExpandShapeState.Collapsed
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
).compose()
)
}
)
) {
// Build the indicator UI based on the current state.
ExpandIndicator(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
private final Animation expandAnimation;
private final Animation collapseAnimation;
private boolean isExpanded;
ClickListener(Context context) {
expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
expandAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
.compose());
}
});
collapseAnimation = AnimationUtils
.loadAnimation(context, R.anim.collapse);
collapseAnimation.setAnimationListener(new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
.compose());
}
});
}
@Override
public void onClick(View view) {
view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
isExpanded = !isExpanded;
}
}
التأرجح (مع الدوران)
من المبادئ الأساسية للمس إبهار المستخدمين. يمكنك استخدام PRIMITIVE_SPIN كطريقة مسلّية لإضافة تأثير اهتزاز لطيف وغير متوقّع. تكون هذه السمة الأساسية أكثر فعالية عند استدعائها أكثر من مرة. يمكن أن يؤدي ربط عدة عمليات تدوير إلى إنشاء تأثير متذبذب وغير ثابت، ويمكن تعزيز هذا التأثير بشكل أكبر من خلال تطبيق تغيير حجم عشوائي إلى حد ما على كل شكل أساسي. يمكنك أيضًا تجربة الفجوة بين العناصر الأساسية المتتالية في عملية الدوران. يؤدي إجراء دورتَين بدون أي فجوة (0 مللي ثانية بينهما) إلى إنشاء إحساس قوي بالدوران. تؤدي زيادة الفجوة بين الدورات من 10 إلى 50 مللي ثانية إلى
إحساس أقل بالدوران، ويمكن استخدامها لمطابقة مدة الفيديو أو
الرسوم المتحركة.
لا تستخدِم فجوة تزيد مدتها عن 100 مللي ثانية، لأنّ الدورات المتتالية لن تتكامل بشكل جيد وستبدو كتأثيرات فردية.
في ما يلي مثال على شكل مرن يرتدّ إلى الأعلى بعد سحبه إلى الأسفل ثم تركه. يتم تحسين الرسوم المتحركة باستخدام تأثيرَين للدوران، ويتم تشغيلهما بدرجات متفاوتة تتناسب مع مقدار الإزاحة الناتجة عن الارتداد.
الشكل 3: يمثّل هذا الشكل الموجي تسارع الاهتزاز الناتج على الجهاز.
Kotlin
@Composable
fun WobbleScreen() {
// Control variables for the dragging and animating state of the elastic.
var dragDistance by remember { mutableStateOf(0f) }
var isWobbling by remember { mutableStateOf(false) }
// Use drag distance to create an animated float value behaving like a spring.
val dragDistanceAnimated by animateFloatAsState(
targetValue = if (dragDistance > 0f) dragDistance else 0f,
animationSpec = spring(
dampingRatio = Spring.DampingRatioHighBouncy,
stiffness = Spring.StiffnessMedium
),
)
if (isWobbling) {
LaunchedEffect(Unit) {
while (true) {
val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement > SPIN_MIN_DISPLACEMENT) {
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).addPrimitive(
VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement)
).compose()
)
}
// Delay the next check for a sufficient duration until the
// current composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
delay(VIBRATION_DURATION)
}
}
}
Box(
Modifier
.fillMaxSize()
.draggable(
onDragStopped = {
isWobbling = true
dragDistance = 0f
},
orientation = Orientation.Vertical,
state = rememberDraggableState { delta ->
isWobbling = false
dragDistance += delta
}
)
) {
// Draw the wobbling shape using the animated spring-like value.
WobbleShape(dragDistanceAnimated)
}
}
// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
// Generate a random offset in the range [-0.1, +0.1] to be added to the
// vibration scale so the spin effects have slightly different values.
val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}
Java
class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
private final Random vibrationRandom = new Random(seed);
private final long lastVibrationUptime;
@Override
public void onAnimationUpdate(
DynamicAnimation animation, float value, float velocity) {
// Delay the next check for a sufficient duration until the current
// composition finishes. Note that you can use
// Vibrator.getPrimitiveDurations API to calculcate the delay.
if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
return;
}
float displacement = calculateRelativeDisplacement(value);
// Use some sort of minimum displacement so the final few frames
// of animation don't generate a vibration.
if (displacement < SPIN_MIN_DISPLACEMENT) {
return;
}
lastVibrationUptime = SystemClock.uptimeMillis();
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
nextSpinScale(displacement))
.compose());
}
// Calculate a random scale for each spin to vary the full effect.
float nextSpinScale(float displacement) {
// Generate a random offset in the range [-0.1,+0.1] to be added to
// the vibration scale so the spin effects have slightly different
// values.
float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
}
}
الارتداد (مع أصوات خافتة)
من التطبيقات المتقدّمة الأخرى للتأثيرات الاهتزازية محاكاة التفاعلات المادية. يمكن أن يؤدي استخدام PRIMITIVE_THUD إلى إنشاء تأثير قوي ومؤثر، ويمكن دمجه مع تصور لتأثير ما، في فيديو أو رسم متحرك مثلاً، لتحسين التجربة بشكل عام.
في ما يلي مثال على صورة متحركة لسقوط كرة تم تحسينها باستخدام تأثير صوت ارتطام يتم تشغيله في كل مرة ترتد فيها الكرة عن أسفل الشاشة:
الشكل 4. يمثّل هذا الشكل الموجي تسارع الاهتزاز الناتج على الجهاز.
Kotlin
enum class BallPosition {
Start,
End
}
@Composable
fun BounceScreen() {
// Control variable for the state of the ball.
var ballPosition by remember { mutableStateOf(BallPosition.Start) }
var bounceCount by remember { mutableStateOf(0) }
// Animation for the bouncing ball.
var transitionData = updateTransitionData(ballPosition)
val collisionData = updateCollisionData(transitionData)
// Ball is about to contact floor, only vibrating once per collision.
var hasVibratedForBallContact by remember { mutableStateOf(false) }
if (collisionData.collisionWithFloor) {
if (!hasVibratedForBallContact) {
val vibrationScale = 0.7.pow(bounceCount++).toFloat()
vibrator.vibrate(
VibrationEffect.startComposition().addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale
).compose()
)
hasVibratedForBallContact = true
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false
}
Screen() {
Box(
Modifier
.fillMaxSize()
.clickable {
if (transitionData.isAtStart) {
ballPosition = BallPosition.End
} else {
ballPosition = BallPosition.Start
bounceCount = 0
}
},
) {
// Build the ball UI based on the current state.
BouncingBall(transitionData)
}
}
}
Java
class ClickListener implements View.OnClickListener {
@Override
public void onClick(View view) {
view.animate()
.translationY(targetY)
.setDuration(3000)
.setInterpolator(new BounceInterpolator())
.setUpdateListener(new AnimatorUpdateListener() {
boolean hasVibratedForBallContact = false;
int bounceCount = 0;
@Override
public void onAnimationUpdate(ValueAnimator animator) {
boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
if (valueBeyondThreshold) {
if (!hasVibratedForBallContact) {
float vibrationScale = (float) Math.pow(0.7, bounceCount++);
vibrator.vibrate(
VibrationEffect.startComposition()
.addPrimitive(
VibrationEffect.Composition.PRIMITIVE_THUD,
vibrationScale)
.compose());
hasVibratedForBallContact = true;
}
} else {
// Reset for next contact with floor.
hasVibratedForBallContact = false;
}
}
});
}
}
شكل موجي للاهتزاز مع أشكال
تتيح لك عملية إنشاء أنماط اهتزاز مخصّصة التحكّم في سعة الاهتزاز لإنشاء تأثيرات سلسة من خلال زيادة الاهتزاز وخفضه تدريجيًا. يوضّح هذا القسم كيفية إنشاء تأثيرات لمسية ديناميكية باستخدام أشكال موجية تتيح التحكّم الدقيق في سعة الاهتزاز وتردده بمرور الوقت. يتيح لك ذلك تصميم تجارب لمسية أكثر ثراءً ودقةً.
بدءًا من Android 16 (المستوى 36 من واجهة برمجة التطبيقات)، يوفّر النظام واجهات برمجة التطبيقات التالية لإنشاء مغلف شكل موجة اهتزاز من خلال تحديد سلسلة من نقاط التحكّم:
BasicEnvelopeBuilder: طريقة سهلة لإنشاء مؤثرات لمسية متوافقة مع جميع الأجهزة.WaveformEnvelopeBuilder: طريقة أكثر تقدّمًا لإنشاء مؤثرات لمسية، تتطلّب معرفة بأجهزة اللمس.
لا يوفّر نظام التشغيل Android بدائل لتأثيرات التغليف. إذا كنت بحاجة إلى هذا الدعم، يُرجى إكمال الخطوات التالية:
- تحقَّق مما إذا كان جهاز معيّن يتوافق مع تأثيرات المغلف باستخدام
Vibrator.areEnvelopeEffectsSupported(). - عليك إيقاف مجموعة التجارب المتّسقة غير المتوافقة، أو استخدام أنماط اهتزاز مخصّصة أو تركيبات كبدائل احتياطية.
لإنشاء المزيد من تأثيرات الظرف الأساسية، استخدِم BasicEnvelopeBuilder مع المَعلمات التالية:
- قيمة الشدة ضمن النطاق \( [0, 1] \)، والتي تمثّل قوة الاهتزاز المحسوسة. على سبيل المثال، يتم اعتبار القيمة \( 0.5 \)نصف الحد الأقصى العالمي لمستوى الجهد الذي يمكن أن يحققه الجهاز.
قيمة الحدة في النطاق \( [0, 1] \)، والتي تمثّل حدة الاهتزاز وتؤدي القيم المنخفضة إلى اهتزازات أكثر سلاسة، بينما تؤدي القيم الأعلى إلى إحساس أكثر حدة.
قيمة المدة التي تمثّل الوقت المستغرَق بالملّي ثانية للانتقال من آخر نقطة تحكّم، أي زوج من الشدّة والحدة، إلى النقطة الجديدة
في ما يلي مثال على شكل موجي يزيد من شدة الاهتزاز من مستوى منخفض إلى مستوى مرتفع، مع اهتزاز بأقصى قوة على مدار 500 مللي ثانية، ثم يعود إلى مستوى\( 0 \) (إيقاف) على مدار 100 مللي ثانية.
vibrator.vibrate(VibrationEffect.BasicEnvelopeBuilder()
.setInitialSharpness(0.0f)
.addControlPoint(1.0f, 1.0f, 500)
.addControlPoint(0.0f, 1.0f, 100)
.build()
)
إذا كان لديك معرفة أكثر تقدّمًا بشأن اللمسات، يمكنك تحديد تأثيرات الغلاف باستخدام WaveformEnvelopeBuilder. عند استخدام هذا العنصر، يمكنك الوصول إلى
ربط التردد بتسارع الإخراج (FOAM) من خلال
VibratorFrequencyProfile.
- قيمة السعة في النطاق \( [0, 1] \)، والتي تمثّل قوة الاهتزاز التي يمكن تحقيقها عند تردد معيّن، كما يحدّدها FOAM للجهاز. على سبيل المثال، تؤدي القيمة \( 0.5 \) إلى توليد نصف الحد الأقصى لتسارع الإخراج الذي يمكن تحقيقه عند التردد المحدّد.
قيمة معدّل التكرار، ويتم تحديدها بالهرتز.
قيمة المدة، وهي تمثّل الوقت المستغرَق بالملّي ثانية للانتقال من نقطة التحكّم الأخيرة إلى النقطة الجديدة.
يوضّح الرمز التالي مثالاً على شكل موجي يحدّد تأثير اهتزاز لمدة 400 مللي ثانية. يبدأ التأثير بتزايد في السعة بمقدار 50 مللي ثانية، من إيقاف التشغيل إلى التشغيل الكامل، بتردد ثابت يبلغ 60 هرتز. بعد ذلك، يتزايد التردد إلى 120 هرتز خلال الـ 100 مللي ثانية التالية ويظل عند هذا المستوى لمدة 200 مللي ثانية. وأخيرًا، تتناقص السعة إلى \( 0 \)، ويعود التردد إلى 60 هرتز خلال الـ 50 مللي ثانية الأخيرة:
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 لمحاكاة التفاعلات المادية المرتدة. توفّر واجهة برمجة التطبيقات الأساسية للمغلف تحكّمًا أدق بكثير، ما يتيح لك تخصيص شدة الاهتزاز وحدّته بدقة.
ويؤدي ذلك إلى توفير تجاوب حسّي يتوافق بشكلٍ أكثر دقة مع الأحداث المتحركة.
إليك مثال على زنبرك يسقط بحرية مع تحسين الصورة المتحركة باستخدام تأثير غلاف أساسي يتم تشغيله في كل مرة يرتد فيها الزنبرك عن أسفل الشاشة:
الشكل 5. رسم بياني لموجة تسارع الإخراج لاهتزاز يحاكي نابضًا مرنًا
@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")
}
}
}
إطلاق صاروخ
يوضّح نموذج سابق كيفية استخدام واجهة برمجة التطبيقات الأساسية الخاصة بالمغلف لمحاكاة تفاعل نابض. تتيح واجهة برمجة التطبيقات WaveformEnvelopeBuilder إمكانية التحكّم الدقيق في نطاق التردد الكامل للجهاز، ما يتيح إنشاء مؤثرات لمسية مخصّصة للغاية. ومن خلال الجمع بين هذه البيانات وبيانات FOAM، يمكنك تخصيص الاهتزازات لتناسب إمكانات ترددية معيّنة.
في ما يلي مثال يوضّح محاكاة لإطلاق صاروخ باستخدام نمط اهتزاز ديناميكي. يتراوح التأثير من الحد الأدنى لناتج تسارع التردد المتوافق، وهو 0.1 G، إلى التردد الرنيني، مع الحفاظ دائمًا على إدخال سعة بنسبة %10. يسمح ذلك بتطبيق التأثير بقوة معقولة في البداية وزيادة الشدة والحدة الظاهرتين، على الرغم من أنّ السعة الأساسية تظل كما هي. عند الوصول إلى حالة الرنين، ينخفض معدّل تكرار التأثير إلى الحد الأدنى، ما يؤدي إلى انخفاض الشدة والحدة. يؤدي ذلك إلى الشعور بمقاومة أولية يعقبها تحرّر، ما يحاكي عملية الإطلاق إلى الفضاء.
لا يمكن تحقيق هذا التأثير باستخدام واجهة برمجة التطبيقات الأساسية الخاصة بالظرف، لأنّها تجرّد المعلومات الخاصة بالجهاز بشأن التردد الرنيني ومنحنى تسارع الإخراج. يمكن أن تؤدي زيادة الحدة إلى دفع التردد المكافئ إلى ما بعد الرنين، ما قد يتسبب في انخفاض غير مقصود في التسارع.
الشكل 6. رسم بياني لموجة تسارع الإخراج لاهتزاز يحاكي عملية إطلاق صاروخ
@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 تصميم العديد من المؤثرات اللمسية المعقّدة من خلال إتاحة التحكّم في مقاطع السعة والتردد في الاهتزاز. مثال آخر على هذا النوع من التصاميم هو محاكاة الأحاسيس المادية الأكثر تجريدًا، مثل "الحيوية".
ويمكن تحقيق ذلك من خلال تمثيل المؤشرات الحيوية لإشارة تخطيط القلب الكهربائي النموذجية باستخدام مقاطع اهتزاز ذات سعات وترددات معيّنة. LavaBeats هو مثال يتم فيه تمثيل مقطعين مميزين من تسجيل تخطيط القلب الكهربائي على شكل نبضتين يفصل بينهما تأخير زمني. النبضة الأولى المميزة هي مركب QRS، الذي يظهر كقمة حادة ذات سعة عالية ومدة قصيرة. النبضة الثانية هي الموجة T، وهي ذات سعة أقل ومدّة أطول وشكل أكثر سلاسة (راجِع الشكل 7).
استخدِم WaveformEnvelopeBuilder لإنشاء تكرارات مختلفة لهاتين النبضتين مفصولتين بتأخير ثابت من النبضة الأولى إلى الثانية. يمكن أن تكون النبضة الأولى إشارة صفير تبدأ بتردد منخفض وتنتهي بتردد أعلى خلال مدة قصيرة. يمكن تمثيل النبضة الثانية كفترة واحدة من موجة جيبية منخفضة التردد. يمكننا دمج النبضتين في إيقاع،
وتكرار الدمج عدة مرات مع تأخير بين كل نبضة وأخرى، وذلك باتّباع
معدّل نموذجي للنبضات في الدقيقة (bpm). والنتيجة هي تأثير لمسي
يشبه نبض القلب.
يمكنك تجربة LavaBeats في نموذج تطبيق تقنية اللمس على GitHub والشعور بالتأثير المصحوب بتصوّر لمصباح الحمم البركانية ينبض بالإيقاع نفسه الذي ينبض به تأثير تقنية اللمس. يمكنك أيضًا تغيير إعدادات التأثير لإنشاء
إحساسات نبض مختلفة من خلال تعديل السعات والترددات والمدد
والتأخيرات الخاصة بالنبضتَين.
الشكل 7. مقطع من تسجيل تخطيط القلب الكهربائي مع مركّب 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,
)