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

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

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

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

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

هنگام اجرای هر افکت سفارشی، موارد زیر را در نظر بگیرید:

  • کدام قابلیت های دستگاه برای اثر مورد نیاز است
  • وقتی دستگاه قادر به پخش افکت نیست چه باید کرد

مرجع Android haptics API جزئیاتی را در مورد نحوه بررسی پشتیبانی از مؤلفه‌های درگیر در هاپتیک شما ارائه می‌کند تا برنامه شما بتواند یک تجربه کلی ثابت را ارائه دهد.

بسته به مورد استفاده شما، ممکن است بخواهید جلوه های سفارشی را غیرفعال کنید یا افکت های سفارشی جایگزین را بر اساس قابلیت های بالقوه مختلف ارائه دهید.

برای کلاس های سطح بالای قابلیت دستگاه زیر برنامه ریزی کنید:

  • اگر از اولیه‌های لمسی استفاده می‌کنید: دستگاه‌هایی که از آن اولیه‌های مورد نیاز افکت‌های سفارشی پشتیبانی می‌کنند. (برای جزئیات بیشتر در مورد بدوی به بخش بعدی مراجعه کنید.)

  • دستگاه هایی با کنترل دامنه

  • دستگاه‌هایی با پشتیبانی ارتعاش اولیه (روشن/خاموش) - به عبارت دیگر، دستگاه‌هایی که فاقد کنترل دامنه هستند.

اگر انتخاب اثر لمسی برنامه شما برای این دسته بندی ها باشد، تجربه کاربری لمسی آن باید برای هر دستگاه جداگانه قابل پیش بینی باقی بماند.

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

اندروید شامل چندین نسخه اولیه لمسی است که هم از نظر دامنه و هم از نظر فرکانس متفاوت هستند. برای دستیابی به اثرات لمسی غنی، می توانید از یک اولیه به تنهایی یا چند نمونه اولیه در ترکیب استفاده کنید.

  • از تأخیرهای 50 میلی‌ثانیه یا بیشتر برای شکاف‌های قابل تشخیص بین دو نمونه اولیه استفاده کنید، همچنین در صورت امکان، مدت زمان اولیه را نیز در نظر بگیرید.
  • از مقیاس هایی استفاده کنید که با نسبت 1.4 یا بیشتر تفاوت دارند تا تفاوت در شدت بهتر درک شود.
  • از مقیاس های 0.5، 0.7، و 1.0 برای ایجاد یک نسخه با شدت کم، متوسط ​​و زیاد از یک اولیه استفاده کنید.

الگوهای ارتعاشی سفارشی ایجاد کنید

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

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

نمونه هایی از الگوهای ارتعاش

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

الگوی شیب دار

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

  1. زمان بندی: آرایه ای از مدت زمان، بر حسب میلی ثانیه، برای هر بخش شکل موج.
  2. دامنه: دامنه ارتعاش مورد نظر برای هر مدت زمان مشخص شده در آرگومان اول، که با یک عدد صحیح از 0 تا 255 نشان داده شده است، با 0 نشان دهنده "وضعیت خاموش" ویبراتور و 255 حداکثر دامنه دستگاه است.
  3. Repeat index: شاخصی در آرایه مشخص شده در آرگومان اول برای شروع تکرار شکل موج، یا -1 اگر باید الگو را فقط یک بار پخش کند.

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

کاتلین

val timings: LongArray = longArrayOf(
    50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200)
val amplitudes: IntArray = intArrayOf(
    33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255)
val repeatIndex = -1 // Don't repeat.

vibrator.vibrate(VibrationEffect.createWaveform(
    timings, amplitudes, repeatIndex))

جاوا

long[] timings = new long[] {
    50, 50, 50, 50, 50, 100, 350, 25, 25, 25, 25, 200 };
int[] amplitudes = new int[] {
    33, 51, 75, 113, 170, 255, 0, 38, 62, 100, 160, 255 };
int repeatIndex = -1; // Don't repeat.

vibrator.vibrate(VibrationEffect.createWaveform(
    timings, amplitudes, repeatIndex));

الگوی تکراری

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

کاتلین

void startVibrating() {
val timings: LongArray = longArrayOf(50, 50, 100, 50, 50)
val amplitudes: IntArray = intArrayOf(64, 128, 255, 128, 64)
val repeat = 1 // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
    timings, amplitudes, repeat)
// repeatingEffect can be used in multiple places.

vibrator.vibrate(repeatingEffect)
}

void stopVibrating() {
vibrator.cancel()
}

جاوا

void startVibrating() {
long[] timings = new long[] { 50, 50, 100, 50, 50 };
int[] amplitudes = new int[] { 64, 128, 255, 128, 64 };
int repeat = 1; // Repeat from the second entry, index = 1.
VibrationEffect repeatingEffect = VibrationEffect.createWaveform(
    timings, amplitudes, repeat);
// repeatingEffect can be used in multiple places.

vibrator.vibrate(repeatingEffect);
}

void stopVibrating() {
vibrator.cancel();
}

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

الگوی با بازگشت

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

کاتلین

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

جاوا

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

ترکیبات ارتعاشی ایجاد کنید

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

فرآیند ایجاد الگوهای ارتعاش سفارشی ، که قبلاً در این صفحه توضیح داده شد، نحوه کنترل دامنه ارتعاش را برای ایجاد جلوه‌های صاف افزایش و پایین رفتن توضیح می‌دهد. Rich Haptics این مفهوم را با کاوش در محدوده فرکانس وسیع تر ویبراتور دستگاه بهبود می بخشد تا جلوه را حتی روان تر کند. این شکل موج ها به ویژه در ایجاد یک افکت کرشندو یا دیمینوندو موثر هستند.

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

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

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

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

اطلاعات بیشتر در مورد نحوه بررسی پشتیبانی دستگاه در بخش های زیر نشان داده شده است.

جلوه های ارتعاشی ترکیبی ایجاد کنید

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

کاتلین

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

جاوا

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

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

ایجاد انواع در ارتعاشات اولیه

اگر می‌خواهید یک نسخه ضعیف و قوی از همان ابتدایی ایجاد کنید، نسبت‌های قدرتی 1.4 یا بیشتر ایجاد کنید، تا تفاوت در شدت به راحتی قابل درک باشد. سعی نکنید بیش از سه سطح شدت از همان ابتدایی ایجاد کنید، زیرا آنها از نظر ادراکی متمایز نیستند. به عنوان مثال، از مقیاس های 0.5، 0.7، و 1.0 برای ایجاد نسخه های کم، متوسط ​​و بالا از یک اولیه استفاده کنید.

فاصله بین ارتعاشات اولیه را اضافه کنید

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

کاتلین

val delayMs = 100
vibrator.vibrate(
    VibrationEffect.startComposition().addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f
    ).addPrimitive(
    VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs
    ).compose()
)

جاوا

int delayMs = 100;
vibrator.vibrate(
    VibrationEffect.startComposition()
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.8f)
        .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN, 0.6f)
        .addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_THUD, 1.0f, delayMs)
        .compose());

بررسی کنید که کدام نسخه های اولیه پشتیبانی می شوند

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

کاتلین

val primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition()
        .addPrimitive(primitive).compose())
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

جاوا

int primitive = VibrationEffect.Composition.PRIMITIVE_LOW_TICK;

if (vibrator.areAllPrimitivesSupported(primitive)) {
  vibrator.vibrate(VibrationEffect.startComposition()
        .addPrimitive(primitive).compose());
} else {
  // Play a predefined effect or custom pattern as a fallback.
}

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

کاتلین

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

جاوا

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

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

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

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

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

انیمیشن یک دایره که به پایین کشیده می شود.
نمودار شکل موج ارتعاش ورودی.

شکل 1. این شکل موج نشان دهنده شتاب خروجی ارتعاش در یک دستگاه است.

کاتلین

@Composable
fun ResistScreen() {
    // Control variables for the dragging of the indicator.
    var isDragging by remember { mutableStateOf(false) }
    var dragOffset by remember { mutableStateOf(0f) }

    // Only vibrates while the user is dragging
    if (isDragging) {
        LaunchedEffect(Unit) {
        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        while (true) {
            // Calculate the interval inversely proportional to the drag offset.
            val vibrationInterval = calculateVibrationInterval(dragOffset)
            // Calculate the scale directly proportional to the drag offset.
            val vibrationScale = calculateVibrationScale(dragOffset)

            delay(vibrationInterval)
            vibrator.vibrate(
            VibrationEffect.startComposition().addPrimitive(
                VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                vibrationScale
            ).compose()
            )
        }
        }
    }

    Screen() {
        Column(
        Modifier
            .draggable(
            orientation = Orientation.Vertical,
            onDragStarted = {
                isDragging = true
            },
            onDragStopped = {
                isDragging = false
            },
            state = rememberDraggableState { delta ->
                dragOffset += delta
            }
            )
        ) {
        // Build the indicator UI based on how much the user has dragged it.
        ResistIndicator(dragOffset)
        }
    }
}

جاوا

class DragListener implements View.OnTouchListener {
    // Control variables for the dragging of the indicator.
    private int startY;
    private int vibrationInterval;
    private float vibrationScale;

    @Override
    public boolean onTouch(View view, MotionEvent event) {
        switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            startY = event.getRawY();
            vibrationInterval = calculateVibrationInterval(0);
            vibrationScale = calculateVibrationScale(0);
            startVibration();
            break;
        case MotionEvent.ACTION_MOVE:
            float dragOffset = event.getRawY() - startY;
            // Calculate the interval inversely proportional to the drag offset.
            vibrationInterval = calculateVibrationInterval(dragOffset);
            // Calculate the scale directly proportional to the drag offset.
            vibrationScale = calculateVibrationScale(dragOffset);
            // Build the indicator UI based on how much the user has dragged it.
            updateIndicator(dragOffset);
            break;
        case MotionEvent.ACTION_CANCEL:
        case MotionEvent.ACTION_UP:
            // Only vibrates while the user is dragging
            cancelVibration();
            break;
        }
        return true;
    }

    private void startVibration() {
        vibrator.vibrate(
            VibrationEffect.startComposition()
                .addPrimitive(VibrationEffect.Composition.PRIMITIVE_LOW_TICK,
                        vibrationScale)
                .compose());

        // Continuously run the effect for vibration to occur even when the view
        // is not being drawn, when user stops dragging midway through gesture.
        handler.postDelayed(this::startVibration, vibrationInterval);
    }

    private void cancelVibration() {
        handler.removeCallbacksAndMessages(null);
    }
}

گسترش (با افزایش و سقوط)

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

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

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

شکل 2. این شکل موج نشان دهنده شتاب خروجی ارتعاش در یک دستگاه است.

کاتلین

enum class ExpandShapeState {
    Collapsed,
    Expanded
}

@Composable
fun ExpandScreen() {
    // Control variable for the state of the indicator.
    var currentState by remember { mutableStateOf(ExpandShapeState.Collapsed) }

    // Animation between expanded and collapsed states.
    val transitionData = updateTransitionData(currentState)

    Screen() {
        Column(
        Modifier
            .clickable(
            {
                if (currentState == ExpandShapeState.Collapsed) {
                currentState = ExpandShapeState.Expanded
                vibrator.vibrate(
                    VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE,
                    0.3f
                    ).addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
                    0.3f
                    ).compose()
                )
                } else {
                currentState = ExpandShapeState.Collapsed
                vibrator.vibrate(
                    VibrationEffect.startComposition().addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE
                    ).compose()
                )
            }
            )
        ) {
        // Build the indicator UI based on the current state.
        ExpandIndicator(transitionData)
        }
    }
}

جاوا

class ClickListener implements View.OnClickListener {
    private final Animation expandAnimation;
    private final Animation collapseAnimation;
    private boolean isExpanded;

    ClickListener(Context context) {
        expandAnimation = AnimationUtils.loadAnimation(context, R.anim.expand);
        expandAnimation.setAnimationListener(new Animation.AnimationListener() {

        @Override
        public void onAnimationStart(Animation animation) {
            vibrator.vibrate(
            VibrationEffect.startComposition()
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_SLOW_RISE, 0.3f)
                .addPrimitive(
                    VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.3f)
                .compose());
        }
        });

        collapseAnimation = AnimationUtils
                .loadAnimation(context, R.anim.collapse);
        collapseAnimation.setAnimationListener(new Animation.AnimationListener() {

            @Override
            public void onAnimationStart(Animation animation) {
                vibrator.vibrate(
                VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SLOW_RISE)
                    .compose());
            }
        });
    }

    @Override
    public void onClick(View view) {
        view.startAnimation(isExpanded ? collapseAnimation : expandAnimation);
        isExpanded = !isExpanded;
    }
}

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

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

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

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

انیمیشن پرش شکل الاستیک
نمودار شکل موج ارتعاش ورودی

شکل 3. این شکل موج نشان دهنده شتاب خروجی ارتعاش در یک دستگاه است.

کاتلین

@Composable
fun WobbleScreen() {
    // Control variables for the dragging and animating state of the elastic.
    var dragDistance by remember { mutableStateOf(0f) }
    var isWobbling by remember { mutableStateOf(false) }

    // Use drag distance to create an animated float value behaving like a spring.
    val dragDistanceAnimated by animateFloatAsState(
        targetValue = if (dragDistance > 0f) dragDistance else 0f,
        animationSpec = spring(
            dampingRatio = Spring.DampingRatioHighBouncy,
            stiffness = Spring.StiffnessMedium
        ),
    )

    if (isWobbling) {
        LaunchedEffect(Unit) {
            while (true) {
                val displacement = dragDistanceAnimated / MAX_DRAG_DISTANCE
                // Use some sort of minimum displacement so the final few frames
                // of animation don't generate a vibration.
                if (displacement > SPIN_MIN_DISPLACEMENT) {
                    vibrator.vibrate(
                        VibrationEffect.startComposition().addPrimitive(
                            VibrationEffect.Composition.PRIMITIVE_SPIN,
                            nextSpinScale(displacement)
                        ).addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_SPIN,
                        nextSpinScale(displacement)
                        ).compose()
                    )
                }
                // Delay the next check for a sufficient duration until the
                // current composition finishes. Note that you can use
                // Vibrator.getPrimitiveDurations API to calculcate the delay.
                delay(VIBRATION_DURATION)
            }
        }
    }

    Box(
        Modifier
            .fillMaxSize()
            .draggable(
                onDragStopped = {
                    isWobbling = true
                    dragDistance = 0f
                },
                orientation = Orientation.Vertical,
                state = rememberDraggableState { delta ->
                    isWobbling = false
                    dragDistance += delta
                }
            )
    ) {
        // Draw the wobbling shape using the animated spring-like value.
        WobbleShape(dragDistanceAnimated)
    }
}

// Calculate a random scale for each spin to vary the full effect.
fun nextSpinScale(displacement: Float): Float {
    // Generate a random offset in the range [-0.1, +0.1] to be added to the
    // vibration scale so the spin effects have slightly different values.
    val randomOffset: Float = Random.Default.nextFloat() * 0.2f - 0.1f
    return (displacement + randomOffset).absoluteValue.coerceIn(0f, 1f)
}

جاوا

class AnimationListener implements DynamicAnimation.OnAnimationUpdateListener {
    private final Random vibrationRandom = new Random(seed);
    private final long lastVibrationUptime;

    @Override
    public void onAnimationUpdate(
        DynamicAnimation animation, float value, float velocity) {
        // Delay the next check for a sufficient duration until the current
        // composition finishes. Note that you can use
        // Vibrator.getPrimitiveDurations API to calculcate the delay.
        if (SystemClock.uptimeMillis() - lastVibrationUptime < VIBRATION_DURATION) {
            return;
        }

        float displacement = calculateRelativeDisplacement(value);

        // Use some sort of minimum displacement so the final few frames
        // of animation don't generate a vibration.
        if (displacement < SPIN_MIN_DISPLACEMENT) {
            return;
        }

        lastVibrationUptime = SystemClock.uptimeMillis();
        vibrator.vibrate(
        VibrationEffect.startComposition()
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
            nextSpinScale(displacement))
            .addPrimitive(VibrationEffect.Composition.PRIMITIVE_SPIN,
            nextSpinScale(displacement))
            .compose());
    }

    // Calculate a random scale for each spin to vary the full effect.
    float nextSpinScale(float displacement) {
        // Generate a random offset in the range [-0.1,+0.1] to be added to
        // the vibration scale so the spin effects have slightly different
        // values.
        float randomOffset = vibrationRandom.nextFloat() * 0.2f - 0.1f
        return MathUtils.clamp(displacement + randomOffset, 0f, 1f)
    }
}

پرش (با ضربات ضربان)

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

در اینجا نمونه‌ای از انیمیشن رها کردن توپ با افکت ضربت با هر بار پرش توپ از پایین صفحه نمایش داده می‌شود:

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

شکل 4. این شکل موج نشان دهنده شتاب خروجی ارتعاش در یک دستگاه است.

کاتلین

enum class BallPosition {
    Start,
    End
}

@Composable
fun BounceScreen() {
    // Control variable for the state of the ball.
    var ballPosition by remember { mutableStateOf(BallPosition.Start) }
    var bounceCount by remember { mutableStateOf(0) }

    // Animation for the bouncing ball.
    var transitionData = updateTransitionData(ballPosition)
    val collisionData = updateCollisionData(transitionData)

    // Ball is about to contact floor, only vibrating once per collision.
    var hasVibratedForBallContact by remember { mutableStateOf(false) }
    if (collisionData.collisionWithFloor) {
        if (!hasVibratedForBallContact) {
        val vibrationScale = 0.7.pow(bounceCount++).toFloat()
        vibrator.vibrate(
            VibrationEffect.startComposition().addPrimitive(
            VibrationEffect.Composition.PRIMITIVE_THUD,
            vibrationScale
            ).compose()
        )
        hasVibratedForBallContact = true
        }
    } else {
        // Reset for next contact with floor.
        hasVibratedForBallContact = false
    }

    Screen() {
        Box(
        Modifier
            .fillMaxSize()
            .clickable {
            if (transitionData.isAtStart) {
                ballPosition = BallPosition.End
            } else {
                ballPosition = BallPosition.Start
                bounceCount = 0
            }
            },
        ) {
        // Build the ball UI based on the current state.
        BouncingBall(transitionData)
        }
    }
}

جاوا

class ClickListener implements View.OnClickListener {
    @Override
    public void onClick(View view) {
        view.animate()
        .translationY(targetY)
        .setDuration(3000)
        .setInterpolator(new BounceInterpolator())
        .setUpdateListener(new AnimatorUpdateListener() {

            boolean hasVibratedForBallContact = false;
            int bounceCount = 0;

            @Override
            public void onAnimationUpdate(ValueAnimator animator) {
            boolean valueBeyondThreshold = (float) animator.getAnimatedValue() > 0.98;
            if (valueBeyondThreshold) {
                if (!hasVibratedForBallContact) {
                float vibrationScale = (float) Math.pow(0.7, bounceCount++);
                vibrator.vibrate(
                    VibrationEffect.startComposition()
                    .addPrimitive(
                        VibrationEffect.Composition.PRIMITIVE_THUD,
                        vibrationScale)
                    .compose());
                hasVibratedForBallContact = true;
                }
            } else {
                // Reset for next contact with floor.
                hasVibratedForBallContact = false;
            }
            }
        });
    }
}

شکل موج ارتعاشی با پاکت

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

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

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

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

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

برای ایجاد جلوه های پاکت اولیه، از 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 تعریف کنید. هنگام استفاده از این شی، می توانید از طریق VibratorFrequencyProfile به نگاشت شتاب فرکانس به خروجی (FOAM) دسترسی داشته باشید.

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

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

کد زیر نمونه ای از شکل موج را نشان می دهد که اثر لرزشی 400 میلی ثانیه را تعریف می کند. با یک رمپ دامنه 50 میلی‌ثانیه، از خاموش به کامل، در 60 هرتز ثابت شروع می‌شود. سپس، فرکانس تا 120 هرتز در 100 میلی‌ثانیه بعدی افزایش می‌یابد و تا 200 میلی‌ثانیه در آن سطح باقی می‌ماند. در نهایت، دامنه به پایین می آید \( 0 \)و فرکانس در 50 میلی ثانیه گذشته به 60 هرتز برمی گردد:

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 برای شبیه سازی فعل و انفعالات پرش فیزیکی استفاده می کند. API پاکت اصلی کنترل بسیار دقیق‌تری را ارائه می‌دهد و به شما امکان می‌دهد شدت لرزش و وضوح را دقیقاً تنظیم کنید. این منجر به بازخورد لمسی می شود که رویدادهای متحرک را با دقت بیشتری دنبال می کند.

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

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

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

پرتاب موشک

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

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

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

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

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

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

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

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

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

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

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

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

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

  vibrator.vibrate(
    VibrationEffect.WaveformEnvelopeBuilder()
      // Quickly reach the 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()
  )
}