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