تتناول هذه الصفحة أمثلة على كيفية استخدام واجهات برمجة التطبيقات لتأثيرات اللمس المختلفة لخلق مؤثرات مخصّصة تتجاوز أشكال الموجات العادية للاهتزاز في أحد التطبيقات المتوافقة مع نظام 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;
}
}
});
}
}
شكل موجة الاهتزاز مع المظروفات
تتيح لك عملية إنشاء أنماط اهتزاز مخصّصة التحكّم في سعة الاهتزاز لإنشاء تأثيرات سلسة للزيادة والنقصان. يوضِّح هذا القسم كيفية إنشاء تأثيرات لمسية ديناميكية باستخدام لفات الموجات التي تتيح التحكّم بدقة في سعة الاهتزاز ومعدّل تكراره بمرور الوقت. يتيح لك ذلك إنشاء تجارب لمسية أكثر ثراءً ودقيقة.
بدءًا من الإصدار 16 من نظام التشغيل Android (المستوى 36 من واجهة برمجة التطبيقات)، يقدّم النظام واجهات برمجة التطبيقات التالية ل إنشاء غلاف شكل موجي للاهتزاز من خلال تحديد تسلسل نقاط التحكّم:
BasicEnvelopeBuilder
: نهج سهل الاستخدام لإنشاء تأثيرات لمسية لا تعتمد على الأجهزةWaveformEnvelopeBuilder
: نهج أكثر تقدمًا لإنشاء تأثيرات haptic، ويتطلب معرفة بالأجهزة المزوّدة بتقنية haptics.
لا يقدّم 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
لمحاكاة تفاعلات الارتداد المادي. توفّر واجهة برمجة التطبيقات الأساسية للغلاف مزيدًا من التحكّم العميق، ما يتيح لك ضبط شدة الاهتزاز وحدّته بدقة.
ويؤدي ذلك إلى توفير ملاحظات لمسية تتّبع الأحداث المتحركة بدقة أكبر.
في ما يلي مثال على زنبرك متدحرج بحرية مع تحسين الصورة المتحركة باستخدام أثر ملف تعريف أساسي يتم تشغيله كلما ارتدّ الزنبرك عن قاع الشاشة:
@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 desired 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% من الشدة. يتيح ذلك بدء التأثير بإخراج قوي إلى حدٍ ما وزيادة الكثافة والحدة المُلاحظة، على الرغم من أنّ اتّساع الموجة هو نفسه. عند بلوغ حالة الرنين، تنخفض وتيرة التأثير مجددًا إلى الحد الأدنى، ما يُلاحظه المستخدمون على أنّه انخفاض في الكثافة والحدة. ويؤدي ذلك إلى إحساس بالمقاومة الأولية متبوعًا بإطلاق، ما يحاكي الانطلاق إلى الفضاء.
لا يمكن تطبيق هذا التأثير باستخدام واجهة برمجة التطبيقات الأساسية للغلاف، لأنّها تزيل المعلومات الخاصة بالجهاز عن التردد الرنان ومنحنى تسارع الإخراج. يمكن أن تؤدي زيادة الحدة إلى رفع التردد المكافئ إلى ما بعد التردد التوافقي، مما قد يؤدي إلى انخفاض غير مقصود في التسارع.
@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 desired 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()
)
}