说明:文章来自《Android群英传》学习笔记
自定义ViewGroup
今天我们来学习如何创建自定义ViewGroup,自定义ViewGroup通常需要重写onMeasure()方法来对子View进行测量,重写onLayout()方法来确定子View的位置,重写onTouchEvent()方法增加相应事件。
本例实现一个类似Android 原生控件 ScrollView 的自定义 ViewGroup,自定义ViewGroup可以实现ScrollView 所具有的上下滑动功能,滑动的过程中,增加一个黏性的效果,即当一个子View向上/下 滑动大于一定的距离后,松开手指,他将自动向上/下 滑动,显示上/下一个View。
在ViewGroup能够滚动之前,需要先放置好它的子View,使用遍历的方式来通知子View对自身进行测量:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//对子View进行测量
int count = getChildCount();//获取子View的个数
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);//获取当前View
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
接下来,就要对子View进行放置位置的设置,让每个子View都显示完整的一屏,这样在滑动的时候,可以比较好的实现后面的效果。在放置子View前,需要确定整个ViewGroup的高度。在本例中一个子View占一屏的高度,因此整个ViewGroup的高度即子View的个数乘以屏幕的高度。
在获取整个 ViewGroup的高度之后,就可以通过遍历来设定每个子View需要放置的位置了,直接通过调用子View的layout()方法,并将具体的位置作为参数传递进去即可。
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//在放置子View前,需要确定整个ViewGroup的高度
//我们这里一个子View占一个屏幕的高度,因此整个View的高度即子View的个数乘以屏幕的高度
int childCount = getChildCount();
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount ;
setLayoutParams(mlp);
for (int j = 0; j < childCount; j++) {
View childView = getChildAt(j);
if (childView.getVisibility() != View.GONE){
//对子View进行放置位置的设定
childView.layout(l,j*mScreenHeight,r,(j+1)*mScreenHeight);
}
}
}
通过上面的步骤,就可以将子View放置到ViewGroup中了,但此时的ViewGroup还不能相应任何触控事件,自然也不能滑动,因此我们需要重写onTouchEvent()方法,为ViewGroup添加响应事件,在ViewGroup中添加滑动事件,通常可以使用scrollBy()方法来辅助滑动。在onTouchEvent()的 ACTION_DOWN 事件中,只要使用scrollBy(0,dy)方法,让手指滑动的时候让ViewGroup中的所有子View也跟着滚动dy即可。
case MotionEvent.ACTION_DOWN:
mLastY = y ;
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y ;//滑动的距离
//getScrollY表示Y轴方向的偏移量,
//如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
//判断其是否小于0或者大于屏幕高度,就不进行滑动
if (getScrollY() < 0){
dy = 0 ; // View已经滑动到最上端
}
if(getScrollY() > getHeight() - mScreenHeight){
dy = 0 ; // View已经滑动到最下端
}
scrollBy(0 , dy);
mLastY = y ;
break;
按如上方法操作就可以实现类似ScrollView的滚动效果了,最后我们实现这个自定义ViewGroup的黏性效果,要实现手指离开后ViewGroup黏性效果,我们自然的想到onTouchEvent()的ACTION_UP事件和Scroller类。在ACTION_UP事件中判断手指滑动的距离,如果超过一定的距离,使用Scroller 类来平滑移动到下一个View,否则移动到原来的位置。
case MotionEvent.ACTION_DOWN:
mLastY = y ;
mStart = getScrollY();//记录触摸起点
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();//记录触摸终点
int dScrollY = mEnd - mStart;
if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
if (dScrollY < mScreenHeight/3){
//滑动过屏幕的 1/3
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
}
}else{////向上滑动(界面显示上面的内容)
if (-dScrollY < mScreenHeight/3){
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
}
}
break;
这里还有一种翻页判断,就是检测我们滑动的速率,当我们手指滑动的速度大于某一个界限值的时候,就滑动到下一个View。这里android为我们提供了一个类:VelocityTracker,通过这个类,我们可以获取到我们手指滑动的速度:
使用方法如下:
//重写onTouchEvent() 为ViewGrounp添加相应事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
obtainVelocity(event);//初始化加速度检测器
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y ;
mStart = getScrollY();//记录触摸起点
L.i("mStart=="+mStart);
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y ;//滑动的距离
//getScrollY表示Y轴方向的偏移量,
//如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
//判断其是否小于0或者大于屏幕高度,就不进行滑动
L.i("getScrollY=="+getScrollY());
if (getScrollY() < 0){
dy = 0 ; // View已经滑动到最上端
}
if(getScrollY() > getHeight() - mScreenHeight){
dy = 0 ; // View已经滑动到最下端
}
scrollBy(0 , dy);
mLastY = y ;
L.i("getVelocity()=="+getVelocity());
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();//记录触摸终点
//设置移动速度单位为:像素/10000ms,即1000毫秒内移动的像素
int dScrollY = mEnd - mStart;
if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
if (dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
//滑动过屏幕的 1/3
mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
}else {
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}
}else{////向上滑动(界面显示上面的内容)
if (-dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}
}
break;
}
postInvalidate();
recycleVelocity();
return true;
}
/**
* 初始化加速度检测器
*/
private void obtainVelocity(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
/**
* 获取y方向的加速度
*/
private int getVelocity(){
mVelocityTracker.computeCurrentVelocity(1000);
return (int) mVelocityTracker.getYVelocity();
}
/**
* 释放资源
*/
private void recycleVelocity(){
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
测试发现这个getVelocity()在ACTION_UP中获取不到滑动的值,所以只能在ACTION_MOVE中去获取,这里也是有瑕疵的,ACTION_MOVE中测试最后的几次回调,获取到的滑动速率的值可能为0,所以这就需要我们自己修改方法,去掉最后的几次回调,这个我相信大家都有很多方法处理,我就不修改代码的。如果各位有其他的好的想法,可以留言,大家共同学习。
最后给出完整的代码:
package com.android.customview;
import android.content.Context;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Scroller;
/*
* 项目名: CustomView
* 包名: com.android.customview
* 文件名: CustomViewGroup
* 创建者: zdd
* 创建时间: 2018/6/28 10:14
* 描述: 自定义ViewGroup
*/
public class CustomViewGroup extends ViewGroup {
private int mScreenHeight = 0;
private int mScreenWidth = 0;
private int mStart,mEnd;//触摸的起点位置和终点位置
private int mLastY;
// 滚动的辅助类
private Scroller mScroller;
//加速度检测
private VelocityTracker mVelocityTracker;
public CustomViewGroup(Context context) {
super(context);
}
public CustomViewGroup(Context context, AttributeSet attrs) {
super(context, attrs);
getScreenSize(context);
mScroller = new Scroller(context);
}
private void getScreenSize(Context context){
WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
DisplayMetrics outMetrics = new DisplayMetrics();
manager.getDefaultDisplay().getMetrics(outMetrics);
mScreenHeight = outMetrics.heightPixels;
mScreenWidth = outMetrics.widthPixels;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
//对子View进行测量
int count = getChildCount();//获取子View的个数
for (int i = 0; i < count; i++) {
View childView = getChildAt(i);//获取当前View
measureChild(childView,widthMeasureSpec,heightMeasureSpec);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//在放置子View前,需要确定整个ViewGroup的高度
//我们这里一个子View占一个屏幕的高度,因此整个View的高度即子View的个数乘以屏幕的高度
int childCount = getChildCount();
MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams();
mlp.height = mScreenHeight * childCount ;
setLayoutParams(mlp);
for (int j = 0; j < childCount; j++) {
View childView = getChildAt(j);
if (childView.getVisibility() != View.GONE){
//对子View进行放置位置的设定
childView.layout(l,j*mScreenHeight,r,(j+1)*mScreenHeight);
}
}
}
//重写onTouchEvent() 为ViewGrounp添加相应事件
@Override
public boolean onTouchEvent(MotionEvent event) {
int y = (int) event.getY();
L.i("Y=="+y);
obtainVelocity(event);
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
mLastY = y ;
mStart = getScrollY();//记录触摸起点
L.i("mStart=="+mStart);
break;
case MotionEvent.ACTION_MOVE:
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
int dy = mLastY - y ;//滑动的距离
L.i("dy=="+dy);
//getScrollY表示Y轴方向的偏移量,
//如果原始值为0,内容向上移动为正(即手指向上滑),否则为负
//判断其是否小于0或者大于屏幕高度,就不进行滑动
L.i("getScrollY=="+getScrollY());
if (getScrollY() < 0){
dy = 0 ; // View已经滑动到最上端
}
L.i("getHeight()=="+getHeight());
if(getScrollY() > getHeight() - mScreenHeight){
dy = 0 ; // View已经滑动到最下端
}
scrollBy(0 , dy);
mLastY = y ;
L.i("getVelocity()=="+getVelocity());
break;
case MotionEvent.ACTION_UP:
mEnd = getScrollY();//记录触摸终点
L.i("mEnd=="+mEnd);
//设置移动速度单位为:像素/10000ms,即1000毫秒内移动的像素
mVelocityTracker.computeCurrentVelocity(1000);
//获取手指在界面滑动的速度。
int velocity = (int) mVelocityTracker.getXVelocity();
L.i("velocity==="+velocity);
int dScrollY = mEnd - mStart;
if (dScrollY > 0){//向下滑动,(界面显示下面的内容)
if (dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
//滑动过屏幕的 1/3
mScroller.startScroll(0,getScrollY(),0,mScreenHeight - dScrollY);
}else {
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}
}else{////向上滑动(界面显示上面的内容)
if (-dScrollY > mScreenHeight/3 || Math.abs(getVelocity()) > 600){
mScroller.startScroll(0,getScrollY(),0,-mScreenHeight - dScrollY);
}else{
mScroller.startScroll(0,getScrollY(),0,-dScrollY);
}
}
break;
}
postInvalidate();
recycleVelocity();
return true;
}
@Override
public void computeScroll() {
super.computeScroll();
//判断滚动过程是否完成,如果没有完成,就需要不停的scrollTo下去,
//所以在最后需要加一个invalidate(),这样可以再次触发computScroll,直到滚动已经结束
if (mScroller.computeScrollOffset()){
scrollTo(0,mScroller.getCurrY());
postInvalidate();
}
}
/**
* 初始化加速度检测器
*/
private void obtainVelocity(MotionEvent event) {
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
}
/**
* 获取y方向的加速度
*/
private int getVelocity(){
mVelocityTracker.computeCurrentVelocity(1000);
return (int) mVelocityTracker.getYVelocity();
}
/**
* 释放资源
*/
private void recycleVelocity(){
if (mVelocityTracker != null) {
mVelocityTracker.recycle();
mVelocityTracker = null;
}
}
}