笔记 Androd 自定义控件学习(五)

说明:文章来自《Android群英传》学习笔记

自定义ViewGroup

今天我们来学习如何创建自定义ViewGroup,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法增加相应事件。

本例实现一个类似Android 原生控件 ScrollView 的自定义 ViewGroup,自定义ViewGroup可以实现ScrollView 所具有的上下滑动功能,滑动的过程中,增加一个黏性的效果,即当一个子View向上/下 滑动大于一定的距离后,松开手指,他将自动向上/下 滑动,显示上/下一个View。

在ViewGroup能够滚动之前,需要先放置好它的子View,使用遍历的方式来通知子View对自身进行测量:

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //对子View进行测量
        int count = getChildCount();//获取子View的个数
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);//获取当前View
            measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }
    }

接下来,就要对子View进行放置位置的设置,让每个子View都显示完整的一屏,这样在滑动的时候,可以比较好的实现后面的效果。在放置子View前,需要确定整个ViewGroup的高度。在本例中一个子View占一屏的高度,因此整个ViewGroup的高度即子View的个数乘以屏幕的高度。
在获取整个 ViewGroup的高度之后,就可以通过遍历来设定每个子View需要放置的位置了,直接通过调用子View的layout()方法,并将具体的位置作为参数传递进去即可。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //在放置子View前,需要确定整个ViewGroup的高度
    //我们这里一个子View占一个屏幕的高度,因此整个View的高度即子View的个数乘以屏幕的高度
    int childCount = getChildCount();
    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
    mlp.height = mScreenHeight * childCount ;
    setLayoutParams(mlp);
    for (int j = 0; j < childCount; j++) {
        View childView = getChildAt(j);
        if (childView.getVisibility() != View.GONE){
            //对子View进行放置位置的设定
            childView.layout(l,j*mScreenHeight,r,(j+1)*mScreenHeight);
        }
     }
 }

通过上面的步骤,就可以将子View放置到ViewGroup中了,但此时的ViewGroup还不能相应任何触控事件,自然也不能滑动,因此我们需要重写onTouchEvent()方法,为ViewGroup添加响应事件,在ViewGroup中添加滑动事件,通常可以使用scrollBy()方法来辅助滑动。在onTouchEvent()的 ACTION_DOWN 事件中,只要使用scrollBy(0,dy)方法,让手指滑动的时候让ViewGroup中的所有子View也跟着滚动dy即可。

case MotionEvent.ACTION_DOWN:
     mLastY = y ;
     break;
case MotionEvent.ACTION_MOVE:
     if(!mScroller.isFinished()){
         mScroller.abortAnimation();
     }
     int dy = mLastY - y ;//滑动的距离
     //getScrollY表示Y轴方向的偏移量,
     //如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
     //判断其是否小于0或者大于屏幕高度,就不进行滑动
     if (getScrollY() < 0){
         dy = 0 ; // View已经滑动到最上端
      }
     if(getScrollY() > getHeight() - mScreenHeight){
         dy = 0 ; // View已经滑动到最下端
      }
     scrollBy(0 , dy);
     mLastY = y ;
     break;

按如上方法操作就可以实现类似ScrollView的滚动效果了,最后我们实现这个自定义ViewGroup的黏性效果,要实现手指离开后ViewGroup黏性效果,我们自然的想到onTouchEvent()的ACTION_UP事件和Scroller类。在ACTION_UP事件中判断手指滑动的距离,如果超过一定的距离,使用Scroller 类来平滑移动到下一个View,否则移动到原来的位置。

case MotionEvent.ACTION_DOWN:
     mLastY = y ;
     mStart = getScrollY();//记录触摸起点
     break;
case MotionEvent.ACTION_UP:
     mEnd = getScrollY();//记录触摸终点
     int dScrollY = mEnd - mStart;
     if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
         if (dScrollY < mScreenHeight/3){
          //滑动过屏幕的 1/3
               mScroller.startScroll(0,getScrollY(),0,-dScrollY);
          }else{
               mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
          }
      }else{////向上滑动(界面显示上面的内容)
          if (-dScrollY < mScreenHeight/3){
               mScroller.startScroll(0,getScrollY(),0,-dScrollY);
           }else{
               mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
           }
       }
      break;

这里还有一种翻页判断,就是检测我们滑动的速率,当我们手指滑动的速度大于某一个界限值的时候,就滑动到下一个View。这里android为我们提供了一个类:VelocityTracker,通过这个类,我们可以获取到我们手指滑动的速度:
使用方法如下:

//重写onTouchEvent() 为ViewGrounp添加相应事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        obtainVelocity(event);//初始化加速度检测器
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastY = y ;
                mStart = getScrollY();//记录触摸起点
                L.i("mStart=="+mStart);
                break;
            case MotionEvent.ACTION_MOVE:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }
                int dy = mLastY - y ;//滑动的距离
                //getScrollY表示Y轴方向的偏移量,
                //如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
                //判断其是否小于0或者大于屏幕高度,就不进行滑动
                L.i("getScrollY=="+getScrollY());
                if (getScrollY() < 0){
                    dy = 0 ; // View已经滑动到最上端
                }
                if(getScrollY() > getHeight() - mScreenHeight){
                    dy = 0 ; // View已经滑动到最下端
                }
                scrollBy(0 , dy);
                mLastY = y ;
                L.i("getVelocity()=="+getVelocity());
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();//记录触摸终点
                //设置移动速度单位为:像素/10000ms,即1000毫秒内移动的像素
                int dScrollY = mEnd - mStart;
                if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
                    if (dScrollY > mScreenHeight/3 ||  Math.abs(getVelocity()) > 600){
                        //滑动过屏幕的 1/3
                        mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
                    }else {
                        mScroller.startScroll(0,getScrollY(),0,-dScrollY);
                    }
                }else{////向上滑动(界面显示上面的内容)
                    if (-dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
                        mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
                    }else{
                        mScroller.startScroll(0,getScrollY(),0,-dScrollY);
                    }
                }
                break;
        }
        postInvalidate();
        recycleVelocity();
        return true;
    }

 /**
  * 初始化加速度检测器
  */
    private void obtainVelocity(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 获取y方向的加速度
     */
    private int getVelocity(){
        mVelocityTracker.computeCurrentVelocity(1000);
        return (int) mVelocityTracker.getYVelocity();
    }

    /**
     * 释放资源
     */
    private void recycleVelocity(){
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

测试发现这个getVelocity()在ACTION_UP中获取不到滑动的值,所以只能在ACTION_MOVE中去获取,这里也是有瑕疵的,ACTION_MOVE中测试最后的几次回调,获取到的滑动速率的值可能为0,所以这就需要我们自己修改方法,去掉最后的几次回调,这个我相信大家都有很多方法处理,我就不修改代码的。如果各位有其他的好的想法,可以留言,大家共同学习。
最后给出完整的代码:

package com.android.customview;

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

/*
 *  项目名:  CustomView 
 *  包名:    com.android.customview
 *  文件名:   CustomViewGroup
 *  创建者:   zdd
 *  创建时间:  2018/6/28 10:14
 *  描述:    自定义ViewGroup
 */
public class CustomViewGroup extends ViewGroup {

    private int mScreenHeight = 0;
    private int mScreenWidth = 0;
    private int mStart,mEnd;//触摸的起点位置和终点位置
    private int mLastY;

    // 滚动的辅助类
    private Scroller mScroller;
    //加速度检测
    private VelocityTracker mVelocityTracker;

    public CustomViewGroup(Context context) {
        super(context);
    }

    public CustomViewGroup(Context context, AttributeSet attrs) {
        super(context, attrs);
        getScreenSize(context);
        mScroller = new Scroller(context);
    }

    private void getScreenSize(Context context){
        WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        manager.getDefaultDisplay().getMetrics(outMetrics);
        mScreenHeight = outMetrics.heightPixels;
        mScreenWidth = outMetrics.widthPixels;       
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        //对子View进行测量
        int count = getChildCount();//获取子View的个数
        for (int i = 0; i < count; i++) {
            View childView = getChildAt(i);//获取当前View
            measureChild(childView,widthMeasureSpec,heightMeasureSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //在放置子View前,需要确定整个ViewGroup的高度
        //我们这里一个子View占一个屏幕的高度,因此整个View的高度即子View的个数乘以屏幕的高度
        int childCount = getChildCount();
        MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
        mlp.height = mScreenHeight * childCount ;
        setLayoutParams(mlp);

        for (int j = 0; j < childCount; j++) {
            View childView = getChildAt(j);
            if (childView.getVisibility() != View.GONE){
                //对子View进行放置位置的设定
                childView.layout(l,j*mScreenHeight,r,(j+1)*mScreenHeight);
            }
        }
    }

    //重写onTouchEvent() 为ViewGrounp添加相应事件
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int y = (int) event.getY();
        L.i("Y=="+y);
        obtainVelocity(event);
        switch (event.getAction()){
            case MotionEvent.ACTION_DOWN:
                mLastY = y ;
                mStart = getScrollY();//记录触摸起点
                L.i("mStart=="+mStart);
                break;
            case MotionEvent.ACTION_MOVE:
                if(!mScroller.isFinished()){
                    mScroller.abortAnimation();
                }

                int dy = mLastY - y ;//滑动的距离
                L.i("dy=="+dy);
                //getScrollY表示Y轴方向的偏移量,
                //如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
                //判断其是否小于0或者大于屏幕高度,就不进行滑动
                L.i("getScrollY=="+getScrollY());
                if (getScrollY() < 0){
                    dy = 0 ; // View已经滑动到最上端
                }
                L.i("getHeight()=="+getHeight());
                if(getScrollY() > getHeight() - mScreenHeight){
                    dy = 0 ; // View已经滑动到最下端
                }
                scrollBy(0 , dy);
                mLastY = y ;

                L.i("getVelocity()=="+getVelocity());
                break;
            case MotionEvent.ACTION_UP:
                mEnd = getScrollY();//记录触摸终点
                L.i("mEnd=="+mEnd);
                //设置移动速度单位为:像素/10000ms,即1000毫秒内移动的像素
                mVelocityTracker.computeCurrentVelocity(1000);
                //获取手指在界面滑动的速度。
                int velocity = (int) mVelocityTracker.getXVelocity();
                L.i("velocity==="+velocity);

                int dScrollY = mEnd - mStart;
                if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
                    if (dScrollY > mScreenHeight/3 ||  Math.abs(getVelocity()) > 600){
                        //滑动过屏幕的 1/3
                        mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
                    }else {
                        mScroller.startScroll(0,getScrollY(),0,-dScrollY);
                    }
                }else{////向上滑动(界面显示上面的内容)
                    if (-dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
                        mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
                    }else{
                        mScroller.startScroll(0,getScrollY(),0,-dScrollY);
                    }
                }
                break;
        }
        postInvalidate();
        recycleVelocity();
        return true;
    }

    @Override
    public void computeScroll() {
        super.computeScroll();
        //判断滚动过程是否完成,如果没有完成,就需要不停的scrollTo下去,
        //所以在最后需要加一个invalidate(),这样可以再次触发computScroll,直到滚动已经结束
        if (mScroller.computeScrollOffset()){
            scrollTo(0,mScroller.getCurrY());
            postInvalidate();
        }
    }

    /**
     * 初始化加速度检测器
     */
    private void obtainVelocity(MotionEvent event) {
        if (mVelocityTracker == null) {
            mVelocityTracker = VelocityTracker.obtain();
        }
        mVelocityTracker.addMovement(event);
    }

    /**
     * 获取y方向的加速度
     */
    private int getVelocity(){
        mVelocityTracker.computeCurrentVelocity(1000);
        return (int) mVelocityTracker.getYVelocity();
    }

    /**
     * 释放资源
     */
    private void recycleVelocity(){
        if (mVelocityTracker != null) {
            mVelocityTracker.recycle();
            mVelocityTracker = null;
        }
    }

}

猜你喜欢

转载自blog.csdn.net/zhu522959034/article/details/80847149