《Android 群英传》学习笔记(Day1)——自定义 ViewGroup – 热爱改变生活
我的GitHub GitHub |     登录
  • If you can't fly, then run; if you can't run, then walk; if you can't walk, then crawl
  • but whatever you do, you have to keep moving forward。
  • “你骗得了我有什么用,这是你自己的人生”
  • 曾有伤心之地,入梦如听 此歌

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

Android组件 sinvader 4089℃ 0评论

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

¥ 有帮助么?打赏一下~

转载请注明:热爱改变生活.cn » 《Android 群英传》学习笔记(Day1)——自定义 ViewGroup


本博客只要没有注明“转”,那么均为原创。 转载请注明链接:sumile.cn » 《Android 群英传》学习笔记(Day1)——自定义 ViewGroup

喜欢 (0)
发表我的评论
取消评论
表情

如需邮件形式接收回复,请注册登录

Hi,你需要填写昵称和邮箱~

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址