كيفية وصف شريط أدوات مطوي بشكل تعريفي



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

الشروط


  • شريط الأدوات - مجموعة من المشاهدات التي نريد عرضها أعلى الشاشة (وليس android.widget.Toolbar).
  • NestedScroll - أي عرض قابل للتمرير يمكن ربطه بـ AppBarLayout (RecyclerView ، NestedScrollView).

لماذا تحتاج لكتابة قرارك


نظرت إلى العديد من المقاربات على "الإنترنت" ، وتم بناؤها تقريبًا على النحو التالي:

  1. يضبط ارتفاع ثابت لـ AppBarLayout.
  2. يتم كتابة CoordinatorLayout.Behavior ، حيث ، مع بعض الحسابات (يتم إضافة ارتفاع العرض المخزن مؤقتًا إلى أسفل عرض آخر ناقص الهامش مضروبًا في التمرير ، المحسوب هنا) يقومون بتغيير نوع من العرض.
  3. تتغير طرق العرض الأخرى في OnOffsetChangedListener من AppBarLayout.

هنا مثال للسلوك مع النهج الموصوف ، 2.5k نجوم على Github.

الانتظار

الحقيقة: ارتدي جهاز OnePlus الخاص بك

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

بالإضافة إلى ذلك ، لا يوضح هذا المثال ما يجب فعله إذا تمت إضافة عناصر إضافية إلى شريط الأدوات والتي تؤثر على ارتفاع شريط الأدوات هذا.

في gif في بداية المقال ، يمكنك أن ترى كيف يتم إخفاء TextView بالنقر على الزر - ويتم سحب NestedScroll إلى أعلى بحيث لا توجد مساحة فارغة).

GIF مرة أخرى

كيف تفعل ذلك؟ الحلول التي تتبادر إلى الذهن أولاً هي كتابة CoordinatorLayout.Behavior لـ NestedScroll (الحفاظ على منطق AppBarLayout.Behavior الأساسي) أو لصق شريط الأدوات في AppBarLayout وتغييره إلى OnOffsetChangedListener. لقد جربت كلا الحلين ، وتبين أن الرمز مرتبط بتفاصيل التنفيذ ، والتي سيكون من الصعب جدًا على شخص آخر فهمها ولا يمكن إعادة استخدامها.

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

كيف تبدو واجهة برمجة التطبيقات؟


لذا ، لإنشاء منسق Layout.Behav تحتاج:

  • ترث BehaviorByRules ؛
  • تجاوز الأساليب التي تُرجع AppBarLayout و CollapsingToolbarLayout وطول التمرير (ارتفاع AppBarLayout).
  • تجاوز طريقة setUpViews - وصف القواعد لكيفية تصرف العرض عندما يتغير شريط التمرير في appBar.

سيبدو TopInfoBehavior لشريط الأدوات من gif في بداية المقالة هكذا (لاحقًا في المقالة سأشرح كيف يعمل):

التخطيط

TopInfoBehavior.kt
class TopInfoBehavior( context: Context?, attrs: AttributeSet? ) : BehaviorByRules(context, attrs) { override fun calcAppbarHeight(child: View): Int = with(child) { return (height + pixels(R.dimen.toolbar_height)).toInt() } override fun View.provideAppbar(): AppBarLayout = ablAppbar override fun View.provideCollapsingToolbar(): CollapsingToolbarLayout = ctlToolbar override fun View.setUpViews(): List<RuledView> = listOf( RuledView( viewGroupTopDetails, BRuleYOffset( min = pixels(R.dimen.zero), max = pixels(R.dimen.toolbar_height) ) ), RuledView( textViewTopDetails, BRuleAlpha(min = 0.6f, max = 1f) .workInRange(from = appearedUntil, to = 1f), BRuleXOffset( min = 0f, max = pixels(R.dimen.big_margin), interpolator = ReverseInterpolator(AccelerateInterpolator()) ), BRuleYOffset( min = pixels(R.dimen.zero), max = pixels(R.dimen.pad), interpolator = ReverseInterpolator(LinearInterpolator()) ), BRuleAppear(0.1f), BRuleScale(min = 0.8f, max = 1f) ), RuledView( textViewPainIsTheArse, BRuleAppear(isAppearedUntil = GONE_VIEW_THRESHOLD) ), RuledView( textViewCollapsedTop, BRuleAppear(0.1f, true) ), RuledView( textViewTop, BRuleAppear(isAppearedUntil = GONE_VIEW_THRESHOLD) ), buildRuleForIcon(ivTop, LinearInterpolator()), buildRuleForIcon(ivTop2, AccelerateInterpolator(0.7f)), buildRuleForIcon(ivTop3, AccelerateInterpolator()) ) private fun View.buildRuleForIcon( view: ImageView, interpolator: Interpolator ) = RuledView( view, BRuleYOffset( min = -(ivTop3.y - tvCollapsedTop.y), max = 0f, interpolator = DecelerateInterpolator(1.5f) ), BRuleXOffset( min = 0f, max = tvCollapsedTop.width.toFloat() + pixels(R.dimen.huge_margin), interpolator = ReverseInterpolator(interpolator) ) ) companion object { const val GONE_VIEW_THRESHOLD = 0.8f } } 


تخطيط Xml (تمت إزالة السمات الواضحة لقابلية القراءة)
 <android.support.design.widget.CoordinatorLayout> <android.support.design.widget.AppBarLayout android:layout_height="wrap_content"> <android.support.design.widget.CollapsingToolbarLayout app:layout_scrollFlags="scroll|exitUntilCollapsed"> <android.support.v7.widget.Toolbar android:layout_height="@dimen/toolbar_height" app:layout_collapseMode="pin"/> </android.support.design.widget.CollapsingToolbarLayout> </android.support.design.widget.AppBarLayout> <!--  --> <RelativeLayout android:translationZ="5dp" app:layout_behavior="TopInfoBehavior"/> <android.support.v4.widget.NestedScrollView app:layout_behavior="@string/appbar_scrolling_view_behavior"> </android.support.v4.widget.NestedScrollView> <android.support.design.widget.FloatingActionButton app:layout_anchor="@id/nesteScroll" app:layout_anchorGravity="right"/> </android.support.design.widget.CoordinatorLayout> 


كيف يعمل


المهمة هي كتابة القواعد:

 interface BehaviorRule { /** * @param view to be changed * @param details view's data when first attached * @param ratio in range [0, 1]; 0 when toolbar is collapsed */ fun manage(ratio: Float, details: InitialViewDetails, view: View) } 

كل شيء واضح هنا - تأتي قيمة تعويم من 0 إلى 1 ، مما يعكس النسبة المئوية للتمرير ActionBar ، ويأتي العرض وحالته الأولية. يبدو BaseBehaviorRule أكثر إثارة للاهتمام - قاعدة ترث منها القواعد الأساسية الأخرى.

 abstract class BaseBehaviorRule : BehaviorRule { abstract val interpolator: Interpolator abstract val min: Float abstract val max: Float final override fun manage( ratio: Float, details: InitialViewDetails, view: View ) { val interpolation = interpolator.getInterpolation(ratio) val offset = normalize( oldValue = interpolation, newMin = min, newMax = max ) perform(offset, details, view) } /** * @param offset normalized with range from [min] to [max] with [interpolator] */ abstract fun perform(offset: Float, details: InitialViewDetails, view: View) } /** * Affine transform value form one range into another */ fun normalize( oldValue: Float, newMin: Float, newMax: Float, oldMin: Float = 0f, oldMax: Float = 1f ): Float = newMin + ((oldValue - oldMin) * (newMax - newMin)) / (oldMax - oldMin) 

بالنسبة للقواعد الأساسية ، يتم تحديد نطاق القيم (min ، max) و interpolator. هذا يكفي لوصف أي سلوك تقريبًا.

افترض أننا نريد تعيين ألفا لعرضنا في نطاق 0.5 إلى 0.9. نريد أيضًا أن تصبح طريقة عرض التمرير شفافة بسرعة أولاً ، ثم سينخفض ​​معدل التغيير.
ستبدو القاعدة كما يلي:

 BRuleAlpha(min = 0.5f, max = 0.9f, interpolator = DecelerateInterpolator()) 

وهنا تنفيذ BRuleAlpha:

BRuleAlpha.kt
 /** * [min], [max] — values in range [0, 1] */ class BRuleAlpha( override val min: Float, override val max: Float, override val interpolator: Interpolator = LinearInterpolator() ) : BaseBehaviorRule() { override fun perform(offset: Float, details: InitialViewDetails, view: View) { view.alpha = offset } } 


وأخيرًا ، كود BehaviorByRules. بالنسبة لأولئك الذين كتبوا سلوكهم ، يجب أن يكون كل شيء واضحًا (باستثناء ما بداخل onMeasureChild ، سأتحدث عن هذا أدناه):

BehaviorByRules.kt
 abstract class BehaviorByRules( context: Context?, attrs: AttributeSet? ) : CoordinatorLayout.Behavior<View>(context, attrs) { private var views: List<RuledView> = emptyList() private var lastChildHeight = -1 private var needToUpdateHeight: Boolean = true override fun layoutDependsOn( parent: CoordinatorLayout, child: View, dependency: View ): Boolean { return dependency is AppBarLayout } override fun onDependentViewChanged( parent: CoordinatorLayout, child: View, dependency: View ): Boolean { if (views.isEmpty()) views = child.setUpViews() val progress = calcProgress(parent) views.forEach { performRules(offsetView = it, percent = progress) } tryToInitHeight(child, dependency, progress) return true } override fun onMeasureChild( parent: CoordinatorLayout, child: View, parentWidthMeasureSpec: Int, widthUsed: Int, parentHeightMeasureSpec: Int, heightUsed: Int ): Boolean { val canUpdateHeight = canUpdateHeight(calcProgress(parent)) if (canUpdateHeight) { parent.post { val newChildHeight = child.height if (newChildHeight != lastChildHeight) { lastChildHeight = newChildHeight setUpAppbarHeight(child, parent) } } } else { needToUpdateHeight = true } return super.onMeasureChild( parent, child, parentWidthMeasureSpec, widthUsed, parentHeightMeasureSpec, heightUsed ) } /** * If you use fitsSystemWindows=true in your coordinator layout, * you will have to include statusBar height in the appbarHeight */ protected abstract fun calcAppbarHeight(child: View): Int protected abstract fun View.setUpViews(): List<RuledView> protected abstract fun View.provideAppbar(): AppBarLayout protected abstract fun View.provideCollapsingToolbar(): CollapsingToolbarLayout /** * You man not want to update height, if height depends on views, that are currently invisible */ protected open fun canUpdateHeight(progress: Float): Boolean = true private fun calcProgress(parent: CoordinatorLayout): Float { val appBar = parent.provideAppbar() val scrollRange = appBar.totalScrollRange.toFloat() val scrollY = Math.abs(appBar.y) val scroll = 1 - scrollY / scrollRange return when { scroll.isNaN() -> 1f else -> scroll } } private fun setUpAppbarHeight(child: View, parent: ViewGroup) { parent.provideCollapsingToolbar().setHeight(calcAppbarHeight(child)) } private fun tryToInitHeight(child: View, dependency: View, scrollPercent: Float) { if (needToUpdateHeight && canUpdateHeight(scrollPercent)) { setUpAppbarHeight(child, dependency as ViewGroup) needToUpdateHeight = false } } private fun performRules(offsetView: RuledView, percent: Float) { val view = offsetView.view val details = offsetView.details offsetView.rules.forEach { rule -> rule.manage(percent, details, view) } } } 


إذن ما الأمر مع onMeasureChild؟

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

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

أشعل النار على أنه يمكنك التقدم عند العمل مع CollapsingToolbarLayout


  • عند تغيير طرق العرض ، يجب تجنب onLayout. على سبيل المثال ، لا يجب عليك تغيير layoutParams أو textSize داخل BehaviorRule ، وإلا سيتم استنزاف الأداء بشكل كبير.
  • إذا كنت ترغب في العمل مع شريط الأدوات من خلال OnOffsetChangedListener ، فإن onLayout أكثر خطورة - سيتم تشغيل طريقة onOffsetChanged إلى أجل غير مسمى.
  • CoordinatorLayout.Behavior لا يجب أن يعتمد على طريقة العرض (layoutDependsOn) ، والتي يمكن أن تنتقل إلى الرؤية. عندما تعود طريقة العرض هذه إلى View.VISIBLE ، لن يتفاعل السلوك.
  • إذا كان شريط الأدوات سيكون خارج AppBarLayout ، فحينئذٍ لمنع شريط الأدوات من حظره ، يلزمك إضافة سمة android: translationZ = "5dp" إلى مجموعة العرض الرئيسية لشريط الأدوات.

في الختام


لدينا حل يسمح لك برسم CollapsingToolbarLayout بسرعة مع منطق يسهل قراءته وتعديله نسبيًا. يتم تشكيل جميع القواعد والتبعيات في إطار فئة واحدة - CoordinatorLayout.Behavior. يمكن عرض الرمز على جيثب .

Source: https://habr.com/ru/post/ar426369/


All Articles