看开发艺术探索笔记
用途
- 主要用于实现自定义的布局
- 需要合适的处理measure和Layout这两个过程,并同时处理子元素的测量和布局过程
例子
- 一个HorizontalScrollViewEx控件的简单实现,他的内部子元素可以进行水平滚动,子元素内部的子元素还可以进行垂直滚动
- 假设他的子元素宽高相同,接下来看一下OnMeasure的主要思路
- 先拿到自己布局的长宽属性,然后用measureChildren(widthMeasureSpec,heightMeasureSpec);这个方法去测量自己儿子View的长宽
- 然后再自己去判断自己的长宽属性,如果长是AT_MOST,则表示用的是wrap_content
- 再去拿到子View的数量,根据先前measureChildren方法已经测量好了子View的长宽高,然后获取到自View总共的高度,然后去setMeasureDimension(长,宽)去设置自己(父控件的长或宽)
- 贴一下代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
final int childCount = getChildCount();
//测量自己儿子View的长
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceModel = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceModel = MeasureSpec.getMode(heightMeasureSpec);
//这就是根据父控件的长款模式和子View的总长来确定父控件的最终长宽
if(childCount == 0){
setMeasuredDimension(0,0);
}else if(widthSpaceModel == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth,measureHeight);
} else if(heightSpaceModel == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize,measureHeight);
} else if(widthSpaceModel == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth,heightSpaceSize);
}
}
代码缺点
- 当没有子元素的时候,直接设置为0.0,没有考虑到在xml文件中对这个viewgroup长宽的设置,比如直接设置成准确的数字
- 没有考虑到子元素padding和marge的限制
接下来看布局(layout)部分
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for(int i = 0;i<childCount;i++){
final View childView = getChildAt(i);
if(childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
- 分析代码吧
- 发现其实布局干了一件很简单的事儿,就是遍历一个个子View,然后去给他们安排左上右下这四个坐标点的位置即可,这里需要注意的是只需要关注没有被Gone的子View即可
粘贴完整代码
import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.VelocityTracker;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Scroller;
public class HorizontalScorllViewEx extends ViewGroup {
private int mChildrenSize;
private int mChildWidth;
private int mChildIndex;
//记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//记录上次滑动的坐标(拦截方法中)
private int mLastXIntercept = 0;
private int mLastyIntercept = 0;
//滑动和速度检测
private Scroller mScroller;
private VelocityTracker mVelocityTracker;
public HorizontalScorllViewEx(Context context) {
super(context);
}
public HorizontalScorllViewEx(Context context, AttributeSet attrs) {
super(context, attrs);
}
public HorizontalScorllViewEx(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
private void init(){
if(mScroller == null){
mScroller = new Scroller(getContext());
mVelocityTracker = VelocityTracker.obtain();
}
}
@Override
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//false 表示不拦截
intercepted = false;
if(!mScroller.isFinished()){
mScroller.abortAnimation();
//终止动画,确认拦截点击事件
//这里我猜一下应该是正在滑动,然后手指点击了一下,于是就停止滑动,将手指点击的这次事件认为是终止滑动的事件
intercepted = true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastyIntercept;
if(Math.abs(deltaX) > Math.abs(deltaY)){
//x方向大于y方向的位移,就代表是x方向的滑动,此时拦截
intercepted = true;
}else {
//y方向的滑动,不拦截
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
//手指抬起事件,貌似没啥暖用
intercepted = false;
break;
default:break;
}
//更新上次滑动的坐标
mLastX = x;
mLastyIntercept = x;
mLastY = y;
mLastyIntercept = y;
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
//给速度测量器加入事件,他就会测量这个事件的速度
mVelocityTracker.addMovement(event);
//拿到事件此时的坐标
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
//处理事件
if(!mScroller.isFinished()){
mScroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastY;
int deltaY = y - mLastY;
//scrollBy(int dx, int dy)主要用于滑屏操作,两个参数分别代表滑屏前与滑屏后的坐标之差
//在这里手指按着滑动了多少,就是多少
scrollBy(-deltaX,0);
break;
case MotionEvent.ACTION_UP:
int scrollX = getScrollX();
//设置速度单位,1000表示一秒内的位移
mVelocityTracker.computeCurrentVelocity(1000);
//得到x方向的速度
float xVelocity = mVelocityTracker.getXVelocity();
if(Math.abs(xVelocity) >= 50){
//如果速度大于50,那么就让这次滑动直接向左或者向右滑动一个子View
mChildIndex = xVelocity > 0 ? mChildIndex - 1 : mChildIndex + 1;
} else {
//如果速度小于50,那么就根据此时的位置来确定滑动到上一个还是下一个子View的位置
mChildIndex = (scrollX + mChildWidth / 2) / mChildWidth;
}
//接下来判断是否滑到了最右边,如果是的话,那就滑不过去了
mChildIndex = Math.max(0,Math.min(mChildIndex,mChildrenSize -1 ));
//计算滑动的距离
int dx = mChildIndex * mChildWidth - scrollX;
//有我们自己实现的一个弹性滑动的方法,从当前x出往右滑动dx的距离
smoothScrollBy(dx,0);
//该有的速度测量完毕,清空数据
mVelocityTracker.clear();
break;
default:break;
}
//滑动完成,表示已经处理该事件,所以返回真表示吸收
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int measureWidth = 0;
int measureHeight = 0;
final int childCount = getChildCount();
measureChildren(widthMeasureSpec,heightMeasureSpec);
int widthSpaceSize = MeasureSpec.getSize(widthMeasureSpec);
int widthSpaceModel = MeasureSpec.getMode(widthMeasureSpec);
int heightSpaceSize = MeasureSpec.getSize(heightMeasureSpec);
int heightSpaceModel = MeasureSpec.getMode(heightMeasureSpec);
if(childCount == 0){
setMeasuredDimension(0,0);
}else if(widthSpaceModel == MeasureSpec.AT_MOST && heightMeasureSpec == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(measureWidth,measureHeight);
} else if(heightSpaceModel == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureHeight = childView.getMeasuredHeight();
setMeasuredDimension(widthSpaceSize,measureHeight);
} else if(widthSpaceModel == MeasureSpec.AT_MOST){
final View childView = getChildAt(0);
measureWidth = childView.getMeasuredWidth() * childCount;
setMeasuredDimension(measureWidth,heightSpaceSize);
}
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int childLeft = 0;
final int childCount = getChildCount();
mChildrenSize = childCount;
for(int i = 0;i<childCount;i++){
final View childView = getChildAt(i);
if(childView.getVisibility() != View.GONE){
final int childWidth = childView.getMeasuredWidth();
mChildWidth = childWidth;
childView.layout(childLeft,0,childLeft+childWidth,childView.getMeasuredHeight());
childLeft += childWidth;
}
}
}
/**
* 弹性滑动,就是一个时间段一个时间段时间段接着去重绘,达到滑动的效果
*/
private void smoothScrollBy(int dx,int dy){
//保存滑动数据,开始x,y终止x,y,时间
mScroller.startScroll(getScrollX(),0,dx,0,500);
//开始滑动,一个类似循环的方法,
invalidate();
}
//为配合上面的弹性滑动实现的方法
@Override
public void computeScroll() {
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}
//view不在显示状态时调用该方法
@Override
protected void onDetachedFromWindow() {
mVelocityTracker.recycle();
super.onDetachedFromWindow();
}
}
- 这里关于滑动部分处理的注释我已经在代码中写的很清楚了
总结
- onMeasure()里面setMeasuredDimension(0,0);这个方法用来设置最终控件的大小