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




你可能感兴趣的:(android)