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





