إنشاء عرض مخصّص تفاعلي

تجربة طريقة ComposeAllowed
Jetpack Compose هي مجموعة أدوات واجهة المستخدم التي ننصح بها لنظام التشغيل Android. تعرَّف على كيفية استخدام التنسيقات في Compose.

يعد رسم واجهة مستخدم جزءًا واحدًا فقط من إنشاء طريقة عرض مخصصة. تحتاج أيضًا إلى جعل طريقة العرض تستجيب لإدخال المستخدم بطريقة تشبه إلى حدٍ كبير الإجراء الواقعي الذي تحاكيه.

اجعل العناصر في تطبيقك تعمل كما تفعل العناصر الحقيقية. على سبيل المثال، تجنَّب ظهور الصور في التطبيق في مكان آخر، لأنّ هذه العناصر في العالم الحقيقي لا تفعل ذلك. بدلاً من ذلك، يمكنك نقل صورك من مكان إلى آخر.

يشعر المستخدمون بسلوكيات دقيقة أو ذكاء في الواجهة ويتفاعلون بشكل أفضل مع التفاصيل الدقيقة التي تحاكي العالم الحقيقي. على سبيل المثال، عندما يوجه المستخدمون كائن واجهة مستخدم، منحهم إحساسًا بالقصور الذاتي في البداية مما يؤخر الحركة. وفي نهاية الحركة، امنحهم إحساسًا بالزخم الذي يحمل الكائن إلى ما هو أبعد من الزخم.

توضح هذه الصفحة كيفية استخدام ميزات إطار عمل Android لإضافة هذه السلوكيات الواقعية إلى طريقة العرض المخصّصة.

يمكنك العثور على معلومات إضافية ذات صلة في نظرة عامة على أحداث الإدخال ونظرة عامة على الصور المتحركة لالموقع.

التعامل مع إيماءات الإدخال

على غرار العديد من أطر عمل واجهة المستخدم الأخرى، يتيح Android نموذج إدخال الأحداث. تتحول إجراءات المستخدم إلى أحداث تؤدي إلى عمليات معاودة الاتصال، ويمكنك إلغاء تلك العمليات لتخصيص كيفية استجابة تطبيقك للمستخدم. حدث الإدخال الأكثر شيوعًا في نظام Android هو اللمس، وهو ما يؤدي إلى تشغيل onTouchEvent(android.view.MotionEvent). يمكنك تجاهُل هذه الطريقة للتعامل مع الحدث، كما يلي:

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return super.onTouchEvent(event)
}

Java

@Override
   public boolean onTouchEvent(MotionEvent event) {
    return super.onTouchEvent(event);
   }

أحداث اللمس وحدها ليست مفيدة بشكل خاص. تحدّد واجهات المستخدم التي تعمل باللمس الحديث التفاعلات من حيث الإيماءات، مثل النقر والسحب والدفع والتمرير والتكبير/التصغير. لتحويل أحداث اللمس الأوّلية إلى إيماءات، يوفّر Android GestureDetector.

يمكنك إنشاء GestureDetector من خلال إدخال مثيل لفئة تنفّذ GestureDetector.OnGestureListener. إذا كنت تريد معالجة بعض الإيماءات فقط، يمكنك توسيع نطاق GestureDetector.SimpleOnGestureListener بدلاً من تنفيذ واجهة GestureDetector.OnGestureListener. على سبيل المثال، ينشئ هذا الرمز فئة تمتد إلى GestureDetector.SimpleOnGestureListener وتلغي التصنيف onDown(MotionEvent).

Kotlin

private val myListener =  object : GestureDetector.SimpleOnGestureListener() {
    override fun onDown(e: MotionEvent): Boolean {
        return true
    }
}

private val detector: GestureDetector = GestureDetector(context, myListener)

Java

class MyListener extends GestureDetector.SimpleOnGestureListener {
   @Override
   public boolean onDown(MotionEvent e) {
       return true;
   }
}
detector = new GestureDetector(getContext(), new MyListener());

وسواء أكنت تستخدم GestureDetector.SimpleOnGestureListener أم لا، نفِّذ دائمًا طريقة onDown() تعرض true. ويُعدّ هذا الإجراء ضروريًا لأنّ كل الإيماءات تبدأ برسالة onDown(). في حال عرض false من onDown()، كما يفعل GestureDetector.SimpleOnGestureListener، يفترض النظام أنك تريد تجاهل بقية الإيماءة، ولن يتم استدعاء طُرق GestureDetector.OnGestureListener الأخرى. يمكنك عرض false من onDown() فقط إذا كنت تريد تجاهل إيماءة كاملة.

بعد تنفيذ GestureDetector.OnGestureListener وإنشاء مثيل من GestureDetector، يمكنك استخدام GestureDetector لتفسير أحداث اللمس التي تتلقّاها في onTouchEvent().

Kotlin

override fun onTouchEvent(event: MotionEvent): Boolean {
    return detector.onTouchEvent(event).let { result ->
        if (!result) {
            if (event.action == MotionEvent.ACTION_UP) {
                stopScrolling()
                true
            } else false
        } else true
    }
}

Java

@Override
public boolean onTouchEvent(MotionEvent event) {
   boolean result = detector.onTouchEvent(event);
   if (!result) {
       if (event.getAction() == MotionEvent.ACTION_UP) {
           stopScrolling();
           result = true;
       }
   }
   return result;
}

عند تجاوز onTouchEvent() حدث لمس لا يمكن التعرّف عليه كجزء من إيماءة، سيتم عرض false. يمكنك بعد ذلك تشغيل الرمز المخصص لاكتشاف الإيماءات.

إنشاء حركة معقولة جسديًا

الإيماءات هي طريقة فعالة للتحكم في الأجهزة ذات الشاشات التي تعمل باللمس، إلا أنها قد تكون عكسية ويصعب تذكرها ما لم ينتج عنها نتائج منطقية.

على سبيل المثال، لنفترض أنك تريد تنفيذ إيماءة الانتقال الأفقي التي تحدد العنصر المرسوم في طريقة العرض والذي يدور حول محوره الرأسي. تكون هذه الإيماءة منطقية إذا استجابت واجهة المستخدم بالتحرك بسرعة في اتجاه القفز، ثم الإبطاء، كما لو كان المستخدم يدفع عجلة العجلة وجعلها تدور.

تقدّم المستندات المتعلقة بكيفية تحريك إيماءة تمرير شرحًا تفصيليًا حول طريقة تنفيذ سلوك مجملك الخاص. لكن محاكاة إحساس الحدافة ليس أمرًا تافهًا. يلزم الكثير من الفيزياء والرياضيات لجعل نموذج الحدافة يعمل بشكل صحيح. لحسن الحظ، يوفر Android فئات مساعدة لمحاكاة هذا السلوك وغيره. الفئة Scroller هي الأساس للتعامل مع إيماءات التنقل السريع بنمط حدافة.

لبدء الانتقال، يمكنك استدعاء fling() مع سرعة البدء وقيمتَي x وy للانتقال. بالنسبة إلى قيمة السرعة، يمكنك استخدام القيمة المحسوبة من خلال GestureDetector.

Kotlin

fun onFling(e1: MotionEvent, e2: MotionEvent, velocityX: Float, velocityY: Float): Boolean {
    scroller.fling(
            currentX,
            currentY,
            (velocityX / SCALE).toInt(),
            (velocityY / SCALE).toInt(),
            minX,
            minY,
            maxX,
            maxY
    )
    postInvalidate()
    return true
}

Java

@Override
public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
   scroller.fling(currentX, currentY, velocityX / SCALE, velocityY / SCALE, minX, minY, maxX, maxY);
   postInvalidate();
    return true;
}

تساعد استدعاء الدالة fling() في إعداد النموذج الفيزيائي للإيماءة. بعد ذلك، يمكنك تعديل Scroller من خلال استدعاء Scroller.computeScrollOffset() على فترات زمنية منتظمة. يعدّل computeScrollOffset() الحالة الداخلية لكائن Scroller من خلال قراءة الوقت الحالي واستخدام النموذج الفيزيائي لاحتساب الموضعين x وy في ذلك الوقت. استدعِ getCurrX() و getCurrY() لاسترداد هذه القيم.

وتمرر معظم طرق العرض الموضعين x وy للكائن Scroller مباشرةً إلى scrollTo(). يختلف هذا المثال قليلاً: فهو يستخدم موضع التمرير x الحالي لضبط زاوية الدوران للعرض.

Kotlin

scroller.apply {
    if (!isFinished) {
        computeScrollOffset()
        setItemRotation(currX)
    }
}

Java

if (!scroller.isFinished()) {
    scroller.computeScrollOffset();
    setItemRotation(scroller.getCurrX());
}

تحسب الفئة Scroller مواضع التمرير نيابةً عنك، ولكنها لا تطبّق هذه المواضع تلقائيًا على طريقة العرض الخاصة بك. قم بتطبيق إحداثيات جديدة غالبًا بما يكفي لجعل الرسوم المتحركة التمريرية تبدو سلسة. هناك طريقتان للقيام بذلك:

  • يمكنك فرض إعادة الرسم من خلال طلب الرقم postInvalidate() بعد طلب الرقم fling(). يتطلّب هذا الأسلوب احتساب إزاحات التمرير في onDraw() وطلب postInvalidate() في كل مرة تتغيّر فيها إزاحة التمرير.
  • يمكنك إعداد ValueAnimator لإضافة تأثيرات متحركة طوال مدة الانتقال، وإضافة أداة استماع لمعالجة تعديلات الصور المتحركة من خلال استدعاء addUpdateListener(). تتيح لك هذه التقنية تحريك خصائص View.

تسهيل الانتقالات

يتوقع المستخدمون أن تنتقل واجهة المستخدم الحديثة بسلاسة بين الحالات: عناصر واجهة المستخدم التي تتلاشى داخل وخارج بدلاً من ظهورها وتختفي، وتبدأ الحركات وتنتهي بسلاسة بدلاً من البدء والتوقف فجأة. تعمل إطار عمل الصور المتحركة لخصائص Android على تسهيل عمليات الانتقال السلسة.

لاستخدام نظام الصور المتحركة، يجب عدم تغيير الخاصية مباشرةً كلما غيّرت إحدى الخصائص ما يؤثر في مظهر الملف الشخصي. بدلاً من ذلك، استخدِم ValueAnimator لإجراء التغيير. في المثال التالي، يؤدي تعديل العنصر الفرعي المحدَّد في طريقة العرض إلى تدوير طريقة العرض المعروضة بأكملها بحيث يكون مؤشر التحديد في الوسط. تغيّر الدالة ValueAnimator التدوير على مدار عدة مئات الملي ثانية، بدلاً من ضبط قيمة التدوير الجديدة على الفور.

Kotlin

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0).apply {
    setIntValues(targetAngle)
    duration = AUTOCENTER_ANIM_DURATION
    start()
}

Java

autoCenterAnimator = ObjectAnimator.ofInt(this, "Rotation", 0);
autoCenterAnimator.setIntValues(targetAngle);
autoCenterAnimator.setDuration(AUTOCENTER_ANIM_DURATION);
autoCenterAnimator.start();

إذا كانت القيمة التي تريد تغييرها هي إحدى سمات View الأساسية، سيكون من الأسهل إنشاء الصور المتحركة، لأنّ الملفات الشخصية تتضمّن ViewPropertyAnimator تم تحسينه لعرض صورة متحركة متزامنة لسمات متعددة، كما في المثال التالي:

Kotlin

animate()
    .rotation(targetAngle)
    .duration = ANIM_DURATION
    .start()

Java

animate().rotation(targetAngle).setDuration(ANIM_DURATION).start();