为加深自定义ViewGroup实现思想,所以自己写了一个SlidingMenu,实现方式类似
Android-自定义ViewGroup(一) 水平滑动;优点在于添加了SlidingMenu的padding(onMeasure中测量,onLayout中布局)及子ViewGroup的Margin(onLayout中布局)。
下面说实现思想:SlidingMenu中添加两个布局
(1)onMeasure中先对子ViewGroup进行测量,调用measureChildren()方法;measureChildren(widthMeasureSpec, heightMeasureSpec);
其次要设置自身SlidingMenu的宽高,当int widthMode = MeasureSpec.getMode(widthMeasureSpec)
得到mode为EXACTLY时,设置getSize得到的宽高,其他模式时对子ViewGroup进行测量得到所有子ViewGroup宽高的总和,传入setMeasuredDimension(),如下:
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
onMeasure代码如下:
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//menu、content总宽
int width = 0;
//menu、content总高
int height = 0;
//可以单独获取子ViewGroup并测量宽高,此处用for是因为所有自定义ViewGroup都适用。
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//必须重写generateLayoutParams(attrs)方法,设置适当的LayoutParams
MarginLayoutParams childLp = (MarginLayoutParams) child.getLayoutParams();
//单独的宽高
width += child.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin;
height += child.getHeight() + childLp.topMargin + childLp.bottomMargin;
//menuWidth为menu的宽,考虑了此ViewGroup的leftPadding值,用作onTouch限定滑动边界、onLayout布局
if (i == 0) {
menuWidth = getPaddingLeft() + childLp.leftMargin + childLp.rightMargin + child.getMeasuredWidth();
}
}
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
//精确模式设置测量得到的尺寸,否则去设置子ViewGroup的宽高测量总和
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
(2)布局只要注意将SlidingMenu的padding和子ViewGroup的margin考虑,没什么复杂的,具体看代码:
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//获取此SlidingMenu的leftPadding,布局时无需考虑rightPadding
int left = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
LinearLayout child = (LinearLayout) getChildAt(i);
if (null != child && child.getVisibility() != GONE) {
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int lc = left + lp.leftMargin;
int tc = lp.topMargin + getPaddingTop();
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
//menuWidth为左边菜单宽度
child.layout(lc - menuWidth, tc, rc - menuWidth, bc);
left += rc + lp.rightMargin;
}
}
}
(3)注意:以上即完成了SlidingMenu的布局,但是注意我们控件继承的是ViewGroup,通过child.getLayoutParams()拿到的是ViewGroup的布局管理器,需重写该方法的返回值拿到我们要的margin属性。如下:
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
关于MarginLayoutParams可以去看下,它是ViewGroup的子类,又是包括LinearLayout、RelativeLayout、RecyclerVeiw等布局管理器的父类,可以去了解下。
(4)以上就完成了SlidingMenu的布局,此时我们要处理滑动事件,处理滑动事件时首先得确定手指左右滑动,并拦截交由SlidingMenu自身去消费事件。拦截与消费即在onInterceptTouchEvent、onTouchEvent判断水平移动时return true。并在onTouchEvent的MotionEvent.ACTION_MOVE中通过Scroller去设置滑动;
关于Scroller的固定用法如下:
Scroller scroller = new Scroller(getContext());
int scrollX = getScrollX();
int delta = destx - scrollX;
//300ms内滑向destX,效果就是慢慢滑动
scroller.startScroll(scrollX, 0, delta, 0, 300);
invalidate();
/**
* 滚动时需要重写的方法,用于控制滚动
*/
@Override
public void computeScroll() {
//判断滚动时候停止
if (scroller.computeScrollOffset()) {
//滚动到指定的位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//这句话必须写,否则不能实时刷新
postInvalidate();
}
}
另外VelocityTracker速度追踪器,及TouchSlop最小有效滑动距离的概念比较简单,有兴趣的可自行了解
下面是该SlidIngMenu的所有代码:
public class ZhanfSlideMenu extends ViewGroup {
private Scroller scroller;
private VelocityTracker velocityTracker;
public ZhanfSlideMenu(Context context) {
this(context, null);
}
public ZhanfSlideMenu(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ZhanfSlideMenu(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
scroller = new Scroller(getContext());
velocityTracker = VelocityTracker.obtain();
int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
}
/**
* 滚动时需要重写的方法,用于控制滚动
*/
@Override
public void computeScroll() {
//判断滚动时候停止
if (scroller.computeScrollOffset()) {
//滚动到指定的位置
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//这句话必须写,否则不能实时刷新
postInvalidate();
}
}
//分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
//分别记录上次滑动的坐标(onINterceptTouchEvent)
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
int menuWidth;
//要移动的点的位置
int destx;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//如果动画还没有结束,再次点击时结束上次动画,即开启这次新的ACTION_DOWN的动画
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastXIntercept;
int deltaY = y - mLastYIntercept;
if (Math.abs(deltaX) > Math.abs(deltaY)) {
return true;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
mLastXIntercept = x;
mLastYIntercept = y;
return super.onInterceptTouchEvent(event);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
velocityTracker.addMovement(event);
velocityTracker.computeCurrentVelocity(1000);
int x = (int) event.getX();
int y = (int) event.getY();
int scrollX = getScrollX();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
//如果动画还没有结束,再次点击时结束上次动画,即开启这次新的ACTION_DOWN的动画
if (!scroller.isFinished()) {
scroller.abortAnimation();
}
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (Math.abs(deltaX) > Math.abs(deltaY) && scrollX >= -menuWidth && scrollX <= 0) {
scrollBy(-deltaX, 0);
}
break;
case MotionEvent.ACTION_UP:
int xVelocity = (int) velocityTracker.getXVelocity();//获取X方向手指滑动的速度,之前必须调用computeCurrentVelocity()方法
int yVelocity = (int) velocityTracker.getYVelocity();
if (Math.abs(xVelocity) > 200 && Math.abs(xVelocity) > Math.abs(yVelocity)) {
destx = xVelocity > 0 ? (-menuWidth) : 0;
} else {
destx = (scrollX < (-menuWidth / 2)) ? (-menuWidth) : 0;
}
int delta = destx - scrollX;
scroller.startScroll(scrollX, 0, delta, 0, 300);
invalidate();
break;
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
measureChildren(widthMeasureSpec, heightMeasureSpec);
//menu、content总宽
int width = 0;
//menu、content总高
int height = 0;
//可以单独获取子ViewGroup并测量宽高,此处用for是因为所有自定义ViewGroup都适用。
for (int i = 0; i < getChildCount(); i++) {
View child = getChildAt(i);
//必须重写generateLayoutParams(attrs)方法,设置适当的LayoutParams
MarginLayoutParams childLp = (MarginLayoutParams) child.getLayoutParams();
//单独的宽高
width += child.getMeasuredWidth() + childLp.leftMargin + childLp.rightMargin;
height += child.getHeight() + childLp.topMargin + childLp.bottomMargin;
//menuWidth为menu的宽,考虑了此ViewGroup的leftPadding值,用作onTouch限定滑动边界、onLayout布局
if (i == 0) {
menuWidth = getPaddingLeft() + childLp.leftMargin + childLp.rightMargin + child.getMeasuredWidth();
}
}
width += getPaddingLeft() + getPaddingRight();
height += getPaddingTop() + getPaddingBottom();
//精确模式设置测量得到的尺寸,否则去设置子ViewGroup的宽高测量总和
setMeasuredDimension(widthMode == MeasureSpec.EXACTLY ? widthSize : width, heightMode == MeasureSpec.EXACTLY ? heightSize : height);
}
@Override
public LayoutParams generateLayoutParams(AttributeSet attrs) {
return new MarginLayoutParams(getContext(), attrs);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
//获取此SlidingMenu的leftPadding,布局时无需考虑rightPadding
int left = getPaddingLeft();
for (int i = 0; i < getChildCount(); i++) {
LinearLayout child = (LinearLayout) getChildAt(i);
if (null != child && child.getVisibility() != GONE) {
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
int lc = left + lp.leftMargin;
int tc = lp.topMargin + getPaddingTop();
int rc = lc + child.getMeasuredWidth();
int bc = tc + child.getMeasuredHeight();
//menuWidth为左边菜单宽度
child.layout(lc - menuWidth, tc, rc - menuWidth, bc);
left += rc + lp.rightMargin;
}
}
}
}
Qusetion:关于实现SlidingMenu的实现就是这么简单,但也遇上了一个问题,就是在onTouchEvent的Move事件中设置了左右边界,当在边界处手指慢慢移动左右边界正常显示不会滑动。但是当在边界处快速滑动的时候,左右能继续弹性滑动一小段距离,可以看到白色的背景。关于这个问题有待进一步研究。
参考:
《Android开发艺术探索》