蓄謀已久的列表控件

之前分析了 事件分布ListView復用機制 ,不能只分析不使用把,這次就用之前分析的知識來完成一個自定義列表控件
首先看一下效果,結合了ListView的復用機制以及觸摸事件的使用。

GIF.gif

首先我們需要實現一個靜態的頁面效果。
base.jpg

他分為四部分,左上角是怎么滑動都不會動的,上和左各有一個首行只可以單向滑動,而藍色部分是可以上下左右,甚至斜著都可以,而且在實現靜態頁面的同是我們利用學過的ListView源碼里的邏輯可以實現只加載屏幕內顯示的View,所以不論有多少數據,我們都不用擔心內存問題。
首先我們看一下需要用到的變量都是干什么的

    private BaseTableAdapter adapter;

    private int downX;//滑動時手指落下的X Y
    private int downY;
    private int scrollX;//滑動的距離
    private int scrollY;
    private int firstRow;//當前第一行postiton
    private int firstColumn; //當前第一列position
    private int[] widths;//存放每個View的寬高
    private int[] heights;

    @SuppressWarnings("unused")
    private View headView;//頭View 為使用
    private List<View> rowViewList;//保存一行數據 因為在滑動是可能一行數據直接就滑上去了
    private List<View> columnViewList;
    private List<List<View>> bodyViewTable;//表格數據
    private int rowCount;//行數
    private int columnCount;//列數
    private int width;//控件寬高
    private int height;
    private final ImageView[] shadows;//分割的黑線
    private final int shadowSize;//黑線寬度

    private int minimumVelocity;//慣性滑動時最小和最大速率
    private int maximumVelocity;
    private final Flinger flinger;//慣性滑動
    private VelocityTracker velocityTracker;//慣性滑動

    private boolean needRelayout;    //需要重繪標志位
    private int touchSlop;    //滑動最小距離
    private Recycler recycler;//復用相關類

接下來按一下onMeasure方法。

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);

        final int w;
        final int h;

        if (adapter != null) {
            this.rowCount = adapter.getRowCount();//獲取數據個數
            this.columnCount = adapter.getColumnCount();
            //
            widths = new int[columnCount + 1];//初始化保存的數組 這里+1 是包括了有一個單向滑動的頭部。
            for (int i = -1; i < columnCount; i++) {//這里從-1開始是為了可以添加columnCount + 1條數據
                widths[i + 1] += adapter.getWidth(i);
            }
            heights = new int[rowCount + 1];
            for (int i = -1; i < rowCount; i++) {
                heights[i + 1] += adapter.getHeight(i);
            }

            if (widthMode == MeasureSpec.AT_MOST) {//AT_MOST wrap_content
                //sumArray方法是計算出數組的總和
                w = Math.min(widthSize, sumArray(widths));//判讀屏幕寬度和數據寬度,取最小的
            } else if (widthMode == MeasureSpec.UNSPECIFIED) {
                w = sumArray(widths);
            } else {//具體指或match_parent
                w = widthSize;
                int sumArray = sumArray(widths);
                if (sumArray < widthSize) {//如果 現有view的寬度小于 屏幕寬度 將會把屏幕寬度平分
                    final float factor = widthSize / (float) sumArray;
                    for (int i = 1; i < widths.length; i++) {
                        widths[i] = Math.round(widths[i] * factor);
                    }
                    widths[0] = widthSize - sumArray(widths, 1, widths.length - 1);
                }
            }

            if (heightMode == MeasureSpec.AT_MOST) {
                h = Math.min(heightSize, sumArray(heights));
            } else if (heightMode == MeasureSpec.UNSPECIFIED) {
                h = sumArray(heights);
            } else {
                h = heightSize;
            }
        } else {
            if (heightMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED) {
                w = 0;
                h = 0;
            } else {
                w = widthSize;
                h = heightSize;
            }
        }
        //必須調用
        setMeasuredDimension(w, h);
    }

通過onMeasure我們不僅適配了屏幕,而且還獲取了每個View的寬高,這樣任由我們擺放了,所以接下來看一下onLayout方法。

    @SuppressLint("DrawAllocation")
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        if (needRelayout || changed) {
            needRelayout = false;
            resetTable();//情況所有的集合和View

            if (adapter != null) {
                width = r - l;//屏幕當前的寬度和高度
                height = b - t;
                //畫那四條黑線,在實現靜態頁面是還沒什么用
                int left, top, right, bottom;

                right = Math.min(width, sumArray(widths));
                bottom = Math.min(height, sumArray(heights));
                addShadow(shadows[0], widths[0], 0, widths[0] + shadowSize, bottom);
                addShadow(shadows[1], 0, heights[0], right, heights[0] + shadowSize);
                addShadow(shadows[2], right - shadowSize, 0, right, bottom);
                addShadow(shadows[3], 0, bottom - shadowSize, right, bottom);
                //畫左上角那個固定的ViewItem(紅色部分)
                headView = makeAndSetup(-1, -1, 0, 0, widths[0], heights[0]);
                //畫除左上角以外的第一行數據(橘黃色部分)
                left = widths[0] ;
                //這里用到了源碼里的機制,只加載屏幕以內的View
                //當left(當前View的左邊<屏幕的寬度才去加載)
                for (int i = firstColumn; i < columnCount && left < width; i++) {
                    //不停的去找下個View的左右邊的值
                    right = left + widths[i + 1];
                    final View view = makeAndSetup(-1, i, left, 0, right, heights[0]);
                    rowViewList.add(view);//保存第一行數據
                    left = right;
                }
                //畫除左上角以外的第一列數據(棕色部分)
                top = heights[0] ;
                for (int i = firstRow; i < rowCount && top < height; i++) {
                    bottom = top + heights[i + 1];
                    final View view = makeAndSetup(i, -1, 0, top, widths[0], bottom);
                    columnViewList.add(view);
                    top = bottom;
                }
                //畫Body部分(藍色部分)
                top = heights[0];
                for (int i = firstRow; i < rowCount && top < height; i++) {
                    bottom = top + heights[i + 1];
                    left = widths[0] - scrollX;
                    List<View> list = new ArrayList<View>();
                    for (int j = firstColumn; j < columnCount && left < width; j++) {
                        right = left + widths[j + 1];
                        final View view = makeAndSetup(i, j, left, top, right, bottom);
                        list.add(view);//當前行 一個一個添加  最后相當于一行數據
                        left = right;
                    }
                    bodyViewTable.add(list);//添加一行數據 最后相當于 表格內所有數據
                    top = bottom;
                }

                shadowsVisibility();//分割的黑線
            }
        }
    }

這里突出了靜態時的一個關鍵點,就是只加載當前頁面內的數據,優化效率非常明顯。之后只要設置數據就可以正常顯示了,具體看 源碼,這里我們看一下優化的效率。首先我們改變一下代碼

//畫Body部分(藍色部分)
for (int i = firstRow; i < rowCount ; i++) {
...
    for (int j = firstColumn; j < columnCount ; j++) {
      ...
    }
...
} 

添加藍色區域View的時候 我們把屏幕限制條件刪除,并且我們隔八秒后添加1億跳數據,我們看一下內存狀況。


memory1.gif

寶寶表示震精了~我們在看一下添加上限制條件后是什么情況。

//畫Body部分(藍色部分)
for (int i = firstRow; i < rowCount && top < height ; i++) {
...
    for (int j = firstColumn; j < columnCount  && left < width ; j++) {
      ...
    }
...
} 
memory2.gif

效果很明顯,在第8秒時內存只是增加了一點,將屏幕填滿了,之后就再也沒有變化,靜態的效果我們已經達到了,之后就是滑動時對View的分離以及復用的操作。首先我們需要了解一下復用類。

public class Recycler {
    private Stack<View>[] views;
    public Recycler(int type) {
        views=new Stack[type];
        for (int i = 0; i < type; i++) {
            views[i]=new Stack<View>();
        }
    }
    public void addRecycledView(View view,int type){//滑動時就會調用
        views[type].push(view);//根據ItemType添加View
    }
    public View getRecyclerView(int type){//添加View的時候調用(靜態頁面第一次添加View也會調用)
        try {//一定要try catch 因為type第一次出現時可能還沒有添加過
            return views[type].pop();//根據ItemType拿到View
        } catch (Exception e) {
            return null;
        }
    }
}

這個類其實就是對View的一個Item滑出屏幕時需要添加到這個數組里,item出現是需要判斷之前是否有緩存過。在添加View的時候就會起到非常大的優化作用。
然后就開始實現滑動效果,這里主要會將觸摸事件攔截,以及滑動時臨界值的計算。首先看一下攔截事件。

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = false;
        switch (ev.getAction()) {
            case MotionEvent.ACTION_DOWN:
                downX = (int) ev.getRawX();
                downY = (int) ev.getRawY();
                break;
            case MotionEvent.ACTION_MOVE:
                //攔截move事件  防止子view中有Button一類的控件
                int x2 =  Math.abs(downX - (int)ev.getRawX());
                int y2 =  Math.abs(downY - (int)ev.getRawY());
                //touchSlop是用來判斷是否是一個合理的滑動
                //因為一般情況只要我們手指按下去,不發生Move事件的情況很少很少,總要動一點點的,可能我們都沒察覺自己動了。
                //這里給一個滑動最小距離,大于這個最小距離才算是滑動。
                if (x2 > touchSlop || y2 > touchSlop) {
                    intercept = true;
                }
                break;
        }
        return intercept;
    }

攔截事件很簡單,防止子View中有Button一類的控件,如果沒有就可以不用攔截。重點還是在onTouchEvent()的Move事件。

    @Override
    public void scrollBy(int x, int y) {
        scrollX += x;
        scrollY += y;
        if (needRelayout) {
            return;
        }
        scrollBounds();

        if (scrollX == 0) {
            // no op
        } else if (scrollX > 0) {//向左滑動
            //當scrollX大于body(藍色區域)內第一個可見View的寬度的時候
            //這里用while是有可能快速移動,直接處理多個View的情況
            while (widths[firstColumn + 1] < scrollX) {
                if (!rowViewList.isEmpty()) {
                    removeLeft();
                }
                scrollX -= widths[firstColumn + 1];
                firstColumn++;
            }
            //如果不快速滑動 這里的rowViewList可以理解為body中可見的View
            //這里的getFilledWidth()其實就是計算出第一列(單向滑動的那列)的寬度+body(藍色區域)內rowViewList的中保存的所有View的寬度(有可能首尾的View超出去一部分或超出多個View,那部分也算)-scrollX
            //所以這里計算的就是body(藍色區域)左邊到rowViewList的中保存的最后一個View的寬度(最后一個View超出屏幕部分也算)。因為scrollX就是向左滑了的部分,也就是左邊超出的部分
            //這里用while是有可能快速移動,直接處理多個View的情況
            while (getFilledWidth() < width) {//這里的判斷就是當把最后一個View超出屏幕的部分全部移回來了,就是添加下個view的時候
                addRight();
            }
        } else {//向右滑動第一個View全部出現時調用一次
            //往右滑的時候scrollX是負的。所以getFilledWidth()里的-scrollX 成了 +|scrollX|
            //和上邊的一樣getFilledWidth()計算的是第一列(單向滑動的那列)的寬度+body(藍色區域)的第一個view到rowViewList的中保存的最后一個View的寬度(最后一個View超出屏幕部分也算)
            //因為只有在右滑時第一個View全部出現的時候調用一次,所以這里body(藍色區域)的第一個view就相當于從body的左邊開始
            //這里判斷就相當于:可見的第一個View到rowViewList的中保存的最后一個
            while (!rowViewList.isEmpty() && getFilledWidth() - widths[firstColumn + rowViewList.size()] >= width) {
                removeRight();
            }
            //當scrollX小于0的時候證明已經 將之前左滑的部分又向右滑回來了
            while (0 > scrollX) {
                addLeft();
                firstColumn--;
                scrollX += widths[firstColumn + 1];
            }
        }
        ...
        repositionViews();//沒有這個體現不出滑動的效果

        shadowsVisibility();//分割線
    }

我在這里只分析了左右滑動的臨界值的計算,說實話有點燒腦,不過自己多嘗試兩遍還是可以理解的。注釋中基本把所有的理解都寫了。提示一點注釋用到rowViewList的地方如果不快速滑動,可以理解為當前行可見View的一個集合。
最后我感覺可以用 源碼 來理解會更方便一些。

這篇文章是在我學習的基礎上進行了總結,可想而知我還是個很小的菜鳥,如果其中有錯誤還請指出,我會盡快修改文章,并改正自己的理解,謝謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,227評論 25 708
  • 接上一篇:Android藝術開發探索第三章————View的事件體系(上) 3.4 View 的事件分發機制 本節...
    kongjn閱讀 1,135評論 1 0
  • 第3章 View的事件體系 [TOC] 3.1 View基礎知識 1. View的位置參數 首先來認識一下View...
    反復橫跳的龍套閱讀 1,010評論 0 5
  • 一天,喂兒子吃飯(孩子自立能力讓我培養的超差)。“媽媽,你炒菜花第一名,太美味了。”兒子認真的夸獎我。 我大笑他的...
    癡行人閱讀 237評論 2 5
  • 8???20日,也不覺得今天??多特別。早上路過花店,突然被路旁五顏六色的玫瑰??給吸引住,身子忍不住的就隨著腳步...
    隨心語錄閱讀 1,568評論 3 3