تشترك هذه المشاريع في شيء واحد على الأقل: هناك قائمة بالعناصر في كل مكان. على سبيل المثال ، قائمة جهات اتصال دفتر الهاتف أو قائمة إعدادات ملف التعريف الخاص بك.
تستخدم مشاريعنا RecyclerView للقوائم. لن أخبرك بكيفية كتابة محول لـ RecyclerView أو كيفية تحديث البيانات في القائمة بشكل صحيح. سأخبرك في مقالتي عن مكون آخر مهم وغالبًا ما يتم تجاهله - RecyclerView.ItemDecoration ، وسأوضح لك كيفية استخدامه لتخطيط القائمة وما يمكن أن يفعله.
بالإضافة إلى البيانات الموجودة في القائمة ، يحتوي RecyclerView أيضًا على عناصر زخرفية مهمة ، على سبيل المثال ، فواصل الخلايا وأشرطة التمرير. وهنا سيساعدنا RecyclerView.ItemDecoration في رسم الديكور بالكامل وعدم إنتاج عروض غير ضرورية في تخطيط الخلايا والشاشة.
ItemDecoration فئة مجردة بثلاث طرق:
طريقة عرض الديكور قبل تقديم ViewHolder
public void onDraw(Canvas c, RecyclerView parent, State state)
طريقة عرض الديكور بعد عرض ViewHolder
public void onDrawOver(Canvas c, RecyclerView parent, State state)
طريقة مسافة بادئة ViewHolder عند ملء RecyclerView
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, State state)
من خلال توقيع أساليب onDraw * ، يمكنك أن ترى أن 3 مكونات رئيسية تستخدم لرسم الديكور.
- قماش - لتقديم الديكور اللازم
- RecyclerView - للوصول إلى معلمات RecyclerVIew نفسها
- RecyclerView.State - يحتوي على معلومات حول حالة RecyclerView
الاتصال بـ RecyclerView
هناك طريقتان لتوصيل مثيل ItemDecoration إلى RecyclerView:
public void addItemDecoration(@NonNull ItemDecoration decor)
public void addItemDecoration(@NonNull ItemDecoration decor, int index)
تتم إضافة جميع مثيلات RecyclerView.ItemDecoration المتصلة إلى قائمة واحدة ويتم عرضها مرة واحدة.
يحتوي RecyclerView أيضًا على طرق إضافية لمعالجة ItemDecoration.
إزالة زخرفة العنصر بالمؤشر
public void removeItemDecorationAt(int index)
إزالة مثيل ItemDecoration
public void removeItemDecoration(@NonNull ItemDecoration decor)
احصل على ItemDecoration بواسطة الفهرس
public ItemDecoration getItemDecorationAt(int index)
احصل على العدد الحالي من ItemDecoration المتصل في RecyclerView
public int getItemDecorationCount()
أعد رسم قائمة ItemDecoration الحالية
public void invalidateItemDecorations()
يحتوي SDK بالفعل على ورثة RecyclerView.emDecoration ، على سبيل المثال ، DeviderItemDecoration. يسمح لك برسم فواصل للخلايا.
إنه يعمل ببساطة شديدة ، فأنت بحاجة إلى استخدام drawable وسيقوم DeviderItemDecoration برسمه كفاصل خلية.
لنقم بإنشاء divider_drawable.xml:
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<size android:height="1dp" />
<solid android:color="@color/gray_A700" />
</shape>
وقم بتوصيل DividerItemDeoration إلى RecyclerView:
val dividerItemDecoration = DividerItemDecoration(this, RecyclerView.VERTICAL)
dividerItemDecoration.setDrawable(resources.getDrawable(R.drawable.divider_drawable))
recycler_view.addItemDecoration(dividerItemDecoration)
نحن نحصل:
مثالي للمناسبات البسيطة.
كل شيء أساسي تحت غطاء "غطاء" DeviderItemDecoration:
final int childCount = parent.getChildCount();
for (int i = 0; i < childCount; i++) {
final View child = parent.getChildAt(i);
parent.getDecoratedBoundsWithMargins(child, mBounds);
final int bottom = mBounds.bottom + Math.round(child.getTranslationY());
final int top = bottom - mDivider.getIntrinsicHeight();
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(canvas);
}
لكل مكالمة onDraw (...) ، قم بإجراء حلقة عبر كل طريقة العرض الحالية في RecyclerView وارسم الرسم الذي تم تمريره.
ولكن يمكن أن تحتوي الشاشة على عناصر تخطيط أكثر تعقيدًا من قائمة العناصر المتطابقة. قد تتضمن الشاشة:
أ. عدة أنواع من الخلايا
ب. عدة أنواع من المقسمات
ج. يمكن أن تحتوي الخلايا على حواف مستديرة ؛
د. يمكن أن تحتوي الخلايا على مسافات بادئة رأسية وأفقية مختلفة حسب بعض الظروف ؛
ه. كل ما سبق مرة واحدة.
لنلق نظرة على النقطة هـ. دعونا نضع لأنفسنا مهمة صعبة ونفكر في حلها.
مهمة:
- هناك ثلاثة أنواع من الخلايا الفريدة على الشاشة ، دعنا نسميها أ ، ب ، ج .
- جميع الخلايا بها مسافة بادئة 16 ديسيبل أفقيًا.
- تحتوي الخلية ب أيضًا على إزاحة رأسية تبلغ 8 ديسيبل.
- تحتوي الخلية أ على حواف مستديرة في الجزء العلوي إذا كانت الخلية الأولى في المجموعة وفي الجزء السفلي إذا كانت هي الخلية الأخيرة في المجموعة.
- يتم رسم الفواصل بين الخلايا ولكن لا يجب أن يكون هناك فاصل بعد الخلية الأخيرة في المجموعة.
- يتم رسم صورة ذات تأثير اختلاف المنظر على خلفية الخلية ج .
يجب أن ينتهي الأمر بهذا الشكل:
لننظر في خيارات الحل:
ملء القائمة بخلايا من أنواع مختلفة.
يمكنك كتابة محول خاص بك ، أو يمكنك استخدام مكتبتك المفضلة.
سوف أستخدم EasyAdapter .
مسافة بادئة الخلايا.
هناك ثلاث طرق:
- تعيين الحشوة ، بدء والنهاية لـ RecyclerView.
لن يعمل هذا الحل إذا لم يكن لكل الخلايا نفس المسافة البادئة. - قم بتعيين layout_marginStart و layout_marginEnd على الخلية.
يجب وضع مسافة بادئة لجميع الخلايا في القائمة بنفس المسافة البادئة. - اكتب تنفيذ ItemDecoration وتجاوز طريقة getItemOffsets.
أفضل بالفعل ، سيكون الحل أكثر تنوعًا وقابلية لإعادة الاستخدام.
زوايا التقريب لمجموعات الخلايا.
يبدو الحل واضحًا: أريد إضافة بعض التعداد {Start، Middle، End} فورًا ووضعه في الخلية مع البيانات. لكن السلبيات تظهر على الفور:
- يصبح نموذج البيانات في القائمة أكثر تعقيدًا.
- لمثل هذه التلاعبات ، سيتعين عليك حساب التعداد الذي سيتم تعيينه لكل خلية مسبقًا.
- بعد إزالة / إضافة عنصر إلى القائمة ، سيتعين عليك إعادة حسابه.
- ItemDecoration. يمكنك فهم الخلية الموجودة في المجموعة ورسم الخلفية بشكل صحيح في طريقة onDraw * ItemDecoration.
رسم فواصل.
يعد رسم الفواصل داخل الخلية ممارسة سيئة ، حيث ستكون النتيجة تخطيطًا معقدًا ، وستواجه الشاشات المعقدة مشاكل في العرض الديناميكي للفواصل. وهكذا يفوز ItemDecoration مرة أخرى. لن يعمل DeviderItemDecoration الجاهز من sdk لنا ، لأنه يرسم فواصل بعد كل خلية ، ولا يمكن حل هذا خارج الصندوق. تحتاج إلى كتابة التنفيذ الخاص بك.
المنظر على خلفية الخلية.
قد تتبادر إلى الذهن فكرة لوضع RecyclerView OnScrollListener واستخدام بعض طرق العرض المخصصة لتقديم الصورة. ولكن هنا مرة أخرى ، سيساعدنا ItemDecoration ، نظرًا لأنه يمكنه الوصول إلى Canvas Recycler وجميع المعلمات الضرورية.
إجمالاً ، نحتاج إلى كتابة 4 تطبيقات على الأقل لـ ItemDecoration. إنه لأمر جيد جدًا أنه يمكننا تقليل جميع النقاط للعمل مع ItemDecoration فقط وعدم لمس التخطيط ومنطق العمل الخاص بالميزة. بالإضافة إلى ذلك ، يمكن إعادة استخدام جميع تطبيقات ItemDecoration إذا كانت لدينا حالات مماثلة في التطبيق.
ومع ذلك ، على مدى السنوات القليلة الماضية ، ظهرت قوائم معقدة في مشاريعنا في كثير من الأحيان وفي كل مرة كان علينا أن نكتب مجموعة ItemDecoration لاحتياجات المشروع. هناك حاجة إلى حل أكثر شمولية ومرونة بحيث يمكن إعادة استخدامه في مشاريع أخرى.
ما هي الأهداف التي تريد تحقيقها:
- اكتب أقل عدد ممكن من ورثة ItemDecoration.
- افصل منطق العرض على اللوحة القماشية والحشو.
- تمتع بفوائد العمل باستخدام أساليب onDraw و onDrawOver.
- اجعل أدوات التزيين أكثر مرونة في التخصيص (على سبيل المثال ، رسم الفواصل حسب الحالة ، وليس كل الخلايا).
- اتخذ قرارًا دون الرجوع إلى الفواصل ، لأن ItemDecoration قادر على أكثر من رسم خطوط أفقية ورأسية.
- يمكن استغلال ذلك بسهولة من خلال النظر إلى نموذج المشروع.
نتيجة لذلك ، لدينا مكتبة ديكور RecyclerView.
تحتوي المكتبة على واجهة Builder بسيطة ، وواجهات منفصلة للعمل مع Canvas والمسافات البادئة ، بالإضافة إلى القدرة على العمل مع أساليب onDraw و onDrawOver. تنفيذ ItemDecoration هو واحد فقط.
دعنا نعود إلى مشكلتنا ونرى كيفية حلها باستخدام المكتبة.
يبدو منشئ الديكور لدينا بسيطًا:
Decorator.Builder()
.underlay()
...
.overlay()
...
.offset()
...
.build()
- .underlay (...) - مطلوب للعرض ضمن ViewHolder.
- .overlay (...) - مطلوب للرسم فوق ViewHolder.
- .offset (...) - تُستخدم لتعيين إزاحة ViewHolder.
هناك 3 واجهات تستخدم لرسم الديكور وتعيين المسافات البادئة.
- RecyclerViewDecor - يعرض الديكور إلى RecyclerView.
- ViewHolderDecor - يعرض الديكور إلى RecyclerView ، لكنه يتيح الوصول إلى ViewHolder.
- OffsetDecor - تستخدم لضبط المسافات البادئة.
لكن هذا ليس كل شيء. يمكن ربط ViewHolderDecor و OffsetDecor بـ ViewHolder معين باستخدام viewType ، مما يسمح لك بدمج عدة أنواع من الديكورات في قائمة واحدة أو حتى خلية. إذا لم يتم تمرير نوع العرض ، فسيتم تطبيق ViewHolderDecor و OffsetDecor على جميع ViewHolders في RecyclerView. لا تملك RecyclerViewDecor مثل هذه الفرصة ، لأنها مصممة للعمل مع RecyclerView بشكل عام ، وليس مع ViewHolders. بالإضافة إلى ذلك ، يمكن تمرير نفس مثيل ViewHolderDecor / RecyclerViewDecor إلى كل من التراكب (...) والطبقة السفلية (...).
لنبدأ في كتابة الكود
مكتبة EasyAdapter يستخدم ItemControllers لإنشاء ViewHolder. باختصار ، فهم مسؤولون عن إنشاء ViewHolder وتحديده. على سبيل المثال لدينا ، وحدة تحكم واحدة كافية ، والتي يمكنها عرض ViewHolders مختلفة. الشيء الرئيسي هو أن نوع viewType فريد لكل تخطيط خلية. تبدو هكذا:
private val shortCardController = Controller(R.layout.item_controller_short_card)
private val longCardController = Controller(R.layout.item_controller_long_card)
private val spaceController = Controller(R.layout.item_controller_space)
لضبط المسافات البادئة ، نحتاج إلى سليل OffsetDecor:
class SimpleOffsetDrawer(
private val left: Int = 0,
private val top: Int = 0,
private val right: Int = 0,
private val bottom: Int = 0
) : Decorator.OffsetDecor {
constructor(offset: Int) : this(offset, offset, offset, offset)
override fun getItemOffsets(
outRect: Rect,
view: View,
recyclerView: RecyclerView,
state: RecyclerView.State
) {
outRect.set(left, top, right, bottom)
}
}
لرسم زوايا دائرية ، يحتاج ViewHolder إلى وراثة ViewHolderDecor. نحن هنا بحاجة إلى OutlineProvider بحيث يتم أيضًا قص حالة الضغط عند الحواف.
class RoundDecor(
private val cornerRadius: Float,
private val roundPolitic: RoundPolitic = RoundPolitic.Every(RoundMode.ALL)
) : Decorator.ViewHolderDecor {
override fun draw(
canvas: Canvas,
view: View,
recyclerView: RecyclerView,
state: RecyclerView.State
) {
val viewHolder = recyclerView.getChildViewHolder(view)
val nextViewHolder =
recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition + 1)
val previousChildViewHolder =
recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition - 1)
if (cornerRadius.compareTo(0f) != 0) {
val roundMode = getRoundMode(previousChildViewHolder, viewHolder, nextViewHolder)
val outlineProvider = view.outlineProvider
if (outlineProvider is RoundOutlineProvider) {
outlineProvider.roundMode = roundMode
view.invalidateOutline()
} else {
view.outlineProvider = RoundOutlineProvider(cornerRadius, roundMode)
view.clipToOutline = true
}
}
}
}
لرسم فواصل ، سنكتب وريث ViewHolderDecor آخر:
class LinearDividerDrawer(private val gap: Gap) : Decorator.ViewHolderDecor {
private val dividerPaint = Paint(Paint.ANTI_ALIAS_FLAG)
private val alpha = dividerPaint.alpha
init {
dividerPaint.color = gap.color
dividerPaint.strokeWidth = gap.height.toFloat()
}
override fun draw(
canvas: Canvas,
view: View,
recyclerView: RecyclerView,
state: RecyclerView.State
) {
val viewHolder = recyclerView.getChildViewHolder(view)
val nextViewHolder = recyclerView.findViewHolderForAdapterPosition(viewHolder.adapterPosition + 1)
val startX = recyclerView.paddingLeft + gap.paddingStart
val startY = view.bottom + view.translationY
val stopX = recyclerView.width - recyclerView.paddingRight - gap.paddingEnd
val stopY = startY
dividerPaint.alpha = (view.alpha * alpha).toInt()
val areSameHolders =
viewHolder.itemViewType == nextViewHolder?.itemViewType ?: UNDEFINE_VIEW_HOLDER
val drawMiddleDivider = Rules.checkMiddleRule(gap.rule) && areSameHolders
val drawEndDivider = Rules.checkEndRule(gap.rule) && areSameHolders.not()
if (drawMiddleDivider) {
canvas.drawLine(startX.toFloat(), startY, stopX.toFloat(), stopY, dividerPaint)
} else if (drawEndDivider) {
canvas.drawLine(startX.toFloat(), startY, stopX.toFloat(), stopY, dividerPaint)
}
}
}
لتكوين divader الخاص بنا ، سنستخدم فئة Gap.kt:
class Gap(
@ColorInt val color: Int = Color.TRANSPARENT,
val height: Int = 0,
val paddingStart: Int = 0,
val paddingEnd: Int = 0,
@DividerRule val rule: Int = MIDDLE or END
)
سوف يساعد على ضبط اللون والارتفاع والحشو الأفقي وقواعد الرسم
للمقسم. يبقى آخر وريث ViewHolderDecor. لرسم صورة ذات تأثير اختلاف المنظر.
class ParallaxDecor(
context: Context,
@DrawableRes resId: Int
) : Decorator.ViewHolderDecor {
private val image: Bitmap? = AppCompatResources.getDrawable(context, resId)?.toBitmap()
override fun draw(
canvas: Canvas,
view: View,
recyclerView: RecyclerView,
state: RecyclerView.State
) {
val offset = view.top / 3
image?.let { btm ->
canvas.drawBitmap(
btm,
Rect(0, offset, btm.width, view.height + offset),
Rect(view.left, view.top, view.right, view.bottom),
null
)
}
}
}
دعونا نجمع كل شيء الآن.
private val decorator by lazy {
Decorator.Builder()
.underlay(longCardController.viewType() to roundDecor)
.underlay(spaceController.viewType() to paralaxDecor)
.overlay(shortCardController.viewType() to dividerDrawer2Dp)
.offset(longCardController.viewType() to horizontalOffsetDecor)
.offset(shortCardController.viewType() to horizontalOffsetDecor)
.offset(spaceController.viewType() to horizontalAndVerticalOffsetDecor)
.build()
}
نقوم بتهيئة RecyclerView ، نضيف الديكور وأجهزة التحكم الخاصة بنا إليه:
private fun init() {
with(recycler_view) {
layoutManager = LinearLayoutManager(this@LinearDecoratorActivityView)
adapter = easyAdapter
addItemDecoration(decorator)
setPadding(0, 16.px, 0, 16.px)
}
ItemList.create()
.apply {
repeat(3) {
add(longCardController)
}
add(spaceController)
repeat(5) {
add(shortCardController)
}
}
.also(easyAdapter::setItems)
}
هذا كل شئ. الديكور في قائمتنا جاهز.
تمكنا من كتابة مجموعة من الديكورات التي يمكن إعادة استخدامها بسهولة وتخصيصها بمرونة.
دعونا نرى كيف يمكن تطبيق الديكورات الأخرى.
PageIndicator لـ RecyclerView الأفقي
رسائل الدردشة الفقاعية وشريط التمرير:
حالة أكثر تعقيدًا - رسم الأشكال والأيقونات وتغيير المظهر دون إعادة تحميل الشاشة:
رأس مثبت
كود المصدر مع الأمثلة
خاتمة
على الرغم من بساطة واجهة ItemDecoration ، إلا أنها تتيح لك القيام بأشياء معقدة باستخدام القائمة دون تغيير التخطيط. آمل أن أكون قادرًا على إظهار أن هذه أداة قوية بما يكفي وتستحق اهتمامك. وستساعدك مكتبتنا في تزيين قوائمك بشكل أسهل.
أشكركم جميعًا على اهتمامكم ، وسأكون سعيدًا بتعليقاتكم.
تحديث: 08/06/2020 مضاف مثال للرأس اللاصق