1. 程式人生 > >動手分析安卓仿QQ聯系人列表TreeView控件

動手分析安卓仿QQ聯系人列表TreeView控件

code children cas tail pri bstr abstract viewgroup teset

因項目需要需要用到仿QQ聯系人列表的控件樣式,於是網上找到一個輪子(https://github.com/TealerProg/TreeView),工作完成現在簡單分析一下這個源碼。

一、 需要用到的知識如下:

①安卓事件分發機制:(http://blog.csdn.net/lvxiangan/article/details/9309927 或 http://gundumw100.iteye.com/blog/1052270) ②安卓View繪制:http://blog.csdn.net/guolin_blog/article/details/16330267 ③安卓View坐標:http://blog.csdn.net/jason0539/article/details/42743531
二、下面簡單分析下 項目結構如下 技術分享 1、ITreeViewHeaderUpdater接口
 1 package com.markmao.treeview.widget;
 2 
 3 import android.view.View;
 4 
 5 /**
 6  * Update interface TreeView‘s header .
 7  *
 8  * @author markmjw
 9  * @date 2014-01-04
10  */
11 public interface ITreeViewHeaderUpdater {
12 /** Header Gone. */ 13 public static final int STATE_GONE = 0x00; 14 /** Header Visible. */ 15 public static final int STATE_VISIBLE_ALL = 0x01; 16 /** Header Push up. */ 17 public static final int STATE_VISIBLE_PART = 0x02; 18 19 /** 20 * Get TreeView‘s header state.
21 * 22 * @param groupPosition The group position. 23 * @param childPosition The child position. 24 * @return {@link #STATE_GONE}, {@link #STATE_VISIBLE_ALL}, 25 * {@link #STATE_VISIBLE_PART} 26 */ 27 public int getHeaderState(int groupPosition, int childPosition); 28 29 /** 30 * Update TreeView‘s header. 31 * 32 * @param header The TreeView‘s header view. 33 * @param groupPosition The group position. 34 * @param childPosition The child position. 35 * @param alpha The header‘s alpha value. 36 */ 37 public void updateHeader(View header, int groupPosition, int childPosition, int alpha); 38 39 /** 40 * The header view onClick. 41 * 42 * @param groupPosition The group position. 43 * @param status {@link #STATE_GONE}, {@link #STATE_VISIBLE_ALL}, 44 * {@link #STATE_VISIBLE_PART} 45 */ 46 public void onHeaderClick(int groupPosition, int status); 47 48 /** 49 * Get the header‘s state on click. 50 * 51 * @param groupPosition The group position. 52 * @return {@link #STATE_GONE}, {@link #STATE_VISIBLE_ALL}, 53 * {@link #STATE_VISIBLE_PART} 54 */ 55 public int getHeaderClickStatus(int groupPosition); 56 }

主要定義了三個狀態:STATE_GONE:HeaderView處於隱藏不顯示狀態,STATE_VISIBLE:HeaderView處於顯示狀態,STATE_VISIBLE_PART:HeaderView處於顯示且需要向上推起的臨界狀態。

2、BaseTreeViewAdapter實現ITreeViewHeaderUpdater接口

package com.markmao.treeview.widget;

import android.util.Log;
import android.util.SparseIntArray;
import android.widget.BaseExpandableListAdapter;

/**
 * The base adapter for TreeView.
 *
 * @author markmjw
 * @date 2014-01-04
 */
public abstract class BaseTreeViewAdapter extends BaseExpandableListAdapter implements
        ITreeViewHeaderUpdater {
    protected TreeView mTreeView;

    protected SparseIntArray mGroupStatusArray;

    public BaseTreeViewAdapter(TreeView treeView) {
        mTreeView = treeView;
        mGroupStatusArray = new SparseIntArray();
    }

    @Override
    public int getHeaderState(int groupPosition, int childPosition) {
        final int childCount = getChildrenCount(groupPosition);

        if (childPosition == childCount - 1) {
            return STATE_VISIBLE_PART;
        } else if (childPosition == -1 && !mTreeView.isGroupExpanded(groupPosition)) {
            return STATE_GONE;
        } else {
            return STATE_VISIBLE_ALL;
        }
    }

    @Override
    public void onHeaderClick(int groupPosition, int status) {
        mGroupStatusArray.put(groupPosition, status);
    }

    @Override
    public int getHeaderClickStatus(int groupPosition) {
        return mGroupStatusArray.get(groupPosition, STATE_GONE);
    }
}

主要方法:getHeaderState方法,當childPosition==childCount-1條件的時候(及 HeaderView處於顯示且需要向上推起的臨界狀態)時返回STATE_VISIBLE_PART

3、TreeView

package com.markmao.treeview.widget;

import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AbsListView.OnScrollListener;
import android.widget.ExpandableListAdapter;
import android.widget.ExpandableListView;
import android.widget.ExpandableListView.OnGroupClickListener;

/**
 * This widget extends {@link android.widget.ExpandableListView}, just like TreeView(IOS).
 *
 * @see android.widget.ExpandableListView
 * @author markmjw
 * @date 2014-01-03
 */
public class TreeView extends ExpandableListView implements OnScrollListener, OnGroupClickListener {
    private static final int MAX_ALPHA = 255;

    private ITreeViewHeaderUpdater mUpdater;

    private View mHeaderView;

    private boolean mHeaderVisible;

    private int mHeaderWidth;

    private int mHeaderHeight;

    public TreeView(Context context) {
        super(context);
        init();
    }

    public TreeView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public TreeView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init();
    }

    private void init() {
        setSmoothScrollbarEnabled(true);

        setOnScrollListener(this);
        setOnGroupClickListener(this);
    }

    /**
     * Sets the list header view
     *
     * @param view
     */
    public void setHeaderView(View view) {
        mHeaderView = view;
        AbsListView.LayoutParams lp = new AbsListView.LayoutParams(ViewGroup.LayoutParams
                .MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT);
        view.setLayoutParams(lp);

        if (mHeaderView != null) {
            setFadingEdgeLength(0);
        }

        requestLayout();
    }

    @Override
    public void setAdapter(ExpandableListAdapter adapter) {
        super.setAdapter(adapter);

        if(adapter instanceof ITreeViewHeaderUpdater) {
            mUpdater = (ITreeViewHeaderUpdater) adapter;
        } else {
            throw new IllegalArgumentException("The adapter must instanceof ITreeViewHeaderUpdater.");
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent ev) {
        // header view is visible
        if (mHeaderVisible) {
            float downX = 0;
            float downY = 0;

            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    downX = ev.getX();
                    downY = ev.getY();
                    if (downX <= mHeaderWidth && downY <= mHeaderHeight) {
                        return true;
                    }
                    break;

                case MotionEvent.ACTION_UP:
                    float x = ev.getX();
                    float y = ev.getY();
                    float offsetX = Math.abs(x - downX);
                    float offsetY = Math.abs(y - downY);
                    // the touch event under header view
                    if (x <= mHeaderWidth && y <= mHeaderHeight && offsetX <= mHeaderWidth &&
                            offsetY <= mHeaderHeight) {
                        if (mHeaderView != null) {
                            onHeaderViewClick();
                        }

                        return true;
                    }
                    break;

                default:
                    break;
            }
        }

        return super.onTouchEvent(ev);
    }

    @Override
    public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition, long id) {
        int status = mUpdater.getHeaderClickStatus(groupPosition);

        switch (status) {
            case ITreeViewHeaderUpdater.STATE_GONE:
                mUpdater.onHeaderClick(groupPosition, ITreeViewHeaderUpdater.STATE_VISIBLE_ALL);
                break;

            case ITreeViewHeaderUpdater.STATE_VISIBLE_ALL:
                mUpdater.onHeaderClick(groupPosition, ITreeViewHeaderUpdater.STATE_GONE);
                break;

            case ITreeViewHeaderUpdater.STATE_VISIBLE_PART:
                // ignore
                break;

            default:
                break;
        }

        return false;
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);

        if (mHeaderView != null) {
            measureChild(mHeaderView, widthMeasureSpec, heightMeasureSpec);
            mHeaderWidth = mHeaderView.getMeasuredWidth();
            mHeaderHeight = mHeaderView.getMeasuredHeight();
        }
    }

    private int mOldState = -1;

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);

        final long listPosition = getExpandableListPosition(getFirstVisiblePosition());
        final int groupPos = ExpandableListView.getPackedPositionGroup(listPosition);
        final int childPos = ExpandableListView.getPackedPositionChild(listPosition);
        Log.v("TreeView--onlayout分析:",String.format("返回所選擇的List %d,返回所選擇的組項 %d,返回所選擇的子項 %d",(int)listPosition,groupPos,childPos));
        int state = mUpdater.getHeaderState(groupPos, childPos);
        if (mHeaderView != null && mUpdater != null && state != mOldState) {
            mOldState = state;
            mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);
        }

        updateHeaderView(groupPos, childPos);

    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        super.dispatchDraw(canvas);
        if (mHeaderVisible) {
            // draw header view
            drawChild(canvas, mHeaderView, getDrawingTime());
            Log.v("TreeView--dispatchDraw分析:","重新繪制mHeaderView");
        }
    }

    @Override
    public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
                         int totalItemCount) {
        final long listPosition = getExpandableListPosition(firstVisibleItem);
        int groupPos = ExpandableListView.getPackedPositionGroup(listPosition);
        int childPos = ExpandableListView.getPackedPositionChild(listPosition);

        updateHeaderView(groupPos, childPos);
    }

    @Override
    public void onScrollStateChanged(AbsListView view, int scrollState) {

    }

    private void updateHeaderView(int groupPosition, int childPosition) {
        if (mHeaderView == null || mUpdater == null || ((ExpandableListAdapter) mUpdater)
                .getGroupCount() == 0) {
            return;
        }

        int state = mUpdater.getHeaderState(groupPosition, childPosition);

        switch (state) {
            case ITreeViewHeaderUpdater.STATE_GONE: {
                mHeaderVisible = false;
                break;
            }

            case ITreeViewHeaderUpdater.STATE_VISIBLE_ALL: {
                mUpdater.updateHeader(mHeaderView, groupPosition, childPosition, MAX_ALPHA);

                if (mHeaderView.getTop() != 0) {
                    mHeaderView.layout(0, 0, mHeaderWidth, mHeaderHeight);
                }

                mHeaderVisible = true;
                break;
            }

            case ITreeViewHeaderUpdater.STATE_VISIBLE_PART: {
                // a part of header view visible
                View firstView = getChildAt(0);
                int bottom = null != firstView ? firstView.getBottom() : 0;

                int headerHeight = mHeaderView.getHeight();
                int topY;
                int alpha;

                if (bottom < headerHeight) {
                    topY = (bottom - headerHeight);
                    alpha = MAX_ALPHA * (headerHeight + topY) / headerHeight;
                } else {
                    topY = 0;
                    alpha = MAX_ALPHA;
                }

                mUpdater.updateHeader(mHeaderView, groupPosition, childPosition, alpha);
                Log.v("TreeView--updateHeaderView分析:",String.format("bottom=%d,headerHeight=%d,topY=%d,getTop=%d,Header Push up",bottom,headerHeight,topY,
                        mHeaderView.getTop()));
                if (mHeaderView.getTop() != topY) {
                    mHeaderView.layout(0, topY, mHeaderWidth, mHeaderHeight + topY);
                }
                mHeaderVisible = true;
                break;
            }
        }
    }

    private void onHeaderViewClick() {
        long packedPosition = getExpandableListPosition(getFirstVisiblePosition());

        int groupPosition = ExpandableListView.getPackedPositionGroup(packedPosition);

        int status = mUpdater.getHeaderClickStatus(groupPosition);
        if (ITreeViewHeaderUpdater.STATE_VISIBLE_ALL == status) {
            collapseGroup(groupPosition);
            mUpdater.onHeaderClick(groupPosition, ITreeViewHeaderUpdater.STATE_GONE);
        } else {
            expandGroup(groupPosition);
            mUpdater.onHeaderClick(groupPosition, ITreeViewHeaderUpdater.STATE_VISIBLE_ALL);
        }

        setSelectedGroup(groupPosition);
    }
}

①、在setHeaderView方法中,調用requestLayout()方法,請求重新布局,該方法執行後,將分別調用onMeasure()、onLayout()和onDraw()方法 ②、onMeasuer方法,在該方法中將測量mHeaderView的寬度和高度, 並保存在mHeaderWidth和mHeaderHeight的成員變量中 ③、onLayout方法,在該方法中,處理mHeadderView的重新布局問題,當屏幕可見的第一個Group所在位置處的getHeaderState狀態改變的時候則重新布局mHeaderView在屏幕的最頂端,並在此方法中調用updateHeaderView方法。 ④、updateHeaderView方法, 需要註意的是在此方法中,在STATE_VISIABLE_PART狀態中,有段代碼如下
if (mHeaderView.getTop() != topY) {
    mHeaderView.layout(0, topY, mHeaderWidth, mHeaderHeight + topY);
}

這段代碼起到了到達臨界狀態時候,起到推送mHeaderView上滑動的作用

⑤、onTouchEvent方法,TreeView中實現該方法,根據事件分發機制適時的捕獲用戶點擊事件,調用onHeaderViewClick方法,起到點擊GroupItem以展開和收起對應組的效果。

三、效果圖 技術分享 技術分享

動手分析安卓仿QQ聯系人列表TreeView控件