这是一个支持横向滑动,并处理了滑动冲突的自定义ViewGroup。几乎涵盖了自定义viewGroup的所有知识,对于理解View的相关知识有一定的帮助,是一个不错的实战Demo。以下为功能,所做的处理及对应的知识点。
为了使布局能够横向滑动,需要重写onTouchEvent()方法,在这个方法中判断是否为横向滑动,如果是的话就使用scrollBy()方法让布局内容滑动。当用户快速滑动时,使用Tracker判断速度是否为横向滑动,如果是的话使用Scroller使布局内容平滑滑动。具体判断方法如下代码。(当然需先判断是否拦截事件)。
//滑动事件处理
@Override
public boolean onTouchEvent(MotionEvent event) {
tracker.addMovement(event);
int x = (int)event.getX();
int y = (int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
if(!scroller.isFinished()){
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x-lastX;
int deltaY = y-lastY;
//每次进行滑动限制
scrollBy(-scrollLimit(deltaX),0);
break;
case MotionEvent.ACTION_UP:
int dx=0;
//处理快速滑动
tracker.computeCurrentVelocity(1000);
float xVelocity = tracker.getXVelocity();
if(Math.abs(xVelocity)>=50){
dx = 0-scrollLimit((int)xVelocity);
}
//使用Scroller
smoothScrollBy(dx,0);
tracker.clear();
break;
}
lastX = x;
lastY = y;
return true;
}
关于scrollBy()方面的知识,可参考以下文章:更好地理解 scrollBy() / scrollTo()
当布局的子view为scrollView或ListView等可以滑动的view时,就需要进行滑动冲突处理,否则最终效果可能与你的目的不同。在这里只处理子view支持纵向滑动与布局之间的滑动冲突。简单来说,就是当为横向滑动时,布局拦截事件,否则传给子view。
//处理滑动冲突,判断是否拦截事件
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
Boolean intercept = false;
int x = (int)ev.getX();
int y = (int)ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercept = false;
if(!scroller.isFinished()){
scroller.abortAnimation();
intercept =true;
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x-lastX;
int deltaY = y-lastY;
//横向滑动
if(Math.abs(deltaX)>Math.abs(deltaY)){
intercept = true;
}
break;
case MotionEvent.ACTION_UP:
intercept = false;
break;
default:
break;
}
//记录上次事件坐标
lastX = x;
lastY = y;
return intercept;
}
如果不进行处理的话,我们的自定义布局设置wrap_content属性跟match_parent属性效果一样。具体原因跟view的测量过程有关。可阅读文章进行了解:Android:为什么你的自定义View wrap_content不起作用?
这里附上处理代码:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
measureChildren(widthMeasureSpec,heightMeasureSpec);
int childCount = getChildCount();
maxHeight=0;
maxWidth=0;
for(int i = 0;i
为了更好的体验,我们需对滑动范围进行限制,不然的话会无限滑动,看到的是一片空白。这里限制的范围是布局子view的总宽度,即滑到子view边缘就不再滑动了。这里比较容易搞错正负值,需要注意。代码如下:
//限制滑动距离
private int scrollLimit(int delta){
//子view总长度小于布局宽度,禁止滑动
if(maxWidth-getWidth()<=0){
return 0;
}else {
if (delta <= 0) {
//左滑
if (getScrollX() == maxWidth - getWidth()) {
//处于最右边,右边缘可见 ,禁止继续左滑
return 0;
}else{
//限制滑动距离,使左滑不超过子view内容的右边缘
int dx = Math.min(maxWidth - getWidth() - getScrollX(), Math.abs(delta));
return 0 - dx;
}
} else {
//右滑
if (getScrollX() == 0) {
//处于开始状态,左边缘可见,禁止继续右滑
return 0;
}else{
//限制滑动距离,使右滑不超过子view内容的左边缘
return Math.min(Math.abs(getScrollX()),delta);
}
}
}
}
为了使布局支持padding及子view间的margin,在进行布局时需将此考虑在内。当我们在记录子view位置信息时就将此考虑在内,代码如下。(其中ViewLocation为记录子view位置信息所创建的类)
//保存各view的位置参数(处理margin)
private void setLocation(View v,MarginLayoutParams lp){
ViewLocation mLocation = new ViewLocation();
mLocation.setLeft(left+lp.leftMargin);
mLocation.setRight(mLocation.getLeft()+v.getMeasuredWidth());
mLocation.setTop(getPaddingTop()+lp.topMargin);
mLocation.setBottom(mLocation.getTop()+v.getMeasuredHeight());
maxWidth += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
left += mLocation.getRight()+lp.rightMargin-mLocation.getLeft()+lp.leftMargin;
maxHeight = (mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin)>=maxHeight?mLocation.getBottom()-mLocation.getTop()+lp.bottomMargin+lp.topMargin:maxHeight;
viewLocationList.add(mLocation);
}
注意我们获取子view的margin属性的方法是先通过获取它的MarginLayoutParams(上图方法第2个参数)。
MarginLayoutParams lp = (MarginLayoutParams) v.getLayoutParams();
此时我们需要重写 generateLayoutParams()方法,否则会报错,类型转换错误,因为这个方法默认返回空值。更多相关的内容可阅读文章:你的自定义View是否真的支持Margin
//为获取子view margin属性,重写方法(否则会报错)
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(),attrs);
}
完整代码见GitHub:https://github.com/YangRT/HorizontalScrollView