《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.ACTION_UP中计算滚动距离的时候使用的是mEnd-mStart,但是这样会有问题:用户下拉了一段距离,但是没有超过三分之一,手松开(此时,view开始向原始位置滑动),然后立刻按下(此时滑动停止,在ACTION_DOWN中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

-------------本文结束  感谢您的阅读-------------
下次一定