Android view 自定义控件
自定义View的思想
1) 掌握View控件的基本功,比如View得弹性滑动、滑动冲突、绘制原理等
2) 面对新的的自定义View时,要能够对其分类并选择合适的实现思路
3) 积累自定义View相关经验,并逐步融会贯通
对于以上的三点,逐步进行讲解,首先针对View控件的基本功
VelocityTracker 追踪手指滑动过程的速度
private VelocityTracker mVelocityTracker ;
@Override
public boolean onTouchEvent(MotionEvent ev) {
mVelocityTracker = VelocityTracker.obtain();
mVelocityTracker.addMovement(ev);
//在调用getXVelocity之前必须调用computeCurrentVelocity
mVelocityTracker.computeCurrentVelocity(1000);
//当从右向左滑时候xVelocity是负值
int xVelocity = (int) mVelocityTracker.getXVelocity();
int yVelocity = (int) mVelocityTracker.getYVelocity();
//消耗改对象
mVelocityTracker.clear();
mVelocityTracker.recycle();
return super.onTouchEvent(ev);
}
GestureDetector 检查单击、长按、滑动、双击等
private GestureDetector mGestureDetector = new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener());
@Override
public boolean onTouchEvent(MotionEvent ev) {
//解决长按无法拖动的问题
mGestureDetector.setIsLongpressEnabled(false);
boolean comsumer = mGestureDetector.onTouchEvent(ev);
return comsumer;
}
在OnGestureListener、onDoubleTapListener里面处理。一般在监听双击需要使用GestureDetector其它情况不太用,在onTouchEvent处理即可。
View的基本功主要涉及到弹性滑动、滑动冲突、绘制原理
一、 View的弹性滑动
View的滑动有三种方式
通过View 本身提供的scrollTo/scrollBy 方法
根据源码scrollBy 调用用scrollTo方法,scrollBy实现了相对位置的滑动
这种滑动主要是控件mScrollX 和mScrollY改变规则。这两个控件可以根据getScrollX,getScrollY获取。在滑动过程中,mScrollX的值总是等于View左边缘和View内容左边缘在水平方向的距离
mScrollY的值总是等于View上边缘和View内容上边缘在竖直方向的距离
比如我们对TextView.scrollBy()它移动的是TextView里面的内容的位置,要想对TextView本身移动必须 ((View)TextView.getParent()).scrollBy.
需要说明的是所移动的位置无法超过其控件的位置,也就是说无法从当前的View滑动到附件View所在的位置。
如图(注:虚线框是View实线框是View内容)
/** * Move the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the amount of pixels to scroll by horizontally<pre name="code" class="java"> /** * Set the scrolled position of your view. This will cause a call to * {@link #onScrollChanged(int, int, int, int)} and the view will be * invalidated. * @param x the x position to scroll to * @param y the y position to scroll to */ public void scrollTo(int x, int y) { if (mScrollX != x || mScrollY != y) { int oldX = mScrollX; int oldY = mScrollY; mScrollX = x; mScrollY = y; invalidateParentCaches(); onScrollChanged(mScrollX, mScrollY, oldX, oldY); if (!awakenScrollBars()) { postInvalidateOnAnimation(); } } } /* @param y the amount of pixels to scroll by vertically */ public void scrollBy(int x, int y) { scrollTo(mScrollX + x, mScrollY + y); }
通过动画给View施加平移效果来实现滑动
平移操作的是View translationX 和 translationY属性
既可以采用传统的View动画也可以采用属性动画,如果采用属性动画需要兼容3.0版本一下加入nineoldandroids(http://nineoldandroids.com/)采用传统View 动画
<!-- res/anim/fade_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fromAlpha="0.0"
android:toAlpha="1.0" />
AlphaAnimation fadeInAnimation = (AlphaAnimation) AnimationUtils.loadAnimation(this, R.anim.fade_in);
view.startAnimation(fadeInAnimation);
Set(AlphaAnimation,TranslateAnimation,ScaleAnimation,RolateAnimation)
要保持移动后不回到原来的状态需要设置fillAfter=true
传统的动画,主要改变的并不是原来的view的位置,而是其影像,所以view的点击时间仍然还在原来的位置。这个需要程序员自行处理。
采用属性动画不存在上面问题
ObjectAnimator.ofFloat(targetView,”translationX”,0,100).setDuration(100).start();(需要考虑3.0之前版本)通过改变View的LayoutParams使得View重新布局从而实现滑动
MarginLayoutParams params = (MarginLayoutParams)mButton1.getLayoutParams(); params.width +=100; params.leftMargin += 100; mButton1.requestLayout(); //或者mButton1.setLayoutParams(params);
View 弹性滑动具体实现有很多,基本实现就是将一次大的滑动分成若干次小的滑动并在一个时间段内完成。这里介绍三种
使用Scroller
Scroller mScroller = new Scroller(context); private void smoothScrollTo(int destX,int destY){ if(mScroller == null){ return; } int scrollX = getScrollX(); int deltX = destX - scrollX; mScroller.startScroll(scrollX,0,deltX,0,1000); invalidate(); } @Override public void computeScroll() { if(mScroller == null){ return; } if(mScroller.computeScrollOffset()){ scrollTo(mScroller.getCurrX(),mScroller.getCurrY()); postInvalidate(); } }
从Scroller源码可以得知,Scroller并木有去滑动View而是通过view的computeScroll,computeScroll每次重绘时候会被调用,这样Scroller 又通过computeScrollOffset方法按照时间比例改变View的x,y,调用scrollTo一步一步滑动,postInvalidate()重绘view回调到View的computeScroll实现滑动。
通过动画
这个不多说,熟练属于View动画和属性动画
使用延时策略
private Handler mHandler = new Handler(){ public void handleMessage(Message msg){ switch(msg.what){ case MESSAGE_SCROLL_TO:{ mCount++; if(mCount <= FRAME_COUNT){ float fraction = mCount/(float)FRAME_COUNT; int scrollX = (int) (fraction)*100; mButton1.scrollTo(scrollX,0); mHandler.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO,DELAYED_TIME); } break; } default: break; } } }
二、View绘制原理与事件分发机制
绘制原理我也不多说,有这两篇文章足以理解view的绘制原理了。
图解 Android 事件分发机制(和源码读才更配噢)
三、View滑动冲突解决
滑动冲突解决的两种原则
1. 上下左右等方位的冲突可以通过计算XY坐标距离处理
2. 无法通过方位的冲突只能通过业务逻辑处理,比如那些控件需要响应事件那些需要拦截
滑动冲突三种场景
1. ViewPager和Fragment上下左右滑动冲突,通过原则1解决
2. scrollView和ListView同方向的上下滑动冲突
3. 上面两种混合比如SlideMenu加了一个ViewPager,ViewPager里面增加了一个ListView.
具体代码处理其实是相同的:
1. 外部拦截 onInterceptTouchEvent( MotionEvent ev )
父View觉得是否将事件传递子view
private int mLastXIntercept = 0;
private int mLastYIntercept = 0;
@Override
public boolean onInterceptTouchEvent( MotionEvent ev ) {
boolean intercepted = false;
int x = (int) ev.getX();
int y = (int) ev.getY();
switch(ev.getAction()){
case MotionEvent.ACTION_DOWN:
intercepted = false;
break;
case MotionEvent.ACTION_MOVE:
if(需要当前事件){
intercepted = true;
}else{
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
不能拦截DOWN世界要不后面的MOVE,UP事件无法传递到子控件了
UP事件也不拦截,拦截木有意义
我们只需要处理MOVE事件中需要处理的事件
- 内部拦截也就是在子控件中处理消耗事件
private int mLastX;
private int mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
this.getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltX = x - mLastX;
int deltY = y - mLastY;
if (parentNeedHandlerEvent()) {
this.getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(ev);
}
《Android开发艺术探究》笔记