《Android群英传》学习笔记(Day1)——自定义ViewGroup

1.为什么使用ViewGroup

从它的名字就可以看出来,它是一堆View的集合,它要对它“旗下”所有的view进行管理,包括但不限于测量view的大小、确定view的位置以及给view添加相应事件。

2.本例介绍

本例准备实现的是一个类似于Android的原生控件ScrollView的自定义ViewGroup。效果就好像我们在访问一个介绍网页的时候,看到的那种一页一页往下翻,每一页一个接受图片。当用手或者鼠标拖动页面上翻或者下翻超过了预设的一段距离,松开手指,页面会继续翻过去,就好像它有了一个惯性。同理,如果拖动翻页的距离没有超过预设的距离的话,已经滑动的页面会返回的初始的位置。

3.开写

//初始化View
private void initView(Context context) {
        //获得窗口管理器
        WindowManager wm= (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
        //新建一个DisplayMetrics
        DisplayMetrics dm=new DisplayMetrics();
        //从窗口管理器中获得数据放到DisplayMetrics中
        wm.getDefaultDisplay().getMetrics(dm);
        //获得实际显示的像素值并赋值给mScreenHeight
        mScreenHeight = dm.heightPixels;
        //创建一个滚动类Scroller
        mScroller = new Scroller(context);
    }
上面的代码获得了屏幕的实际像素然后保存了起来,并创建了一个SCroller对象。 然后在onMeasure中测量子view的大小:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //获得子View的个数
    int childCount = getChildCount();
    //遍历测量每一个子view的大小
    for (int i = 0; i < childCount; i++) {
        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    }
}
现在子view的大小已经配置好了,来配置每一个view在界面上的位置并显示
@Override
protected void onLayout(boolean changed,
                        int l, int t, int r, int b) {
    int childCount = getChildCount();
    // 设置ViewGroup的高度
    //MarginLayoutParams主要用于定义和边缘的空白
    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
    //因为每个子view都会独占一个页面,所以viewGroup的总高度就是view的个数乘以屏幕的高度
    mlp.height = mScreenHeight * childCount;
    setLayoutParams(mlp);
    for (int i = 0; i < childCount; i++) {
        //获得所有的子view
        View child = getChildAt(i);
        //如果view的显示状态为gone时,不计算它的位置
        if (child.getVisibility() != View.GONE) {
            //如果view的显示不为gone,计算位置,在界面上显示
            child.layout(l, i * mScreenHeight,
                    r, (i + 1) * mScreenHeight);
        }
    }
}
至此,view就已经显示正常了,可以来看一下layout中的代码
<cn.sumile.systemwidget.SumileScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test1" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test2" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test3" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test4" />

</cn.sumile..systemwidget.SumileScrollView>
可以看到,我们在自定义的view中放了四个ImageView,这四个view就是上面代码里面说的子view,而我们的SumileScrollView就是那个ViewGroup,在onLayout中我们给每个子View设置了独占一页,所以我们当前在界面上也只能看到一张图片。 现在,我们来给它添加点击事件:
@Override
public boolean onTouchEvent(MotionEvent event) {
    //获得最左上角的坐标相对于我们view刚显示出来原点的位置
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录左上角的坐标
            mLastY = y;
            //获得开始时y轴的坐标
            mStart = getScrollY();
            break;
        case MotionEvent.ACTION_MOVE:
            //首先判断mScroller是否已经完成动作了
            if (!mScroller.isFinished()) {
                //没有完成动作的话,立刻停止动画
                mScroller.abortAnimation();
            }
            //当前手指的坐标减去上一次停止时的坐标
            int dy = mLastY - y;
            //如果当前y坐标的位置小于0,滑动的距离设为0(此时的状态应该是显示第一页,并向下拉)
            if (getScrollY() < 0) {
                dy = 0;
            }
            //左上角的坐标大于了父view的高度减去最后一屏的高度(此时的状态应该是显示的是最后一页,并且向上拉)
            if (getScrollY() > getHeight() - mScreenHeight) {
                dy = 0;
            }
            scrollBy(0, dy);
            mLastY = y;
            break;
        case MotionEvent.ACTION_UP:
            //计算Y轴移动距离
            int dScrollY = checkAlignment();
            if (dScrollY > 0) {
                //Y轴移动大于0,说明手是向下滑动的,页面也是往下滑动的
                if (dScrollY < mScreenHeight / 3) {
                    //Y轴滑动的距离小于屏幕的三分之一,页面要回到原始的位置上面去
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    //Y轴滑动的距离大于屏幕的三分之一,页面滑动到下一页
                    mScroller.startScroll(
                            0, getScrollY(),
                            //mScreenHeight - dScrollY    页高减去手动滚动了的距离=应该滚动的距离
                            0, mScreenHeight - dScrollY);
                }
            } else {
                //Y轴移动小于0,说明手是向上滑动的,页面也是往上滑动的
                if (-dScrollY < mScreenHeight / 3) {
                    //Y轴滑动的距离小于屏幕的三分之一,页面要回到原始的位置上面去
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    //Y轴滑动的距离大于屏幕的三分之一,页面滑动到上一页
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -mScreenHeight - dScrollY);
                }
            }
            break;
    }
    postInvalidate();
    return true;
}

private int checkAlignment() {
    int mEnd = getScrollY();
    boolean isUp = ((mEnd - mStart) > 0) ? true : false;
    int lastPrev = mEnd % mScreenHeight;
    int lastNext = mScreenHeight - lastPrev;
    if (isUp) {
        //向上的
        return lastPrev;
    } else {
        return -lastNext;
    }
}
注:原文中在MotionEvent.ACTIONUP中计算滚动距离的时候使用的是mEnd-mStart,但是这样会有问题:用户下拉了一段距离,但是没有超过三分之一,手松开(此时,view开始向原始位置滑动),然后立刻按下(此时滑动停止,在ACTIONDOWN中mStart中被重新设置了值,而这个值并不是此时显示的ImageView的左上角的点),于是,ImageView的显示出现了偏移。

关于checkAlignment中的计算方法:

可以类比为一个数列,0,100,200,300...... 间隔100就是屏幕的高度,通过getScrollY获得了当前左上角的y坐标,判断它离数列中的哪个最近。
首先将当前左上角位置对间距取余,获得的值就是它相对于前面的距离 
int lastPrev=mEnd%mScreenHeight;      此处就是对于0的距离为70
int lastNext=mScreenHeight-lastPrev;        这个获得的就是它对于后面的距离为30
比较30和70的值即可
根据手势的动作是向上还是向下,来判断应该是返回lastPrev还是lastNext,如果是向上的,那么就上面出去的(lastPrev)也就是滑动距离(最开始左上角是0,向上滑动后变成了70,所以应该是正的)
最后,加上下面的代码就可以了
@Override
public void computeScroll() {
    super.computeScroll();
    if (mScroller.computeScrollOffset()) {
        scrollTo(0, mScroller.getCurrY());
        postInvalidate();
    }
}
XML调用的代码:
<com.imooc.systemwidget.SumileScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test1" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test2" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test3" />

    <ImageView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:scaleType="fitXY"
        android:src="@drawable/test4" />

</com.imooc.systemwidget.SumileScrollView>
SumileScrollView的完整代码:
package com.imooc.systemwidget;

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

public class SumileScrollView extends ViewGroup {

private int mScreenHeight;
private Scroller mScroller;
private int mLast;

public SumileScrollView(Context context) {
    this(context, null);
}

public SumileScrollView(Context context, AttributeSet attrs) {
    this(context, attrs, 0);
}

public SumileScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
    initView(context);
}

//初始化View
private void initView(Context context) {
    //获得窗口管理器
    WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
    //新建一个DisplayMetrics
    DisplayMetrics dm = new DisplayMetrics();
    //从窗口管理器中获得数据放到DisplayMetrics中
    wm.getDefaultDisplay().getMetrics(dm);
    //获得实际显示的像素值并赋值给mScreenHeight
    mScreenHeight = dm.heightPixels;
    //创建一个滚动类Scroller
    mScroller = new Scroller(context);
}

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    super.onMeasure(widthMeasureSpec, heightMeasureSpec);
    //获得子View的个数
    int childCount = getChildCount();
    //遍历测量每一个子view的大小
    for (int i = 0; i < childCount; i++) {
        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
    }
}

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    //设置viewGroup的高度
    int childCount = getChildCount();
    MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
    mlp.height = mScreenHeight * childCount;
    setLayoutParams(mlp);
    for (int i = 0; i < childCount; i++) {
        View v = getChildAt(i);
        v.layout(l, i * mScreenHeight, r, (i + 1) * mScreenHeight);
    }
}

private int mStart;
private int mLastY;
private int mEnd;

/**
 * @param event
 * @return
 */
@Override
public boolean onTouchEvent(MotionEvent event) {
    //获得最左上角的坐标相对于我们view刚显示出来原点的位置
    int y = (int) event.getY();
    switch (event.getAction()) {
        case MotionEvent.ACTION_DOWN:
            //记录左上角的坐标
            mLastY = y;
            //获得开始时y轴的坐标
            mStart = getScrollY();
            break;
        case MotionEvent.ACTION_MOVE:
            //首先判断mScroller是否已经完成动作了
            if (!mScroller.isFinished()) {
                //没有完成动作的话,立刻停止动画
                mScroller.abortAnimation();
            }
            //当前手指的坐标减去上一次停止时的坐标
            int dy = mLastY - y;
            //如果当前y坐标的位置小于0,滑动的距离设为0(此时的状态应该是显示第一页,并向下拉)
            if (getScrollY() < 0) {
                dy = 0;
            }
            //左上角的坐标大于了父view的高度减去最后一屏的高度(此时的状态应该是显示的是最后一页,并且向上拉)
            if (getScrollY() > getHeight() - mScreenHeight) {
                dy = 0;
            }
            scrollBy(0, dy);
            mLastY = y;
            break;
        case MotionEvent.ACTION_UP:
            //计算Y轴移动距离
            int dScrollY = checkAlignment();
            if (dScrollY > 0) {
                //Y轴移动大于0,说明手是向下滑动的,页面也是往下滑动的
                if (dScrollY < mScreenHeight / 3) {
                    //Y轴滑动的距离小于屏幕的三分之一,页面要回到原始的位置上面去
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    //Y轴滑动的距离大于屏幕的三分之一,页面滑动到下一页
                    mScroller.startScroll(
                            0, getScrollY(),
                            //mScreenHeight - dScrollY    页高减去手动滚动了的距离=应该滚动的距离
                            0, mScreenHeight - dScrollY);
                }
            } else {
                //Y轴移动小于0,说明手是向上滑动的,页面也是往上滑动的
                if (-dScrollY < mScreenHeight / 3) {
                    //Y轴滑动的距离小于屏幕的三分之一,页面要回到原始的位置上面去
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -dScrollY);
                } else {
                    //Y轴滑动的距离大于屏幕的三分之一,页面滑动到上一页
                    mScroller.startScroll(
                            0, getScrollY(),
                            0, -mScreenHeight - dScrollY);
                }
            }
            break;
    }
    postInvalidate();
    return true;
}

private int checkAlignment() {
    int mEnd = getScrollY();
    boolean isUp = ((mEnd - mStart) > 0) ? true : false;
    int lastPrev = mEnd % mScreenHeight;
    int lastNext = mScreenHeight - lastPrev;
    if (isUp) {
        //向上的
        return lastPrev;
    } else {
        return -lastNext;
    }
}

@Override
public void computeScroll() {
    super.computeScroll();
    if (mScroller.computeScrollOffset()) {
        scrollTo(0, mScroller.getCurrY());
        postInvalidate();
    }
}
}
The End

(转)程序员必须知道的10大基础实用算法及其讲解 2015-12-25
Android webview点击或长按有蒙层 2016-01-19

评论区