تتناول هذه الصفحة أمثلة على كيفية استخدام واجهات برمجة التطبيقات لتأثيرات اللمس المختلفة لخلق مؤثرات مخصّصة تتجاوز أشكال الموجات العادية للاهتزاز في أحد التطبيقات المتوافقة مع نظام Android.
تتضمّن هذه الصفحة الأمثلة التالية:
- أنماط الاهتزاز المخصّصة
- نمط الزيادة التدريجية: نمط يبدأ بسلاسة.
- نمط متكرّر: نمط لا ينتهي
- النمط مع العنصر الاحتياطي: عرض توضيحي للعنصر الاحتياطي
- تركيبات الاهتزاز
للاطّلاع على أمثلة إضافية، يمكنك الاطّلاع على مقالة إضافة ردود فعل لمسية إلى الأحداث، ويجب دائمًا اتّباع مبادئ تصميم اللمس.
استخدام العناصر الاحتياطية للتعامل مع التوافق مع الأجهزة
عند تنفيذ أي تأثير مخصّص، يجب مراعاة ما يلي:
- ميزات الجهاز المطلوبة لاستخدام التأثير
- الإجراءات التي يجب اتّخاذها عندما لا يكون الجهاز قادرًا على تشغيل التأثير
يوفّر مرجع واجهة برمجة التطبيقات لتأثيرات لمس Android تفاصيل حول كيفية التحقّق من توفّر المكوّنات المعنيّة بتأثيرات اللمس، حتى يتمكّن تطبيقك من تقديم تجربة شاملة متّسقة.
استنادًا إلى حالة الاستخدام، قد تحتاج إلى إيقاف المؤثرات المخصّصة أو توفير مؤثرات مخصّصة بديلة استنادًا إلى الإمكانات المحتملة المختلفة.
خطط للفئات العالية المستوى التالية لإمكانيات الجهاز:
إذا كنت تستخدِم أساسيات اللمس: الأجهزة المتوافقة مع هذه الأساسيات التي تحتاجها التأثيرات المخصّصة (اطّلِع على القسم التالي لمعرفة تفاصيل عن العناصر الأساسية).
الأجهزة التي تتضمّن ميزة التحكّم في الشدة
الأجهزة التي تتيح استخدام ميزة الاهتزاز (تفعيل/إيقاف)، أي الأجهزة التي لا تتيح التحكّم في شدة الاهتزاز
إذا كان خيار التأثيرات الحسية في تطبيقك يراعي هذه الفئات، يجب أن تظل تجربة المستخدم الحسية متوقّعة لأي جهاز فردي.
استخدام العناصر الأساسية لللمس
يتضمّن Android عدة عناصر أساسية لللمس تختلف في كلّ من الشدة والتردد. يمكنك استخدام شكل أولي واحد فقط أو أشكال أولية متعددة معًا لتحقيق تأثيرات لمسية غنية.
- استخدِم تأخيرات تبلغ 50 ملي ثانية أو أكثر للفواصل الزمنية الواضحة بين العنصرَين الأساسيَين، مع مراعاة مدة العنصر الأساسي إن أمكن.
- استخدِم مقاييس تختلف بنسبة 1.4 أو أكثر حتى يتم تمييز الفرق في الشدة بشكل أفضل.
استخدِم المقاييس 0.5 و0.7 و1.0 لإنشاء إصدارات منخفضة ومتوسطة وعالية الكثافة لعنصر أساسي.
إنشاء أنماط اهتزاز مخصّصة
غالبًا ما تُستخدَم أنماط الاهتزاز في تقنية اللمس التي تجذب الانتباه، مثل الإشعارات
ونغمات الرنين. يمكن لخدمة Vibrator
تشغيل أنماط اهتزاز طويلة
تؤدي إلى تغيير سعة الاهتزاز بمرور الوقت. وتُسمّى هذه التأثيرات
الموجات الصوتية.
يمكن عادةً رصد تأثيرات أشكال الموجات، ولكن يمكن أن تفاجئ الارتجاجات الطويلة المفاجئة المستخدم إذا تم تشغيلها في بيئة هادئة. قد يؤدي أيضًا الارتفاع إلى شدة صوت مستهدَفة بشكلٍ سريع إلى إصدار ضوضاء صاخبة. يمكنك تصميم أنماط منحنيات صوتية لمنح سلاسة لعمليات النقل في الشدة من أجل إنشاء تأثيرات متزايدة أو متناقصة.
أمثلة على أنماط الاهتزاز
تقدّم الأقسام التالية عدة أمثلة على أنماط الاهتزاز:
نمط توفير الميزة
يتم تمثيل أشكال الموجات على شكل VibrationEffect
مع ثلاث مَعلمات:
- المواقيت: صفيف للمدّات، بالمللي ثانية، لكلّ قطعة من منحنى إشارة الصوت
- المعدّلات: هي معدّل الاهتزاز المطلوب لكل مدة محدّدة في الوسيطة الأولى، ويتم تمثيلها بقيمة عددية من 0 إلى 255، حيث يمثّل الصفر "حالة الإيقاف" للاهتزاز ويكون 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));
}
إنشاء تركيبات اهتزاز
يقدّم هذا القسم طرقًا لإنشاء اهتزازات في شكل تأثيرات مخصّصة أطول وأكثر تعقيدًا، ويتجاوز ذلك لاستكشاف تجارب لمس ثرية باستخدام إمكانات الأجهزة المتقدّمة. يمكنك استخدام مجموعات من التأثيرات التي تتغيّر فيها amplitude وfrequency لإنشاء تأثيرات لمسية أكثر تعقيدًا على الأجهزة التي تتضمّن ملفّات تشغيل لمسية ذات نطاق تردد أوسع.
توضِّح عملية إنشاء أنماط اهتزاز مخصّصة، الموضّحة سابقًا في هذه الصفحة، كيفية التحكّم في شدّة الاهتزاز لإنشاء تأثيرات سلسله للزيادة والنقصان. تعمل تقنية "اللمسات الحسية" الغنية على تحسين هذا المفهوم من خلال استكشاف نطاق التردد الأوسع لجهاز الاهتزاز لجعل التأثير أكثر سلاسة. تكون أشكال الموجات هذه فعّالة بشكل خاص في إنشاء تأثيرات الزيادة والتقليل في الصوت.
إنّ العناصر الأساسية للتركيب، الموضّحة سابقًا في هذه الصفحة، هي من تنفيذ الشركة المصنّعة للجهاز. وتوفّر هذه الميزة اهتزازًا واضحًا وقصيرًا وممتعًا يتوافق مع مبادئ اللمس للحصول على لمسة واضحة. لمزيد من التفاصيل عن هذه الإمكانات وطريقة عملها، اطّلِع على مقدّمة عن ملفّات برمجة وحدات التحكّم في الاهتزاز.
لا يوفّر Android عناصر احتياطية للمقطوعات التي تحتوي على عناصر أساسية غير متوافقة. لذلك، عليك اتّباع الخطوات التالية:
قبل تفعيل ميزة "اللمس المتقدّم"، تأكَّد من أنّ الجهاز المعني يتيح استخدام جميع العناصر الأساسية التي تستخدمها.
أوقِف المجموعة المتّسقة من التجارب غير المتوافقة، وليس فقط التأثيرات التي لا تتضمّن عنصرًا أساسيًا.
يمكنك الاطّلاع على مزيد من المعلومات حول كيفية التحقّق من توفّر الجهاز في القسمين التاليين:
إنشاء تأثيرات اهتزاز مركبة
يمكنك إنشاء تأثيرات اهتزاز مركبة باستخدام
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، ويرتبط الصفر في الواقع بحد أدنى من السعة التي يمكن للمستخدم (بالكاد) أن يشعر فيها بهذه القيمة الأساسية.
إنشاء صِيغ في العناصر الأساسية للاهتزاز
إذا أردت إنشاء نسخة ضعيفة وقوية من الشكل الأساسي نفسه، أنشئ نسبتَي قوة تبلغان 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;
}
}
});
}
}