android之自定義viewGroup仿scrollView詳解

相信學了安卓的朋友都知道自定義viewGroup離不開重寫onmeasure()和onLayout(),開始講解代碼之前,先來看看與這兩個方法相關知識:

   一、onMeasure() :這是測量自身的寬高和子view的寬高方法,測量涉及的知識點除了寬高之外,還有三種模式

          (1) 三種模式如下:

               1、MeasureSpec.EXACTLY:精確值模式: 控件的layout_width或layout_heiht指定爲具體值,比如200dp,或者指定爲match_parent(佔據父view的大小),系統返回的是這個模式

               2、MeasureSpec.AT_MOST: 最大值模式,控件的layout_width或layout_heiht指定爲wrap_content時,控件大小一般隨着控件的子控件或內容的變化而變化,此時控件的尺寸不能超過父控件

              3、MeasureSpec.UNSPECIFIED:不指定其大小測量模式,通常在繪製定義view的時候纔會使用,即多大由開發者在onDraw()的時候指定大小

      (2)寬高測量用到的知識點:

                我們都知道onMeasure(int widthMeasureSpec, int heightMeasureSpec)一般用來測量自身寬高和子view的寬高,而其會有兩個參數,這兩個參數就是系統測量好的自身view的寬高,但我們無法直接使用這個寬高,打印日誌我們就知道這個寬高是一長串整型數據並不是設置的寬高比如200px,那麼它們怎麼代表什麼意思?又怎麼使用呢?

               其實: widthMeasureSpec, heightMeasureSpec這兩個參數是32位的,包含兩個意思:

              它們高2位代表前邊所指的三種模式中的一種,由系統測量我們在xml佈局中設置的layout_width和layout_height得出的結果,而低30位就是系統測量該控件的寬高

               那怎麼轉化成我們自身先要的數據呢,其實谷歌給出一個短小精悍的類MeasureSpec類,提供瞭如下兩個方法:

             

              // 獲取自身寬高,返回的就是諸如 200px這樣的寬高,參數widthMeasureSpec正是onmeasure()中的參數

               int measureSelfWidth = MeasureSpec.getSize(widthMeasureSpec);

             //確定該view用的是那種模式,即以上三種模式中的一種
               int mode = MeasureSpec.getMode(widthMeasureSpec)

   二、onLayout():ViewGroup的一個抽象方法,一般在此方法通過計算來設置子view的座標,(子view的位置)

          而返回的參數onLayout(boolean changed, int l, int t, int r, int b),當調用requestLayout(),時changed用以代表此view是否有發生改變,其它參數分別代表着自身的座標(即左上右下)


  三、 ondraw()在viewGroup中不提倡去重寫這個方法,除非是背景什麼的,一般ViewGroup只作爲容器,其他的都由它子view去繪製,所以viewGroup的onDraw()既是view的onDraw()


 四、由於下面將用到Scroller,所以講講它的基本用法
      Scroller是個非常特別的類,view或者ViewGroup經常在滑動的時候會用到它,那麼它是個什麼鬼?又用來幹什麼?

      (1)它是個什麼鬼?

                通過名字不難猜出它跟滑動相關,確實,說白了,它算是一個插值器吧(控制物理速率變化),提供插值計算,讓滾動過程很平滑很動畫,就單純的爲滑動提供計算

      (2) 作用:它是一個輔助類,用以滑動時計算座標起點和始點區間的平滑過渡座標點。

        主要用到的方法: 

    //使用前scroller初始化 x,y表示起點座標,dx,dy表示偏移量,正數代表手指向上滑動,負數代表手指向下滑動

      mScroller.startScroll(x, y, dx, dy);

       //返回的是boolean值,用以判斷是否計算完畢,比如滾動時,用以判斷是否滾動完成

       mScroller.computeScrollOffset();

      //可以獲取從起點到終點區間的過渡座標點
        mScroller.getCurrY();

   一般配合View的一個方法使用:

        computeScroll();//這個方法是個空方法,但在ondraw()的時候會調用,主要是用以配合滑動時使用

            所以當初始化後一定要調用invalidate()或者postInvalidate()方法去執行onDraw()這樣就可以在這個方法處理scroller所需要的邏輯

         注意一點:

         getScrollY()獲取的是滾動到view的起點座標點,所以計算的時候別算錯了

         scrollTo(x,y);//滾動到參數指定的座標即(x,y)

        scrollBy(dx, (int) dy);//滾動到參數指定的偏移量,即當前的座標 (x+dx,y+dy)


好了,基本知識都瞭解了,以下看看簡單的仿造ScrollView的demo,因該就容易理解了,直接上代碼,demo在最後,歡迎下載,有不對的地方請不吝指正

   

package com.example.administrator.customscrollviewdemo;

import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;

/**
 * author : zhongwr on 2016/7/20
 */
public class CustomScrollView extends ViewGroup {
    private static final String TAG = "CustomScrollView";
    private Context mContext;
    private int mScreenHeight;
    private int totalHeight;
    private Scroller mScroller;

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

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

    private void init(Context context) {
        mContext = context;
        mScreenHeight = getScreenSize(mContext).heightPixels;
        mScroller = new Scroller(mContext);
    }

    /***
     * 獲取真實的寬高 比如200px
     *
     * @param widthMeasureSpec
     * @return
     */
    public int measureRealWidth(int widthMeasureSpec) {
        int result = 200;
        int specMode = MeasureSpec.getMode(widthMeasureSpec);
        int realWidth = MeasureSpec.getSize(widthMeasureSpec);
        switch (specMode) {
            case MeasureSpec.EXACTLY:
                //MeasureSpec.EXACTLY:精確值模式: 控件的layout_width或layout_heiht指定爲具體值,比如200dp,或者指定爲match_parent(佔據父view的大小),系統返回的是這個模式
                result = realWidth;
                Log.d(TAG, "EXACTLY result " + result);
                break;
            case MeasureSpec.AT_MOST:
                // MeasureSpec.AT_MOST: 最大值模式,控件的layout_width或layout_heiht指定爲wrap_content時,控件大小一般隨着控件的子控件或內容的變化而變化,此時控件的尺寸不能超過父控件
                result = Math.min(result, realWidth);
                Log.d(TAG, "AT_MOST result " + result);
                break;
            case MeasureSpec.UNSPECIFIED:
                    // MeasureSpec.UNSPECIFIED:不指定其大小測量模式,通常在繪製定義view的時候纔會使用,即多大由開發者在onDraw()的時候指定大小
                result = realWidth;
                Log.d(TAG, "UNSPECIFIED result " + result);
                break;
        }
        return result;
    }

    /***
     * @param widthMeasureSpec  系統測量的寬 一共是32位的 高2位代表模式 低30位表示大小
     * @param heightMeasureSpec 系統測量的高 一共是32位的 高2位代表模式 低30位表示大小
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        Log.d(TAG, "widthMeasureSpec " + widthMeasureSpec);
        Log.d(TAG, "heightMeasureSpec " + heightMeasureSpec);
        /***自身寬*/
        int measureSelfWidth = measureRealWidth(widthMeasureSpec);
        int measureSelfHeight = MeasureSpec.getSize(heightMeasureSpec);
        Log.d(TAG, "widthMeasure " + measureSelfWidth);
        Log.d(TAG, "widthMode " + MeasureSpec.getMode(widthMeasureSpec));
        Log.d(TAG, "heightMeasure " + MeasureSpec.getSize(heightMeasureSpec));
        Log.d(TAG, "heightMode " + MeasureSpec.getMode(heightMeasureSpec));

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            measureChild(childView, widthMeasureSpec, heightMeasureSpec);
        }
        //設置viewGroup的寬高,也可以在onlayout中通過layoutParams設置
        totalHeight = getScreenSize(mContext).heightPixels * childCount;
        setMeasuredDimension(measureSelfWidth, totalHeight);
    }


    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        Log.d(TAG, "onLayout left " + l);
        Log.d(TAG, "onLayout top " + t);
        Log.d(TAG, "onLayout right " + r);
        Log.d(TAG, "onLayout bottom " + b);
        Log.d(TAG, "onLayout heightPixels " + getScreenSize(mContext).heightPixels);
        int childCount = getChildCount();
//        LayoutParams lp = getLayoutParams();
//        totalHeight = getScreenSize(mContext).heightPixels * childCount;
//        lp.height = totalHeight;//設置viewgroup總高度
//        setLayoutParams(lp);

        for (int i = 0; i < childCount; i++) {
            View childView = getChildAt(i);
            childView.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
        }
    }

    private float lastDownY;
    private float mScrollStart;
    private float mScrollEnd;

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                lastDownY = event.getY();
                mScrollStart = getScrollX();
                Log.d(TAG, "totalHeight = " + totalHeight);
                break;
            case MotionEvent.ACTION_MOVE:
                float currentY = event.getY();
                float dy;
                dy = lastDownY - currentY;
                Log.d(TAG, "dy = " + dy);
                Log.d(TAG, "getScrollY() = " + getScrollY());
                Log.d(TAG, "getHeight()  = " + getHeight());
                Log.d(TAG, "getHeight() - mScreenHeight = " + (getHeight() - mScreenHeight));
                if (getScrollY() < 0) {
                    dy = 0;
                    //最頂端,超過0時,不再下拉,要是不設置這個,getScrollY一直是負數
//                    setScrollY(0);
                } else if (getScrollY() > getHeight() - mScreenHeight) {
                    dy = 0;
                    //滑到最底端時,不再滑動,要是不設置這個,getScrollY一直是大於getHeight() - mScreenHeight的數,無法再滑動
//                    setScrollY(getHeight() - mScreenHeight);
                }
                scrollBy(0, (int) dy);
                //不斷的設置Y,在滑動的時候子view就會比較順暢
                lastDownY = event.getY();
                break;
            case MotionEvent.ACTION_UP:
                mScrollEnd = getScrollY();
                int dScrollY = (int) (mScrollEnd - mScrollStart);
                if (mScrollEnd < 0) {// 最頂端:手指向下滑動,回到初始位置
                    Log.d(TAG, "mScrollEnd < 0" + dScrollY);
                    mScroller.startScroll(0, getScrollY(), 0, -getScrollY());
                } else if (mScrollEnd > getHeight() - mScreenHeight) {//已經到最底端,手指向上滑動回到底部位置
                    Log.d(TAG, "getHeight() - mScreenHeight - (int) mScrollEnd " + (getHeight() - mScreenHeight - (int) mScrollEnd));
                    mScroller.startScroll(0, getScrollY(), 0, getHeight() - mScreenHeight - (int) mScrollEnd);
                }
                postInvalidate();// 重繪執行computeScroll()
                break;
        }
        return true;//需要返回true否則down後無法執行move和up操作
    }

    /**
     * Scroller只是個計算器,提供插值計算,讓滾動過程具有動畫屬性,但它並不是UI,也不是滑動輔助UI運動,反而是單純地爲滑動提供計算
     * 需要invalidate()之後纔會調用,這個方法在onDraw()中調用
     */
    @Override
    public void computeScroll() {
        super.computeScroll();
        Log.d(TAG, "mScroller.getCurrY() " + mScroller.getCurrY());
        if (mScroller.computeScrollOffset()) {//是否已經滾動完成
            scrollTo(0, mScroller.getCurrY());//獲取當前值,startScroll()初始化後,調用就能獲取區間值
            postInvalidate();
        }
    }

    /**
     * 獲取屏幕大小,這個可以用一個常量不用每次都獲取
     *
     * @param context
     * @return
     */
    public static DisplayMetrics getScreenSize(Context context) {
        DisplayMetrics metrics = new DisplayMetrics();
        WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        wm.getDefaultDisplay().getMetrics(metrics);
        return metrics;
    }

}

demo: http://download.csdn.net/detail/zhongwn/9582516




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章