كيفية تطبيق ميزة ‘التمرير لإظهار الخيارات’ في RecyclerView على أندرويد

دقائق القراءة: 9
هل سبق أن رغبت في تعديل عنصر ضمن قائمة في تطبيقك دون الحاجة إلى فتح هذا العنصر والبحث عن خيارات التعديل؟ إن تمكين هذه الوظيفة يمكن أن يوفر للمستخدم تجربة استخدام (UX) ممتازة. فكر في تطبيق مثل Pocket، وهو تطبيق لحفظ المقالات مملوك لشركة Mozilla، والذي يطبق شيئًا مشابهًا. يمكنك مشاركة، أرشفة، أو حذف مقالاتك المحفوظة مباشرة من القائمة دون فتح المقال نفسه. بدلاً من ذلك، يمكنك النقر على زر القائمة في الزاوية العلوية اليمنى واختيار خيار التعديل الخاص بك. في هذا الدليل التعليمي، سنسعى لتطوير هذه الوظيفة برمجياً.

إليك ما نطمح لتحقيقه في نهاية هذا الدليل:
مثال على تطبيق ميزة التمرير لإظهار الخيارات في RecyclerView

إنشاء قائمة RecyclerView أساسية

RecyclerView هو نسخة متقدمة ومرنة من ListView و GridView. إنه قادر على استيعاب كميات كبيرة من بيانات القائمة ويقدم أداءً أفضل من سابقاته. كما يوحي الاسم، يقوم RecyclerView ‘بإعادة تدوير’ عناصر قائمتنا بمجرد خروجها من مجال الرؤية عند التمرير، ويعيد ملئها عند عودتها إلى العرض. وبهذه الطريقة، لا يحتاج حاوية القائمة إلا للحفاظ على عدد محدود من العروض (views) وليس القائمة بأكملها. إنه مرن لدرجة أن الفئة الجديدة ViewPager2، المستخدمة لإنشاء علامات تبويب قابلة للتمرير، مبنية فوق RecyclerView.

إنشاء كائن POJO لبيانات القائمة

لنبدأ بإنشاء كائن POJO (Plain Old Java Object) بسيط لاحتواء بيانات القائمة. هذا الكائن سيمثل كل عنصر في قائمتنا.

 public class RecyclerEntity { private String title; private boolean showMenu = false ; private int image; public RecyclerEntity () { } public RecyclerEntity (String title, int image, boolean showMenu) { this .title = title; this .showMenu = showMenu; this .image = image; } public int getImage () { return image; } public void setImage ( int image) { this .image = image; } //... all the getters and setters }

لاحظ أن لدينا عضوًا showMenu هنا، وهو متغير منطقي (boolean) سيتولى مهمة التحكم في ظهور القائمة (menu) لعنصر القائمة هذا ضمن RecyclerView الخاص بنا.

إنشاء محول RecyclerView (Adapter)

المحول (Adapter) هو الجسر بين بياناتنا وواجهة المستخدم (UI). هو المسؤول عن إنشاء العروض (views) لكل عنصر في القائمة وربط البيانات بها.

 public class RecyclerAdapter extends RecyclerView . Adapter < RecyclerView . ViewHolder > { List<RecyclerEntity> list; Context context; public RecyclerAdapter (Context context, List<RecyclerEntity> articlesList) { this .list = articlesList; this .context = context; } @Override public RecyclerView. ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) { View v; v= LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false ); return new MyViewHolder(v); } @Override public void onBindViewHolder (RecyclerView.ViewHolder holder, final int position) { RecyclerEntity entity = list.get(position); if (holder instanceof MyViewHolder){ ((MyViewHolder)holder).title.setText(entity.getTitle()); ((MyViewHolder)holder).imageView.setImageDrawable(context.getResources().getDrawable(entity.getImage())); } } @Override public int getItemCount () { return list.size(); } public class MyViewHolder extends RecyclerView . ViewHolder { TextView title; ImageView imageView; ConstraintLayout container; public MyViewHolder (View itemView) { super (itemView); title = itemView.findViewById(R.id.title); imageView = itemView.findViewById(R.id.imageView); container = itemView.findViewById(R.id.container); } } }

عادةً، نضع الفئة الفرعية ViewHolder (مثل MyViewHolder) داخل قالب الفئة الرئيسية (super class template). هذا يسمح لنا بإرجاع كائن الفئة الفرعية ViewHolder المعرف مباشرة من دالة onCreateViewHolder(). وبالتالي، لا نحتاج إلى تحويل نوعه (cast) مرارًا وتكرارًا في دالة onBindViewHolder(). لكن هنا لا يمكننا فعل ذلك، وسنتعلم السبب في دقيقة.

تهيئة RecyclerView في النشاط الرئيسي (Activity)

الآن، لنقم بتهيئة RecyclerView في ملف النشاط الرئيسي MainActivity.java وتزويده بالبيانات والمحول.

 public class MainActivity extends AppCompatActivity { RecyclerView recyclerView; List<RecyclerEntity> list; RecyclerAdapter adapter; @RequiresApi(api = Build.VERSION_CODES.M) @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); recyclerView = findViewById(R.id.recyclerview); list = new ArrayList<>(); list.add( new RecyclerEntity( "This is the best title" , R.drawable.one, false )); list.add( new RecyclerEntity( "This is the second-best title" , R.drawable.two, false )); //... rest of the list items adapter = new RecyclerAdapter( this , list); recyclerView.setLayoutManager( new LinearLayoutManager( this )); recyclerView.setAdapter(adapter); } }

إضافة ميزة “التمرير لإظهار الخيارات”

الآن لنبدأ بجعل الأمور أكثر إثارة من خلال دمج ميزة التمرير.

إنشاء مورد تخطيط للقائمة (Menu Layout) وتهيئته في المحول

سنحتاج إلى تخطيط منفصل للقائمة التي ستظهر عند التمرير. بعد إنشاء ملف التخطيط (على سبيل المثال، recycler_menu.xml)، سنقوم بتعديل المحول RecyclerAdapter ليتعامل مع نوعين من العروض: عرض العنصر الأساسي وعرض القائمة.

 public class RecyclerAdapter extends RecyclerView . Adapter < RecyclerView . ViewHolder > { List<RecyclerEntity> list; Context context; private final int SHOW_MENU = 1 ; private final int HIDE_MENU = 2 ; public RecyclerAdapter (Context context, List<RecyclerEntity> articlesList) { this .list = articlesList; this .context = context; } @Override public int getItemViewType ( int position) { if (list.get(position).isShowMenu()){ return SHOW_MENU; } else { return HIDE_MENU; } } @Override public RecyclerView. ViewHolder onCreateViewHolder (ViewGroup parent, int viewType) { View v; if (viewType==SHOW_MENU){ v= LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_menu, parent, false ); return new MenuViewHolder(v); } else { v= LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_list, parent, false ); return new MyViewHolder(v); } } @Override public void onBindViewHolder (RecyclerView.ViewHolder holder, final int position) { RecyclerEntity entity = list.get(position); if (holder instanceof MyViewHolder){ //... same as above } if (holder instanceof MenuViewHolder){ //Menu Actions } } @Override public int getItemCount () { return list.size(); } public class MyViewHolder extends RecyclerView . ViewHolder { //... same as above } //Our menu view public class MenuViewHolder extends RecyclerView . ViewHolder { public MenuViewHolder (View view) { super (view); } } }

الآن أصبح لدينا فئتان فرعيتان من ViewHolder في المحول الخاص بنا: MyViewHolder (لعنصر القائمة الفعلي) و MenuViewHolder (لعرض القائمة). كلاهما يرث نفس الفئة RecyclerView.ViewHolder، لذلك نُرجع الفئة الأم RecyclerView.ViewHolder من دالة onCreateViewHolder(). تُرجع دالة getItemViewType() المتغير الصحيح (int variable) viewType الذي يخبرنا بنوع العرض الذي نريد إظهاره في RecyclerView لموضع معين: إما MyViewHolder أو MenuViewHolder. يتم استخدام متغير viewType هذا بعد ذلك بواسطة onCreateViewHolder() الذي يُرجع كائن ViewHolder المعني فعليًا.

إضافة وظائف إظهار/إخفاء القائمة في RecyclerAdapter

سنضيف الآن ثلاث دوال إلى RecyclerAdapter للتحكم في ظهور القائمة: إظهار القائمة لعنصر معين، التحقق مما إذا كانت أي قائمة ظاهرة، وإخفاء جميع القوائم.

 public void showMenu ( int position) { for ( int i= 0 ; i<list.size(); i++){ list.get(i).setShowMenu( false ); } list.get(position).setShowMenu( true ); notifyDataSetChanged(); } public boolean isMenuShown () { for ( int i= 0 ; i<list.size(); i++){ if (list.get(i).isShowMenu()){ return true ; } } return false ; } public void closeMenu () { for ( int i= 0 ; i<list.size(); i++){ list.get(i).setShowMenu( false ); } notifyDataSetChanged(); }

تجدر الإشارة إلى أن هناك العديد من الطرق للتعامل مع هذا الأمر. ولكن لتبسيط الشرح، نحتفظ بقيمة منطقية (boolean value) في كائن POJO الخاص بنا للحفاظ على رؤية القائمة. بعد تغيير قائمة البيانات الخاصة بنا، نستدعي دالة notifyDataSetChanged() لإعادة رسم القائمة.

إظهار القائمة عند الضغط المطول على عنصر القائمة

يمكننا تفعيل ظهور القائمة عند الضغط المطول على أي عنصر في القائمة. سنقوم بتعديل دالة onBindViewHolder() لإضافة مستمع للضغط المطول (OnLongClickListener).

 @Override public void onBindViewHolder (RecyclerView.ViewHolder holder, final int position) { RecyclerEntity entity = list.get(position); if (holder instanceof MyViewHolder){ ((MyViewHolder)holder).title.setText(entity.getTitle()); ((MyViewHolder)holder).imageView.setImageDrawable(context.getResources().getDrawable(entity.getImage())); ((MyViewHolder)holder).container.setOnLongClickListener( new View.OnLongClickListener() { @Override public boolean onLongClick (View v) { showMenu(position); return true ; } }); } if (holder instanceof MenuViewHolder){ //Set Menu Actions like: //((MenuViewHolder)holder).edit.setOnClickListener(null); } }

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

إظهار القائمة عند التمرير الجانبي (Swipe)

لتحقيق ميزة التمرير الجانبي، سنضيف مساعد لمس (ItemTouchHelper) في ملف MainActivity.java. هذا المساعد سيكتشف حركة التمرير ويقوم بتشغيل دالة showMenu().

 public class MainActivity extends AppCompatActivity { RecyclerView recyclerView; List<RecyclerEntity> list; RecyclerAdapter adapter; @RequiresApi(api = Build.VERSION_CODES.M) @Override protected void onCreate (Bundle savedInstanceState) { //... same as above adapter = new RecyclerAdapter( this , list); recyclerView.setLayoutManager( new LinearLayoutManager( this )); recyclerView.setAdapter(adapter); ItemTouchHelper.SimpleCallback touchHelperCallback = new ItemTouchHelper.SimpleCallback( 0 , ItemTouchHelper.LEFT) { private final ColorDrawable background = new ColorDrawable(getResources().getColor(R.color.background)); @Override public boolean onMove (RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) { return false ; } @Override public void onSwiped (RecyclerView.ViewHolder viewHolder, int direction) { adapter.showMenu(viewHolder.getAdapterPosition()); } @Override public void onChildDraw (Canvas c, RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, float dX, float dY, int actionState, boolean isCurrentlyActive) { super .onChildDraw(c, recyclerView, viewHolder, dX, dY, actionState, isCurrentlyActive); View itemView = viewHolder.itemView; if (dX > 0 ) { background.setBounds(itemView.getLeft(), itemView.getTop(), itemView.getLeft() + (( int ) dX), itemView.getBottom()); } else if (dX < 0 ) { background.setBounds(itemView.getRight() + (( int ) dX), itemView.getTop(), itemView.getRight(), itemView.getBottom()); } else { background.setBounds( 0 , 0 , 0 , 0 ); } background.draw(c); } }; ItemTouchHelper itemTouchHelper = new ItemTouchHelper(touchHelperCallback); itemTouchHelper.attachToRecyclerView(recyclerView); }

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

إخفاء القائمة

هناك ثلاث طرق رئيسية لإخفاء القائمة بعد ظهورها، مما يضمن تجربة مستخدم سلسة.

إخفاء القائمة عند التمرير على صف آخر

تم التعامل مع هذه الحالة بالفعل في دالة showMenu() داخل المحول (Adapter). قبل إظهار القائمة لأي صف، نقوم أولاً باستدعاء setShowMenu(false) لجميع الصفوف لإخفاء أي قائمة قد تكون ظاهرة.

إخفاء القائمة عند الضغط على زر الرجوع (Back Button)

يمكننا تجاوز دالة onBackPressed() في النشاط الرئيسي (Activity) لإخفاء القائمة إذا كانت ظاهرة، بدلاً من إغلاق النشاط مباشرة.

 @Override public void onBackPressed () { if (adapter.isMenuShown()) { adapter.closeMenu(); } else { super .onBackPressed(); } }

إخفاء القائمة عند تمرير المستخدم للقائمة

لضمان إخفاء القائمة عند بدء المستخدم في التمرير عبر القائمة، يمكننا إضافة مستمع لتغيير التمرير (OnScrollChangeListener) إلى RecyclerView.

recyclerView.setOnScrollChangeListener( new View.OnScrollChangeListener() { @Override public void onScrollChange (View v, int scrollX, int scrollY, int oldScrollX, int oldScrollY) { adapter.closeMenu(); } });

على الرغم من أن تطبيق Pocket يعتمد فقط على الضغط المطول لإظهار القائمة، فقد أضفنا في هذا المثال ميزة التمرير لإظهار القائمة لوظائف إضافية. يمكنك أيضًا إخفاء عنصر القائمة عند التمرير يمينًا/يسارًا مرة أخرى، لكنني أعتقد أن هذا قد يربك المستخدم.

الخلاصة التقنية

تُعد ميزة ‘التمرير لإظهار الخيارات’ في RecyclerView إضافة قيمة لتحسين تجربة المستخدم (UX) في تطبيقات أندرويد، خاصةً للعناصر التي تتطلب إجراءات سريعة دون الحاجة للانتقال إلى شاشة تفصيلية. لقد استعرضنا في هذا الدليل كيفية بناء هذه الميزة خطوة بخطوة، بدءًا من هيكلة البيانات والمحول، وصولاً إلى دمج ItemTouchHelper للتعامل مع إيماءات التمرير، وآليات إخفاء القائمة لضمان سلاسة التفاعل. ومع ذلك، من المهم تقييم مدى ملاءمة هذه الميزة لحجم البيانات وتعقيد الإجراءات. ففي التطبيقات التي تتعامل مع مجموعات بيانات ضخمة أو تتطلب خيارات تعديل متعددة، قد تكون حلول مثل التحرير الجماعي (bulk-edit) أو استخدام مربعات حوار BottomSheet أكثر فعالية. تذكر دائمًا أن الهدف هو توفير تجربة مستخدم بديهية وفعالة.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *