18/9/2013 - برمجة Android - خاصية البحث لقوائم ListView المُتقدمه




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

أولاً، أفترض إنّك قد قرأت الدرس بناء قوائم ListView مُتقدمه و فهمته لأنني سأعتمد عليه وسأُكمل على الشيفره التي قمنا بكتابتها في ذلك الدرس، إن لم تكن قد قرأته يجب قراءته أولاً حتى تتمكن من إستيعاب هذا الدرس بشكل جيد.

الشيفرة التي سنعتمد عليها هي التاليه، وهي نفس شيفرة ذلك الدرس :

package com.maastaar.androidtutorials;

import java.util.ArrayList;
import java.util.Iterator;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.CheckBox;
import android.widget.CompoundButton;
import android.widget.CompoundButton.OnCheckedChangeListener;
import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ListView listView = (ListView) findViewById( R.id.listView1 );
Button btnOK = (Button) findViewById( R.id.button1 );

// ... //

final PersonsAdapter personsAdapter = new PersonsAdapter( this );

personsAdapter.add( new Person( "Name 1", "12345678", "name1@example.com" ) );
personsAdapter.add( new Person( "Name 2", "87654321", "name2@example.com" ) );
personsAdapter.add( new Person( "Name 3", "12348765", "name3@example.com" ) );

// ... //

listView.setAdapter( personsAdapter );

// ... //

btnOK.setOnClickListener( new OnClickListener()
{
@Override
public void onClick( View parent )
{
String message = "";

ArrayList selectedPersons = personsAdapter.getCheckedList();

Iterator it = selectedPersons.iterator();

while ( it.hasNext() )
{
Person currPerson = it.next();

message += currPerson.getName() + ", ";
}

Toast.makeText( getApplicationContext(), message , Toast.LENGTH_LONG ).show();
}
});
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}

class PersonsAdapter extends ArrayAdapter
{
private Context context;
private ArrayList checkedList;

public PersonsAdapter( Context context )
{
super( context, R.layout.person_row );

this.context = context;
this.checkedList = new ArrayList();
}

@Override
public View getView( final int position, View convertView, ViewGroup parent )
{
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );

View rowView = inflater.inflate( R.layout.person_row, parent, false );

// ... //

CheckBox checkBox = ( CheckBox ) rowView.findViewById( R.id.checkBox1 );
TextView txtPhone = ( TextView ) rowView.findViewById( R.id.textView1 );
TextView txtEmail = ( TextView ) rowView.findViewById( R.id.textView2 );

// ... //

Person currPerson = getItem( position );

checkBox.setText( currPerson.getName() );
txtPhone.setText( currPerson.getPhone() );
txtEmail.setText( currPerson.getEmail() );

// ... //

checkBox.setOnCheckedChangeListener( new OnCheckedChangeListener()
{
@Override
public void onCheckedChanged( CompoundButton parent, boolean isChecked )
{
if ( isChecked )
checkedList.add( getItem( position ) );
else
checkedList.remove( getItem( position ) );
}
});

// ... //

return rowView;
}

public ArrayList getCheckedList()
{
return this.checkedList;
}
}

class Person
{
private String name;
private String phone;
private String email;

Person( String name, String phone, String email )
{
this.name = name;
this.phone = phone;
this.email = email;
}

public String getName()
{
return this.name;
}

public String getPhone()
{
return this.phone;
}

public String getEmail()
{
return this.email;
}
}


عندما قمنا ببناء خاصيّة البحث مع القوائم البسيطة (في الدرس السابق) كانت العملية بسيطة، و كُل ما قمنا به هو إستخدام الدالة filter والموجوده في كائن من نوع Filter يُقدمه لنا المُحوّل (Adapter) نفسه عن طريق مناداة الدالة getFilter. ( إن لم تكن تتذكر هذه التفاصيل أنصح بمراجعة الدرس "إضافة خاصيّة البحث إلى قوائم ListView" ) و لكن الأمور هُنا مُختلفه، لأنّ محتويات قائمتنا الحالية ليست مُجرّد نص و بالتالي يُمكن للدالة filter أن تبحث فيه، بل تحتوي قائمتنا على عَدد من الحقول (الإسم و رقم الهاتف و البريد الإلكتروني) المُجمعه في كائن واحد من نوع Person.

إن جربت إستخدام الطريقة العادية لإضافة خاصية البحث والتي شُرحت في الدرس السابق مع قائمتنا الحالية فإنها لن تُجدي نفعاً لنفس السبب الذي ذكرته منذ قليل (وهو تعدد حقول المعلومات و عدم الإعتماد على محتوى وحيد نصّي). إذاً ما الحل؟

ببساطة سنُعيد كتابة الدالة getFilter و نُعيد تعريف الكائن الذي تُعيده هذه الدالة :).

الخطوة الأولى : كتابة الشيفرات الإعتيادية
لنبدأ أول خطوة بسيطة، أضف مُربع نص EditText في واجهة البرنامج أعلى القائمه، و داخل الدالة onCreate ننادي مُربع النص برمجياً :

EditText txtSearch = (EditText) findViewById( R.id.editText1 );


وبعد شيفرة إضافة البيانات إلى المُحوّل و ربط الزر نقوم بإضافة الشيفرة التالية وهي المسؤولة عن مناداة الدالة filter والتي تُفلتر البيانات وفقاً لما يكتبه المُستخدم في مربع النص وذلك عندما يكتب المُستخدم شيئاً في مربع النص، تماماً كما فعلنا في الدرس السابق و الذي بنينا فيه خاصية البحث لقائمة بسيطة :

txtSearch.addTextChangedListener( new TextWatcher() 
{

@Override
public void afterTextChanged(Editable arg0) { }

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

@Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
personsAdapter.getFilter().filter( s );
}
});


لحد الآن لا يوجد شيء جديد هنا، فكُل شيء قد تعاملت معه مُسبقاً، الشيفرة الجديدة للدالة onCreate بعد التعديل أصبحت كالتالي :

	protected void onCreate(Bundle savedInstanceState) 
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ListView listView = (ListView) findViewById( R.id.listView1 );
Button btnOK = (Button) findViewById( R.id.button1 );
EditText txtSearch = (EditText) findViewById( R.id.editText1 );

// ... //

final PersonsAdapter personsAdapter = new PersonsAdapter( this );

personsAdapter.add( new Person( "Name 1", "12345678", "name1@example.com" ) );
personsAdapter.add( new Person( "Name 2", "87654321", "name2@example.com" ) );
personsAdapter.add( new Person( "Name 3", "12348765", "name3@example.com" ) );

// ... //

listView.setAdapter( personsAdapter );

// ... //

btnOK.setOnClickListener( new OnClickListener()
{
@Override
public void onClick( View parent )
{
String message = "";

ArrayList selectedPersons = personsAdapter.getCheckedList();

Iterator it = selectedPersons.iterator();

while ( it.hasNext() )
{
Person currPerson = it.next();

message += currPerson.getName() + ", ";
}

Toast.makeText( getApplicationContext(), message , Toast.LENGTH_LONG ).show();
}
});

// ... //

txtSearch.addTextChangedListener( new TextWatcher()
{

@Override
public void afterTextChanged(Editable arg0) { }

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

@Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
personsAdapter.getFilter().filter( s );
}
});

// ... //
}


الخطوة الثانية : التعديل على المُحوّل الذي قمنا بكتابته
الخطوة الأهم الآن والتي من خلالها سنكتب الشيفرة التي ستقوم بالعمل المطلوب هي التعديل على المُحوّل PersonsAdapter، فكما ذكرت مُسبقاً يجب علينا أن نكتب (نُعيد تعريف Override) الدالة getFilter بنفسنا حتى نتمكن من تحديد كيفية البحث في قائمتنا، فما نريده هو البحث في القائمة وفقاً لإسم الشخص، وسيتبين لك فيما بعد إنّه لك حرية إختيار الحقل الذي سنبحث فيه.

أولاً سأطلب منك إضافة القائمة التاليه ضمن حقول الفئة PersonsAdapter :

private ArrayList resultList = null;


و كذا سأطلب منك كتابة (إعادة تعريف) الدوال التاليه للفئة :

	@Override
public Person getItem( int position )
{
if ( this.resultList != null )
return this.resultList.get( position );
else
return super.getItem( position );
}

@Override
public int getCount()
{
if ( this.resultList != null )
return this.resultList.size();
else
return super.getCount();
}


في الوقت الحالي لن أشرح ما الغرض من القائمة resultList ولماذا تم إستخدامها في إعادة تعريف دالة إحضار عنصر من القائمة (getItem) و دالة معرفة عدد عناصر القائمة (getCount)، و لكن فيما بعد سيتبين لنا سبب إضافة هذه القائمة وإعادة تعريف هاتين الدالتين.

لنبدأ الآن بإعادة تعريف الدالة getFilter :

	@Override
public Filter getFilter()
{


الدالة getFilter كما هو واضح تُعيد كائن من نوع Filter، الفئة Filter تُوفّر للمُبرمج دالة بإسم filter يُمرر المُبرمج نصاً إلى هذه الداله لتتم عملية البحث وفقاً لهذا النص، وهي بالضبط الدالة التي إستخدمناها مُسبقاً، كذلك هُناك دالتان مَحميتان (Protected) في هذه الفئة و هما performFiltering و publishResults.

بالنسبة للدالة performFiltering فهي التي تعمل أولاً (في خيط تنفيذ) وهي المسؤولة عن عمليات البحث و إعادة ناتج البحث، أما الثانية وهي publishResults فيتم مناداتها بعد performFiltering وهي مسؤولة عن واجهة المُستخدم وتحديث البيانات المعروضه في واجهة المُستخدم.

الدالة performFiltering تُعيد قيمة من نوع FilterResults، وهو كائن بسيط سنرى طريقة التعامل معه عند كتابة الشيفرة.

المطلوب منّا هو إعادة كتابة هاتين الدالتين و بالتالي يُمكننا أن نبحث في أسماء الأشخاص، نُكمل ما بدأنا به من تعريف للدالة getFilter :

		return ( new Filter() {

@Override
protected FilterResults performFiltering( CharSequence c )
{


الأمور بسيطة هنا بعدما شرحنا طريقة العمل في الأعلى، أنشأنا كائناً من نوع Filter و بدأنا بتعريف الدالة التي يتم مناداتها أولاً وهي performFiltering والتي ستقوم بعملية البحث الفعلية و تُعيد قائمة النتائج. البارامتر التي تستقبله الداله performFiltering هو النص المطلوب البحث في القائمة على أساسه.

ننتقل للجزئية التاليه :

				resultList = null;
ArrayList resultArray = new ArrayList();


هُنا قمنا بوضع القيمه null للقائمة resultList و التي عرفناها في الفئة PersonsAdapter و سأخبرك بعد قليل بالسبب، السطر الثاني نُعرّف مصفوفه من نوع Person و هي التي ستحتوي على نتائج البحث.

				for ( int k = 0; k < getCount(); k++ )
{
Person current = getItem( k );

if ( current.getName().contains( c ) )
resultArray.add( current );
}


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

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

هكذا نكون قد إنتهينا من الجزء الأساسي من عملية البحث، يجب علينا الآن أن نُعيد نتيجة البحث، ولكن لحظه! نتيجة البحث لدينا مُخزنه في مصفوفة ArrayList و الدالة performFiltering تُعيد قيمة من نوع FilterResults! إذاً كيف سنُعيد نتيجة البحث حتى نتمكن من عرضها في واجهة المستخدم الرسومية من خلال الدالة الثانيه publishResults؟

حسناً لا تقلق، الموضوع بسيط :) :

				FilterResults results = new FilterResults();

results.values = resultArray;
results.count = resultArray.size();

// ... //

return results;


هكذا كُل شيء، عرّفنا كائن من نوع FilterResults و الذي يحتوي على مُتغيرين الأول values يجب أن يحتوي على عناصر الناتج و الثاني هو count و الذي يجب أن يحتوي على عدد هذه القيم.

و أخيراً قمنا بإعادة هذا الكائن، و نكون بهذه الحالة إنتهينا من كتابة الدالة التي يتم مناداتها أولاً و هي performFiltering و شيفرتها الكاملة كالتالي :

			@Override
protected FilterResults performFiltering( CharSequence c )
{
resultList = null;
ArrayList resultArray = new ArrayList();

// ... //

for ( int k = 0; k < getCount(); k++ )
{
Person current = getItem( k );

if ( current.getName().contains( c ) )
resultArray.add( current );
}

// ... //

FilterResults results = new FilterResults();

results.values = resultArray;
results.count = resultArray.size();

// ... //

return results;
}


بعدما تنتهي الدالة performFiltering من عملها يتم مناداة الدالة publishResults والتي يجب أن تحتوي على الشيفرة المُتعلقه بأمور الواجهة الرسوميه لأنها تعمل بخيط التنفيذ الخاص بالواجهة الرسومية (UI Thread)، على كُل حال إحدى البارامترات التي تُمرر إلى هذه الدالة هي نتيجة البحث، ستكون شيفرة publishResults كالتالي :

			@Override
protected void publishResults( CharSequence constraint, FilterResults results )
{
resultList = (ArrayList results.values;
notifyDataSetChanged();
}


أولاً، قُمنا بأخذ نتيجة البحث الموجودة في البارامتر results والتي إستخلصناها بالأساس في الدالة الأولى performFiltering ثم وضعنا قيمها في المصفوفة resultList و التي عرّفناها في مُحوّلنا عندما بدأنا بتعديل المُحوّل PersonsAdapter.

بعد ذلك قمنا بإستدعاء الدالة notifyDataSetChanged و هي دالة تُقدمها لنا الفئة ArrayAdapter و من خلالها نُخبر النظام بأنّ البيانات تم تحديثها لذلك يجب تحديث الواجهة الرسومية لتعرض هذه البيانات الجديده.

إلى هُنا نكون قد إنتهينا من العمل البرمجي ولكن هناك نقطه هامه للتوضيح :)، هذه النقطة تخص المصفوفه resultList، فكما تذكر فإننا أضفنا هذه المصفوفة و أعدنا تعريف الدالتين getItem و getCount في المُحوّل ولكن لم نشرح السبب.

إذا كنت قد لاحظت فإنّ القيمة الأوليّه لـ resultList هي null، و في هذه الحاله نَعني إنّ قائمة العناصر يجب أن تظهر كامله لأنه لا يوجد عملية بحث تمّت، و على هذا الأساس تم تعريف الدالتين getItem و getCount :

	@Override
public Person getItem( int position )
{
if ( this.resultList != null )
return this.resultList.get( position );
else
return super.getItem( position );
}


لاحظ، ففي حال لم تكن resultList فارغه هذا يعني إنّ المُستخدم قام بعملية بحث وبالتالي القائمة التي يجب الإعتماد عليها هي هي قائمة نتيجة البحث وليست القائمة الأصلية و التي تحتوي على جميع العناصر.

أما في حال كانت resultList فارغه هذا يعني إنه لم تتم أي عملية بحث وبالتالي إعتمد على قائمتنا الأساسية (والتي تحتوي على جميع العناصر) في إحضار عناصر هذه القائمة.

و كذا الحال بالنسبة لـ getCount :

	@Override
public int getCount()
{
if ( this.resultList != null )
return this.resultList.size();
else
return super.getCount();
}


إذا الموضوع ببساطة، نقوم بعملية البحث و نستخلص قائمة تحتوي على نتائج البحث نضعها في resultList، ثم نطلب تحديث القائمة في واجهة المُستخدم الرسومية حتى يتم عرض البيانات الجديدة، عملية تحديث الواجهة الرسومية تتطلب مناداة الدالتين getCount و getItem لتتمكن من إظهار العناصر، و لأنّ التعريف الأصلي لهاتين الدالتين يتعامل مع قائمة البيانات الأصلية (التي تحتوي على جميع العناصر) فقط فإننا بحاجة إلى إعادة تعريف هاتين الدالتين لتتمكنَّ من التعامل مع نتائج البحث في حال وجودها.

أما السطر التالي والذي وضعناه في أوّل تعريفنا للدالة performFiltering :

resultList = null;


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

أرجو أن يكون الشرح واضحاً، إن رغبت أن تسأل أي سؤال يُمكنك مراسلتي عن طريق البريد الإلكتروني أو تويتر و سأحاول أن أساعدك بأقرب وقت إن شاء الله.

الشيفرة الكامله :

package com.maastaar.androidtutorials;

import java.util.ArrayList;

public class MainActivity extends Activity {

@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);

ListView listView = (ListView) findViewById( R.id.listView1 );
Button btnOK = (Button) findViewById( R.id.button1 );
EditText txtSearch = (EditText) findViewById( R.id.editText1 );

// ... //

final PersonsAdapter personsAdapter = new PersonsAdapter( this );

personsAdapter.add( new Person( "Name 1", "12345678", "name1@example.com" ) );
personsAdapter.add( new Person( "Name 2", "87654321", "name2@example.com" ) );
personsAdapter.add( new Person( "Name 3", "12348765", "name3@example.com" ) );

// ... //

listView.setAdapter( personsAdapter );

// ... //

btnOK.setOnClickListener( new OnClickListener()
{
@Override
public void onClick( View parent )
{
String message = "";

ArrayList selectedPersons = personsAdapter.getCheckedList();

Iterator it = selectedPersons.iterator();

while ( it.hasNext() )
{
Person currPerson = it.next();

message += currPerson.getName() + ", ";
}

Toast.makeText( getApplicationContext(), message , Toast.LENGTH_LONG ).show();
}
});

// ... //

txtSearch.addTextChangedListener( new TextWatcher()
{

@Override
public void afterTextChanged(Editable arg0) { }

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) { }

@Override
public void onTextChanged(CharSequence s, int start, int before, int count)
{
personsAdapter.getFilter().filter( s );
}
});

// ... //
}

@Override
public boolean onCreateOptionsMenu(Menu menu)
{
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.main, menu);
return true;
}
}

class PersonsAdapter extends ArrayAdapter
{
private Context context;
private ArrayList checkedList;
private ArrayList resultList = null;

public PersonsAdapter( Context context )
{
super( context, R.layout.person_row );

this.context = context;
this.checkedList = new ArrayList();
}

@Override
public Person getItem( int position )
{
if ( this.resultList != null )
return this.resultList.get( position );
else
return super.getItem( position );
}

@Override
public int getCount()
{
if ( this.resultList != null )
return this.resultList.size();
else
return super.getCount();
}


@Override
public View getView( final int position, View convertView, ViewGroup parent )
{
LayoutInflater inflater = (LayoutInflater) context.getSystemService( Context.LAYOUT_INFLATER_SERVICE );

View rowView = inflater.inflate( R.layout.person_row, parent, false );

// ... //

CheckBox checkBox = ( CheckBox ) rowView.findViewById( R.id.checkBox1 );
TextView txtPhone = ( TextView ) rowView.findViewById( R.id.textView1 );
TextView txtEmail = ( TextView ) rowView.findViewById( R.id.textView2 );

// ... //

Person currPerson = getItem( position );

checkBox.setText( currPerson.getName() );
txtPhone.setText( currPerson.getPhone() );
txtEmail.setText( currPerson.getEmail() );

// ... //

checkBox.setOnCheckedChangeListener( new OnCheckedChangeListener()
{
@Override
public void onCheckedChanged( CompoundButton parent, boolean isChecked )
{
if ( isChecked )
checkedList.add( getItem( position ) );
else
checkedList.remove( getItem( position ) );
}
});

// ... //

return rowView;
}

@Override
public Filter getFilter()
{
return ( new Filter() {

@Override
protected FilterResults performFiltering( CharSequence c )
{
resultList = null;
ArrayList resultArray = new ArrayList();

// ... //

for ( int k = 0; k < getCount(); k++ )
{
Person current = getItem( k );

if ( current.getName().contains( c ) )
resultArray.add( current );
}

// ... //

FilterResults results = new FilterResults();

results.values = resultArray;
results.count = resultArray.size();

// ... //

return results;
}

@Override
protected void publishResults( CharSequence constraint, FilterResults results )
{
resultList = (ArrayList results.values;
notifyDataSetChanged();
}

});
}

public ArrayList getCheckedList()
{
return this.checkedList;
}
}

class Person
{
private String name;
private String phone;
private String email;

Person( String name, String phone, String email )
{
this.name = name;
this.phone = phone;
this.email = email;
}

public String getName()
{
return this.name;
}

public String getPhone()
{
return this.phone;
}

public String getEmail()
{
return this.email;
}
}