2017年1月9日 星期一


SAMPLE USING <ArrayAdapter> J

Let’s try a sample using ArrayAdapter that illustrated 7 techniques:
1. How to use Custom Adapter Class.
2. How to use Spinner.
3. How to receive Spinner selected item.
4. How to pass data to Custom Adapter Class.
5. How to get data from Custom Adapter Class.
6. How to make and use Custom ArrayAdapter Filter
7. How to Interface Activity from Custom Adapter Class


The sample code is to let you group 30 students (with student id, name and description) into playgroups which can be reviewed by using the Spinner’s dropdown list.
The maximum number of students in a playgroup can be changed by the SeekBar on top of the screen. You can select any student from the ListView with the CheckBox. When there is students selected, a Button on the right of the Spinner is shown. The Button shows the group number and number of students selected. Click that Button will put all selected students into the corresponding playgroup and add a new item in the Spinner dropdown list. Note that the first selected student is assigned as leader of that group. Click on the Spinner and its dropdown list comes up. It shows the number of students in the group, group number and leader. Choose any playgroup, the ListView will display all the members of the playgroup with leader highlighted. Choose “Not grouped” from Spinner to continue setting the other playgroups. When all students have set, “Not grouped” is deleted from the Spinner.



In the sample code, comments are added for corresponding technique and here are additional explanations:
Technique 1 - How to use Custom Adapter Class
Since we inflate our custom layout inside the getView() method, no need to pass layout for custom adapter.


Technique 2 - How to use Spinner
For Spinner, it should be noted that that are two type of views. The “Selected” item is always shown which controlled by getView() method. When the Spinner is expanded, the item view in the dropdown list is controlled by getDropDownView() method. If dataset is a simple String array [or class with toString() method)], the dropdown list can be simply set with setDropDownViewResource() using a layout of single TextView [android:id="@android:id/text1"].


Technique 3 - How to receive Spinner selected item
As “Selected” item is always shown in Spinner. Therefore most of the time, OnItemSelectedListener() is used rather than OnItemClickListener().


Technique 4 - How to pass data to Custom Adapter Class
The simplest way is to create public methods inside the Adapter, then it can pass data into the Adapter.


Technique 5 - How to get data from Custom Adapter Class
The simplest way is to create public methods that have return values inside the Adapter, then it can get data from Adapter to Activity.


Technique 6 - How to make and use Custom ArrayAdapter Filter
Remember to create a backup list of the original dataset and filter data out from there. The other important thing is that Filter is an async operation. The method, performFiltering(), is on another thread and after it is completed, publishResults() is called on UI thread to display result. Therefore when filter a large dataset that takes long time, you may show a progress bar in Activity before filtering and in publishResults(), call the Activity to dismiss the progress bar [Technique 7].


Technique 7 - How to Interface Activity from Custom Adapter Class
There are a few different ways to communicate between Activity and Adapter, the one shown here is using Interface.



Sample is tested with Android Studio. Codes in the files (3 Java and 2 XML) are below.
SampleDataItem.java:
class SampleDataItem {
    private int playgroup;
    private int num;
    private String name;
    private String description;
    private boolean selected;

    SampleDataItem(int playgroup, int num, String name, String description){
        this.playgroup = playgroup;
        this.num = num;
        this.name = name;
        this.description = description;
        selected = false;
    }
    int getPlaygroup() {return playgroup;}
    int getNum() {return num;}
    String getName() {return name;}
    String getDescription() {return description;}
    boolean isSelected() {return selected;}
    void setPlaygroup(int groupNum) {this.playgroup = groupNum;}
    void setNum(int newNum) {this.num = newNum;}
    void setSelected(boolean flag) { this.selected = flag;}

    @Override
    public String toString() {return getNum() + " " + getName();}
}


MainActivity.java:
@SuppressWarnings({"NullableProblems", "ConstantConditions"})
// 7. Code for Interface Activity from Custom Adapter Class sample.
public class MainActivity extends Activity implements MyArrayAdapter.MyArrayAdapterCallbacks{
    static final int NUM_OF_ITEMS_IN_LIST = 30;

    Button mbtAddGroup;
    ListView mListView;
    SeekBar mSeekBar;
    Spinner mSpinner;
    TextView mtvPlaygroupSize;
    ArrayList<SampleDataItem> mStudentList = new ArrayList<>();
    ArrayList<SampleDataItem> mPlaygroupList = new ArrayList<>();
    int mNewGroupNum = 1;

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

        // Setup sample DataSet.
        for(int i=1; i<=NUM_OF_ITEMS_IN_LIST; i++){
            SampleDataItem student = new SampleDataItem(0, i, "Student<"+i+">", "Description<"+i+">");
            mStudentList.add(student);
        }
        mPlaygroupList.add(new SampleDataItem(0, NUM_OF_ITEMS_IN_LIST, "Not Grouped", ""));

        mListView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE);
        // Spinner do not have ChoiceMode.

        mSeekBar.setOnSeekBarChangeListener(new OnSeekBarChangeListener(){
            @Override
            public void onStartTrackingTouch(SeekBar seekBar) {}
            @Override
            public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
                // 5. Code for getting data from Custom Adapter Class sample.
                MyArrayAdapter tmpAdapter = (MyArrayAdapter) mListView.getAdapter();
                if(progress < tmpAdapter.getSelectedItemsCount()){
                    progress = tmpAdapter.getSelectedItemsCount();
                }else if(progress < 2){
                    progress = 2;
                }
                seekBar.setProgress(progress);
                mtvPlaygroupSize.setText(String.valueOf(progress));
            }
            @Override
            public void onStopTrackingTouch(SeekBar seekBar) {
                // 4. Code for passing data to Custom Adapter Class sample.
                MyArrayAdapter tmpAdapter = (MyArrayAdapter) mListView.getAdapter();
                tmpAdapter.setPlaygroupSize(seekBar.getProgress());
            }
        });
        mbtAddGroup.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                // 5. Code for getting data from Custom Adapter Class sample.
                MyArrayAdapter tmpAdapter =  (MyArrayAdapter) mListView.getAdapter();
                ArrayList<SampleDataItem> selectedList = tmpAdapter.updateLists(mNewGroupNum);
                int groupSize = selectedList.size();

                tmpAdapter.getFilter().filter("0");
                mPlaygroupList.add(new SampleDataItem(mNewGroupNum, groupSize,
                        "in Group " + mNewGroupNum, "Leader: " + selectedList.get(0).getName()));
                SampleDataItem notGrouped = mPlaygroupList.get(0);
                groupSize = notGrouped.getNum() - groupSize;
                notGrouped.setNum(groupSize);
                if(groupSize == 0){
                    mPlaygroupList.remove(0);
                    tmpAdapter.getFilter().filter("1");
                }
                ArrayAdapter tmpAdapter1 = (ArrayAdapter) mSpinner.getAdapter();
                tmpAdapter1.notifyDataSetChanged();
                mNewGroupNum++;
                mbtAddGroup.setVisibility(View.INVISIBLE);
            }
        });
        sampleCodeForArrayAdapter();
    }

    // Get Views reference of main layout.
    private void getViews(){
        mtvPlaygroupSize = (TextView) findViewById(R.id.tvGroupSize);
        mSeekBar = (SeekBar) findViewById(R.id.seekBar1);
        mSpinner = (Spinner) findViewById(R.id.spinner);
        mbtAddGroup = (Button) findViewById(R.id.bt_group);
        mListView = (ListView) findViewById(R.id.listView1);
        mtvPlaygroupSize.setText(String.valueOf(mSeekBar.getProgress()));
        mbtAddGroup.setText(String.valueOf(mNewGroupNum));
        mbtAddGroup.setVisibility(View.INVISIBLE);
    }

    /**
     * Sample Code for using ArrayAdapter:
     */
    public void sampleCodeForArrayAdapter(){
        // 1. Code for Custom Adapter Class sample.
        MyArrayAdapter mStudentAdapter = new MyArrayAdapter(this, mStudentList);

        // 2. Code for Spinner sample.
        ArrayAdapter mPlaygroupAdapter = new ArrayAdapter<SampleDataItem>(this,
                android.R.layout.simple_list_item_1, mPlaygroupList){
            @Override
            public View getDropDownView(int position, View convertView, ViewGroup parent) {
                if(convertView == null) convertView = getLayoutInflater()
                    .inflate(R.layout.custom_list_items, parent, false);
                TextView tvId = (TextView) convertView.findViewById(R.id.tvId);
                TextView tvName = (TextView) convertView.findViewById(R.id.tvName);
                TextView tvDescription = (TextView) convertView.findViewById(R.id.tvDescription);
                CheckBox cb = (CheckBox) convertView.findViewById(R.id.cb);
                SampleDataItem currentItem = getItem(position);
                if(currentItem != null) {
                    tvId.setText(String.valueOf(currentItem.getNum()));
                    tvName.setText(currentItem.getName());
                    tvDescription.setText(currentItem.getDescription());
                    cb.setVisibility(View.INVISIBLE);
                }
                if(currentItem.getPlaygroup() == 0) convertView.setBackgroundColor(Color.TRANSPARENT);
                else if((position % 2) == 0) convertView.setBackgroundColor(0xff777700);
                else convertView.setBackgroundColor(0xff007777);
                return convertView;
            }
        };

        // 3. Code for Spinner item select sample.
        mSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener(){
            @Override
            public void onItemSelected(AdapterView<?> adapterView, View view, int position, long id) {
                SampleDataItem selectedItem = (SampleDataItem) adapterView.getItemAtPosition(position);
                MyArrayAdapter tmpAdapter =  (MyArrayAdapter) mListView.getAdapter();
                // 6. Code for Custom ArrayAdapter Filter sample.
                tmpAdapter.getFilter().filter(String.valueOf(selectedItem.getPlaygroup()));

                if((selectedItem.getPlaygroup() == 0)&&(tmpAdapter.getSelectedItemsCount() > 0))
                    mbtAddGroup.setVisibility(View.VISIBLE);
                else mbtAddGroup.setVisibility(View.INVISIBLE);
            }
            @Override
            public void onNothingSelected(AdapterView<?> adapterView) {}
        });

        mSpinner.setAdapter(mPlaygroupAdapter);
        mListView.setAdapter(mStudentAdapter);
    }

    // 7. Code for Interface Activity from Custom Adapter Class sample.
    @Override
    public void adapterCheckBoxClicked(int selectedCount) {
        if(selectedCount == 0) mbtAddGroup.setVisibility(View.INVISIBLE);
        else{
            String msg = mNewGroupNum + "-" + selectedCount;
            mbtAddGroup.setText(msg);
            mbtAddGroup.setVisibility(View.VISIBLE);
        }
    }
    @Override
    public void getLeaderName() {
        SampleDataItem selectedItem = (SampleDataItem) mSpinner.getSelectedItem();
        MyArrayAdapter tmpAdapter =  (MyArrayAdapter) mListView.getAdapter();
        tmpAdapter.setLeader(selectedItem.getDescription().substring(8));
    }
}


MyArrayAdapter.java:
@SuppressWarnings({"ConstantConditions", "SuspiciousMethodCalls", "NullableProblems"})
class MyArrayAdapter extends ArrayAdapter<SampleDataItem> {
    private LayoutInflater mInflater;
    private int mPlaygroupSize = 5;
    private List<Integer> mSelectedList = new ArrayList<>();
    // 6. Code for Custom ArrayAdapter Filter sample.
    private List<SampleDataItem> mBackupList = new ArrayList<>();
    // 7. Code for Interface Activity from Custom Adapter Class sample.
    private MyArrayAdapterCallbacks mCallbacks;

    MyArrayAdapter(Context context, List<SampleDataItem> objects) {
        super(context, 0, objects);
        mInflater = LayoutInflater.from(context);
        // 6. Code for Custom ArrayAdapter Filter sample.
        mBackupList.addAll(objects);
        // 7. Code for Interface Activity from Custom Adapter Class sample.
        mCallbacks = (MyArrayAdapterCallbacks) context;
    }

    private class ViewHolder{
        TextView tvId;
        TextView tvName;
        TextView tvDescription;
        CheckBox cb;

        ViewHolder(View convertView){
            this.tvId = (TextView) convertView.findViewById(R.id.tvId);
            this.tvName = (TextView) convertView.findViewById(R.id.tvName);
            this.tvDescription = (TextView) convertView.findViewById(R.id.tvDescription);
            this.cb = (CheckBox) convertView.findViewById(R.id.cb);
        }
    }

    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        ViewHolder viewHolder;
        // Prepare views ready for data
        if(convertView == null){
            convertView = mInflater.inflate(R.layout.custom_list_items, parent, false);
            viewHolder = new ViewHolder(convertView);
            convertView.setTag(viewHolder);
        }else{
            viewHolder = (ViewHolder) convertView.getTag();
        }
        viewHolder.cb.setOnClickListener(new View.OnClickListener(){
            @Override
            public void onClick(View view) {
                // Get view, position and DataList(position)
                CheckBox buttonView = (CheckBox) view;
                int pos = (int) buttonView.getTag();
                SampleDataItem selectedItem = getItem(pos);
                // Update DataLists
                selectedItem.setSelected(buttonView.isChecked());
                if(!buttonView.isChecked()){
                    mSelectedList.remove((Object)pos);
                }else{
                    if(mSelectedList.size() < mPlaygroupSize){
                        mSelectedList.add(pos);
                    }else{
                        Toast.makeText(getContext(), "Group already Full!!", Toast.LENGTH_SHORT).show();
                        selectedItem.setSelected(false);
                        buttonView.setChecked(false);
                    }
                }

                // Update all UI views by
                notifyDataSetChanged();
                // 7. Code for Interface Activity from Custom Adapter Class sample.
                mCallbacks.adapterCheckBoxClicked(mSelectedList.size());
            }
        });
        // Get data from DataList and set Views accordingly
        SampleDataItem currentItem = getItem(position);
        if(currentItem != null) {
            viewHolder.tvId.setText(String.valueOf(currentItem.getNum()));
            viewHolder.tvName.setText(currentItem.getName());
            viewHolder.tvDescription.setText(currentItem.getDescription());
            viewHolder.cb.setChecked(currentItem.isSelected());
            if(currentItem.getPlaygroup() != 0) viewHolder.cb.setVisibility(View.INVISIBLE);
            else viewHolder.cb.setVisibility(View.VISIBLE);
            if(currentItem.isSelected()) convertView.setBackgroundColor(Color.CYAN);
            else convertView.setBackgroundColor(Color.TRANSPARENT);
        }
        // Save position to CheckBox, so position can be retrieved when CheckBox is clicked
        viewHolder.cb.setTag(position);
        return convertView;
    }

    // 6. Code for Custom ArrayAdapter Filter sample.
    @Override
    public Filter getFilter() {return new SampleFilter();}
    @SuppressWarnings("unchecked")
    private class SampleFilter extends Filter{
        @Override
        protected FilterResults performFiltering(CharSequence charSequence) {
            int groupNum = Integer.parseInt(charSequence.toString());
            List<SampleDataItem> filteredList = new ArrayList<>();
            SampleDataItem tmpItem;
            for(int i=0; i<mBackupList.size(); i++){
                tmpItem = mBackupList.get(i);
                if(tmpItem.getPlaygroup() == groupNum) filteredList.add(tmpItem);
            }
            FilterResults filterResults = new FilterResults();
            filterResults.count = filteredList.size();
            filterResults.values = filteredList;
            return filterResults;
        }
        @Override
        protected void publishResults(CharSequence charSequence, FilterResults filterResults) {
            clear();
            addAll((List<SampleDataItem>) filterResults.values);
            if(!charSequence.toString().equals("0")){
                // 7. Code for Interface Activity from Custom Adapter Class sample.
                mCallbacks.getLeaderName();
            }
        }
    }

    // 4. Code for passing data to Custom Adapter Class sample.
    void setPlaygroupSize(int newSize) {mPlaygroupSize = newSize;}
    void setLeader(String leaderName){
        SampleDataItem tmpItem;
        for(int i=0; i<getCount(); i++){
            tmpItem = getItem(i);
            if(tmpItem.getName().equals(leaderName)) tmpItem.setSelected(true);
        }
    }
    // 5. Code for getting data from Custom Adapter Class sample.
    int getSelectedItemsCount() {return mSelectedList.size();}
    ArrayList<SampleDataItem> updateLists(int groupNum) {
        ArrayList<SampleDataItem> outList = new ArrayList<>();
        SampleDataItem tmpItem;
        for(int i=0; i<mSelectedList.size(); i++){
            tmpItem = getItem(mSelectedList.get(i));
            tmpItem.setPlaygroup(groupNum);
            outList.add(tmpItem);
            tmpItem.setSelected(false);
        }
        mSelectedList.clear();
        return outList;
    }
    // 7. Code for Interface Activity from Custom Adapter Class sample.
    interface MyArrayAdapterCallbacks {
        void adapterCheckBoxClicked(int selectedCount);
        void getLeaderName();
    }
}


activity_main.xml:
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <LinearLayout
        android:id="@+id/llHeader"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#aa7777aa"
        android:padding="7dp" >

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/tvGroupSize"
            android:layout_weight="5"
            android:layout_marginLeft="10dp"
            android:layout_marginStart="10dp"
            android:textSize="20sp"
            android:gravity="center" />

        <SeekBar
            android:id="@+id/seekBar1"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:max="9"
            android:progress="5"
            android:layout_gravity="center_vertical" />

    </LinearLayout>

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:background="#ff00ff00"
        android:layout_below="@+id/llHeader"
        android:id="@+id/rlSpinner">

        <Button
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/bt_group"
            android:layout_alignParentEnd="true"
            android:layout_alignParentRight="true"
            android:gravity="center"
            android:layout_centerVertical="true" />

        <Spinner
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:id="@+id/spinner"
            android:layout_margin="7dp"
            android:layout_toLeftOf="@+id/bt_group"
            android:layout_toStartOf="@+id/bt_group" />

    </RelativeLayout>

    <ListView
        android:id="@+id/listView1"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/rlSpinner"
        android:layout_margin="7dp" >
    </ListView>

</RelativeLayout>


custom_list_items.xml:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/rlRowView"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="horizontal"
    android:paddingBottom="15dp"
    android:paddingTop="15dp" >

    <CheckBox
        android:id="@+id/cb"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignParentEnd="true"
        android:layout_alignParentRight="true"
        android:focusable="false" />

    <TextView

        android:id="@+id/tvId"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/cb"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true"
        android:layout_alignParentTop="true"
        android:layout_marginBottom="5dp"
        android:layout_marginTop="5dp"
        android:gravity="center"
        android:minWidth="70dp"
        android:textSize="25sp" />

    <TextView

        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_alignBaseline="@+id/cb"
        android:layout_alignParentTop="true"
        android:layout_toEndOf="@id/tvId"
        android:layout_toLeftOf="@+id/cb"
        android:layout_toRightOf="@id/tvId"
        android:layout_toStartOf="@id/cb"
        android:id="@+id/tvName" />

    <TextView

        android:id="@+id/tvDescription"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_below="@+id/tvId"
        android:gravity="center"
        android:textSize="20sp" />

</RelativeLayout>

2017年1月8日 星期日


Something about item view in Adapter code J

Item view is defined in XML file but there are something in Adapter code should be known so that it can be used efficiently and error free.



1.          View Recycling: For example, a list is scrolled forward and so the first item view is disappeared while new item view is inflated on the bottom. Android does not delete the disappeared item view, it is passed to AdapterView’s getView() method as “convertView”. Therefore in code, it is always found in the beginning of getView() like “if(convertView == null) …” which is refer as View Recycling.

Android provides View Recycling which improves efficiency, so it should be implemented. The only exceptional case that I know is: there are many different item view types. This is because even a convertView is present, it may be a wrong type and new view needs to be inflated. For this situation, you may remove the check condition “if(convertView == null)” and inflate item view directly.

2.          View Holder: View Recycling gives back the whole item view but we normally do not operate directly on it with data, e.g. the item view is a RelativeLayout contains TextViews and CheckBoxes. Therefore in the getView() method, there is a list of codes “convertView.findViewById(…)” to find the view elements and then put data into them. However findViewById() is considered as having long execution time, so to avoid doing this on every data item, we use a View Holder class. View Holder is a brief layout/framework of the item view (convertView). When item view is created for the first time, we create a View Holder and attached it to item view by setTag(). Therefore when the view is recycled, we can use getTag() to get back this brief layout/framework rather than doing a list of findViewById().

For AdapterView, View Holder is optional but Android has further developed the idea and make a new View type, RecyclerView. For this one, it is more flexible with complex layout but View Holder is necessary.

3.          AdapterView’s ChildView: Most of the time, an AdapterView only display part of the dataset on the screen. Those visible item views are referred as child view. For the first or last item view, even it may only partly displayed, is also count as a child. One common mistake on beginner: try to get a “child view” that is not visible on the screen.

4.         notifyDataSetChanged(): This is one of the method that renders all the visual views again in the AdapterView. In the beginning of development, it should be already be used after data changed. This is because the ListView may have some design changes during development. Use this method helps to avoid making mistakes. Only when the app is come to final stage and notifyDataSetChanged() is causing performance issue [since all views are rendered again and it may takes time], use editor to find out the word “notifyDataSetChanged()” and replace it with codes that modify corresponding views directly.