مرحبا يا هبر! اسمي ايليا أوسينتسيف ، أنا مطور أندرويد في Apiqa. ستجد تحت القطة مثالًا على استخدام ViewDragHelper لإنشاء مكون واجهة مستخدم يشبه SwipeDismissBehavior ، ولكن يعمل بشكل عمودي.
مع ظهور تصميم المواد ، أصبحت التطبيقات عناصر أكثر تفاعلية تستجيب لإجراءات المستخدم. فهي لا توفر مساحة فحسب ، بل توفر أيضًا تفاعلات متناهية الصغر. في العديد من مشاريعنا ، قررنا استخدام لافتات متحركة رأسياً على آليات السحب السريع للإقالة. لإضفاء الحيوية على الواجهة ، يجب أن تأخذ الشعارات في الاعتبار سرعة المؤشر وتغيير الشفافية اعتمادًا على اتجاه الإزاحة.
تقييم المهمة
في تطبيق الحساب الشخصي الخاص بنا ، تعمل اللافتة كطريقة سريعة لترك نداء إلى خدمة البحث عن المستأجر لمنزلك. في تطبيق "Appliance Technique" ، يتيح لك الشعار حفظ سياق عمل المستخدم بالمهمة عند التبديل من بطاقة معلومات إلى تعليقات. في الحالة الأولى ، نؤكد على خيار خدمة PIK-Rent ونجعل العميل يشعر بأنه في منزله في التطبيق. في حالة أخرى ، نقوم بتنفيذ التمرير السريع في الموجه بحيث لا يتداخل مع خط الرسالة بين المرسل والمنفذين.
للبدء ، جمعت عرضًا تجريبيًا بسيطًا يستند إلى SwipeDismissBehavior لمعرفة كيفية عمله ولتقدير حجم التغييرات. محاولة تحديده في ترميز xml يلقي استثناءً عند تنفيذه:
E/AndroidRuntime: FATAL EXCEPTION: main Process: io.apiqa.android.example, PID: 1024 android.view.InflateException: Binary XML file line #115: Could not inflate Behavior subclass com.google.android.material.behavior.SwipeDismissBehavior
انتهك المطورون عقد السلوك ونسوا تجاوز مُنشئ الفصل الدراسي من السياق و AttributeSet. يمكنك الآن استخدام هذا السلوك فقط عن طريق إنشاء مثيل له في التعليمات البرمجية الخاصة بك ، ولكن حتى ذلك الحين ، لا يفي سلوك العرض بشكل أساسي بمتطلباتنا ، حتى دون مراعاة الاتجاه الأفقي.

كانت هناك رسائل في سجلات التطبيق التجريبي تفيد بأن بعض أحداث اللمس لم تقع في المعالج.
E / ViewDragHelper: تجاهل pointerId = -1 لأنه لم يتم تلقي ACTION_DOWN لهذا المؤشر قبل ACTION_MOVE. حدث ذلك على الأرجح لأن ViewDragHelper لم يتلق كل الأحداث في ساحة الحدث.
كبديل ، كان هناك حل قائم على OnTouchListener
، فهو يحرمنا من فرصة استخدام OnClickListener ، مما يعني أنه سيتعين علينا وصف قانون حركة اللافتة في النشاط. لا نريد تغيير معلمات الحركة (على سبيل المثال ، الحساسية) أثناء تحركنا ، ويبدو أن استخدام OnTouchListener هنا أمر ضروري. بالإضافة إلى ذلك ، في كل من مشاريعنا وضعت لافتات في CoordinatorLayout.
إذا كنت لا تأخذ في الحسبان مجموعة أدوات تحديد المعلمات الاختيارية وأداة ضبطها ، فإن SwipeDismissBehavior نفسه قصير جدًا ، ويستخدم ViewDragHelper
. لقد وجدت العديد من المنشورات عنه على الشبكة وقررت أن أكتب تنفيذي الخاص للمكون المطلوب.
نحن ننسق مع ViewDragHelper
يعد ViewDragHelper فئة من الأدوات المساعدة لتسهيل دعم السحب والإفلات على مستوى العرض. يتتبع موضع الأدوات المصغّرة ويحتوي على العديد من الوظائف المفيدة لتنشيطها على محور أو محورين داخل ViewGroup الأصل. للعمل ، يحتاج إلى معالج يقوم بتنفيذ ViewDragHelper.Callback . لدى المعالج طريقة إلزامية واحدة ، ولكي يبدأ اللافتة في التحرك ، يكفي إعادة تعريف زوجين آخرين. بشكل عام ، هذه المساعدة سهلة الاستخدام ، وهي متوفرة في أي مشروع ، لأنها تأتي مع appcompat. لإنشاء مساعد ، يلزم الرجوع إلى الأصل CoordinatorLayout ، لذلك سننظم التهيئة البطيئة. في onInterceptTouchEvent
و onTouchEvent
يجب علينا استدعاء أساليب المساعد المطابق ، وسوف يكون بقية المنطق داخل المعالج.
class VerticalSwipeBehavior<V: View>: CoordinatorLayout.Behavior<V> { companion object { @Suppress("UNCHECKED_CAST") fun <V: View> from(v: V): VerticalSwipeBehavior<V> { val lp = v.layoutParams require(lp is CoordinatorLayout.LayoutParams) val behavior = lp.behavior requireNotNull(behavior) require(behavior is VerticalSwipeBehavior) return behavior as VerticalSwipeBehavior<V> } } @Suppress("unused") constructor() : super() @Suppress("unused") constructor(context: Context, attrs: AttributeSet) : super(context, attrs) private var dragHelper: ViewDragHelper? = null private var interceptingEvents = false override fun onInterceptTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean { var isIntercept = interceptingEvents when (ev.actionMasked) { MotionEvent.ACTION_DOWN -> { isIntercept = parent.isPointInChildBounds(child, ev.x.toInt(), ev.y.toInt()) interceptingEvents = isIntercept } MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { interceptingEvents = false } } return if (isIntercept) { helper(parent).shouldInterceptTouchEvent(ev) } else false } override fun onTouchEvent(parent: CoordinatorLayout, child: V, ev: MotionEvent): Boolean { val helper = helper(parent) val isViewUnder = helper.isViewUnder(child, ev.x.toInt(), ev.y.toInt()) if (helper.capturedView == child || isViewUnder ) { helper.processTouchEvent(ev) return true } else { return false } } private fun helper(parent: ViewGroup): ViewDragHelper { var h = dragHelper if (h == null) { h = ViewDragHelper.create(parent, callback) dragHelper = h return h } return h } }
في tryCaptureView
معالج tryCaptureView
المطلوبة tryCaptureView
يجب أن نقرر نقل عرض معين. حتى لا يفوت المساعد الأحداث الضرورية ، نعترف بالمؤشر الذي تم استلامه مسبقًا. للحصول على الحل الأكثر مرونة ، يتم تقديم ثلاث واجهات إضافية ، يمكنك من خلالها التحكم في تصميم الشعارات بالتفصيل:
- يعكس
SideEffect
التقدم SideEffect
في خصائص العرض VerticalClamp
تصميم VerticalClamp
للحد من حركة العرض عموديًا- يسمى
PostAction
بعد أن يتوقف المستخدم عن التمرير السريع ، وهنا يمكننا متابعة حركة المشاهدة.
في كل منها ، يتم onViewCaptured(View)
طريقة onViewCaptured(View)
، حيث يمكن onViewCaptured(View)
العميلة استخراج القيم الأولية لخصائص العرض. ترتيب المكالمات لهذه الطريقة غير مضمون.
var sideEffect: SideEffect = AlphaElevationSideEffect() var clamp: VerticalClamp = FractionConstraintWithTopMargin(1f, 1f) var settle: PostAction = OriginSettleAction() private val callback = object: ViewDragHelper.Callback() { private val INVALID_POINTER_ID = -1 private var currentPointer = INVALID_POINTER_ID private var originTop: Int = 0 override fun tryCaptureView(child: View, pointerId: Int): Boolean { return currentPointer == INVALID_POINTER_ID || pointerId == currentPointer } override fun onViewCaptured(child: View, activePointerId: Int) { originTop = child.top currentPointer = activePointerId sideEffect.onViewCaptured(child) settle.onViewCaptured(child) clamp.onViewCaptured(child.top) } override fun onViewReleased(child: View, xvel: Float, yvel: Float) {
على الرغم من أن وجهة نظرنا لا تتحرك عموديًا ، إلا أننا يجب أن نتذكر تطبيق clampViewPositionHorizontal
في المعالج لتجنب الأخطاء المرئية. إرجاع تطبيق بسيط الإحداثي الأيسر ، مما يعني أن القطعة لا تتحرك أفقياً.
في حالتنا ، يتم تفويض الدعوة إلى معالج clampViewPositionVertical
إلى واجهة VerticalClamp . يجب أن تُرجع طريقة constraint
إحداثي ارتفاع محدد بواسطة الحد الأقصى و / أو الحد الأدنى لموضع العرض. عندما يتم الوصول إليه ، سيقيد ViewDragHelper الحركة. إن downCast
upCast(distance, top, height, dy)
و downCast
لها نفس التوقيع وإرجاع جزء صغير من المسافة المقطوعة ، مع مراعاة الموضع الأولي للعرض. في onViewPositionChanged
معالج onViewPositionChanged
نحصل على تقدم التحرك SideEffect#apply(View, Float)
إلى SideEffect#apply(View, Float)
، حيث يمكنك تغيير الشفافية أو خصائص العرض الأخرى حسب تقدم الإيماءة. إذا كان الموضع الحالي للعرض أعلى من الأولي ، فسيتم إرسال التقدم بعلامة ناقص.
override fun clampViewPositionVertical(child: View, top: Int, dy: Int): Int { return clamp.constraint(child.height, top, dy) } override fun onViewPositionChanged(child: View, left: Int, top: Int, dx: Int, dy: Int) { val factor = if (top < originTop) { val diff = originTop - top -clamp.bottomCast(diff, top, child.height, dy) } else { val diff = top - originTop clamp.topCast(diff, top, child.height, dy) } sideEffect.apply(child, factor) }
بشكل افتراضي ، FractionClamp
استخدام FractionClamp
، مما يحد من حركة العرض بارتفاع واحد لأعلى ولأسفل (يتم تعيين المعاملات في المُنشئ) ، AlphaElevationSideEffect
شفافية وارتفاع الشعار. للنظر في المهمة المنجزة ، يجب عليك إضافة القدرة على تحريك الشعار بعد أن أصدره المستخدم.
عندما يقوم المستخدم بإصدار العرض ، سيتذكر المساعد سرعة المؤشر ويدعو onViewReleased
على المعالج. في ذلك ، يمكننا أن نبدأ الرسوم المتحركة للحركة باستخدام settleCapturedViewAt
أو smoothSlideViewTo
. وفقًا للعقد ، بعد استدعاء ناجح لأي منهم ، يجب استدعاء ContinueSettling على كل إطار تالي حتى يستمر العرض في التحرك. في هذه الحالة ، لا يمكن استدعاء settleCapturedViewAt إلا من طريقة onViewReleased عندما يتم تعيين العلامة الداخلية لمساعد mReleaseInProgress إلى true. الفرق الآخر هو أن smoothSlideViewTo لا يأخذ في الاعتبار سرعة المؤشر.
override fun onViewReleased(child: View, xvel: Float, yvel: Float) { val diff = child.top - originTop if (abs(yvel) > 0) { val settled = dragHelper?.let { if (diff > 0) { settle.releasedBelow(it, diff, child) } else { settle.releasedAbove(it, diff, child) } } ?: false if (settled) { listener?.onPreSettled(diff) child.postOnAnimation(RecursiveSettle(child, diff)) } } currentPointer = INVALID_POINTER_ID }
يتم تغليف هذا المنطق في واجهة PostAction
. يمكن أن يتم تطبيق أساليب releasedBelow
releasedAbove
والإصدار السابق بحيث أنه عند إزاحة الشعار يستمر في التحرك بنفس السرعة ، مع ترك الشاشة ، وعند الإزاحة لأسفل ، فإنه يعود إلى موقعه الأصلي. إذا تم إرجاع أي من الطرق إلى حقيقة ، فقد تم تشغيل الحركة وإضافة "RecursiveSettle" إلى قائمة انتظار حدث العرض ، والتي ستبقى فيها حتى تنتهي الرسوم المتحركة. بشكل افتراضي ، OriginSettleAction
استخدام OriginSettleAction
عندما يعود العرض في أي إزاحة إلى نقطة البداية. خيار آخر - SettleOnTopAction
عند تحريك العرض لأسفل SettleOnTopAction
إلى نقطة البداية ، وعندما يتحرك أعلى ، فإنه يأخذك خارج الشاشة.
class SettleOnTopAction: PostAction { private var originTop: Int = -1 override fun onViewCaptured(child: View) { originTop = child.top } override fun releasedAbove(helper: ViewDragHelper, child: View): Boolean { return helper.settleCapturedViewAt(child.left, originTop) } override fun releasedBelow(helper: ViewDragHelper, child: View): Boolean { return helper.settleCapturedViewAt(child.left, -child.height) } }
إذا لزم الأمر ، يمكنك الاشتراك في الأحداث من خلال تطبيق واجهة VerticalSwipeBehavior.SwipeListener
. لديه طريقتين متماثلتين ، واحدة تسمى قبل بداية الحركة المتحركة ، والآخر بعد انتهائها. توضح الوسيطة الاتجاه والمسافة التي أصدر بها المستخدم الشعار. النتيجة الناتجة تلبي متطلباتنا.

للحصول على نتائجها ، يكفي تعريف الخصائص كما يلي:
val drag = findViewById<View>(R.id.drag) VerticalSwipeBehavior.from(drag).apply { settle = SettleOnTopAction() sideEffect = NegativeFactorFilterSideEffect(AlphaElevationSideEffect()) clamp = BelowFractionalClamp() }
بالمناسبة ، يوفر المساعد أيضًا ميزات التحكم في الحركة الأخرى. على سبيل المثال ، باستخدام طريقة setMinVelocity(Float)
، يمكنك تحديد الحد الأدنى للسرعة setMinVelocity(Float)
المشاهدة. كما يدعم المساعد التعرف على الضربات الشديدة من حدود الشاشة ، لذلك تحتاج إلى تحديدها في طريقة setEdgeTrackingEnabled(Int)
. يجب أن نتذكر أن مثيل واحد من ViewDragHelper يمكنه التحكم في حركة عرض واحد فقط ويأخذ في الاعتبار مؤشر واحد فقط.
استخلاص النتائج
تجربتي هي أن ViewDragHelper يجعل من السهل إنشاء لوحات السحب والإفلات أو نقلها في تطبيق ما. المساعد سهل الاستخدام في ViewGroup أو السلوك الذي تم تجاوزه. لديها العديد من الطرق المفيدة لعرض موحية والتحكم في حركتهم. تعتبر دراسة التنفيذ الداخلي لمكونات تصميم المواد تجربة جيدة في حياة مطور Android. تحفزني هذه المهام من المصممين على استكشاف طرق جديدة لبناء واجهة تطبيق ومشاركة المعرفة مع الزملاء والمجتمع.
يمكنك استخدام المكتبة الناتجة في مشاريعك. لتمكينه ، حدد التبعية في ملف build.gradle
dependencies { implementation 'io.apiqa.android:verticalswipebehavior:1.0.0' }
للحصول على عرض مناسب داخل CoordinatorLayout ، حدد خاصية app:layout_behavior="io.apiqa.android.verticalswipe.VerticalSwipeBehavior"
في الترميز. يمكنك وضع لافتة داخل الأصل باستخدام المسافات البادئة. من خلال تنسيق تطبيقات SideEffect و VerticalClamp و PostAction ، يمكنك تحقيق سلوك الشعارات الذي تحتاجه. تتوفر إصدارات العمل الخاصة بكل منها في المستودع .
سنة جديدة سعيدة!