Demo链接: 点击打开链接
包括自定义继承view,FrameLayout,,TextView等实现进度条,弹性滑动等功能
一.为什么要用自定义控件
1.特定的显示风格 (特殊效果)
2.处理特有的用户交互(例如TextView滑动文字)
3.优化我们的布局(一次测量绘制的过程慢)
4.封装等....(例如底部tab按钮封装)
一般情况,有自绘控件,组合控件,继承控件
二.View的滑动
view的滑动分为以下三种:
1)View本身不滚动,指滚动View的内容,这也是View类提供的原始方法,通过scrollTo和ScrollBy方法来实现。scollBy()里面友调用了scollTo()方法
因为View在ViewGroup中的位置是由LayoutParams的margin等参数决定的,要想滚动View或者说要想改变View的位置只需要改变LayoutParams的相关参数就可以。但是scrollTo和scrollBy改变的只是mScrollX和mScrollY的值(看源码),这两个值对于改变View在ViewGroup里面的位置是毫无关系的
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();
}
}
}
public void scrollBy(int x, int y) {
scrollTo(mScrollX + x, mScrollY + y);
}
2)使用动画,让View来产生滚动效果
3)通过动态的修改LayoutParams的margin等属性让View来产生滚动
三.如何实现弹性滑动
Scroll类,可能会用到滑动速度跟踪类 VelocityTracker
在onTouchEvent里面判断 如果是ACTION_DOWN,就利用
mScroller.startScroll(getScrollX(), getScrollY(), 0, 200, 1000);让控件向上弹起200像素
在ACTION_UP时将200改成-200就ok了,当然,每次设置startScroll时得重绘invalidate()重新绘制 触发computeScroll()方法,在里面scrollTo()当前坐标
四.View的滑动冲突
(1)、常见的滑动冲突场景
场景1——外部滑动方向和内部滑动方向不一致,如:ViewPager中有多个fragment,而fragment中有ListView,这时ViewPager可以左右滑动,而ListView可以上下滑动,这就造成了滑动冲突。注意:这只是举个例子说明一下场景1,事实上ViewPager内部已经处理了这种滑动冲突,在采用ViewPager时,我们无需关注这个问题。
场景2——外部滑动方向和内部滑动方向一致。
场景3——上述两种场景的嵌套,即共有三层,外层与中层的滑动方向一致,而中层与内层的滑动方向不一致。
(2)、滑动冲突的处理规则
场景1的处理规则:
1、当用户左右滑动时,让外部的View拦截点击事件,当用户上下滑动时,让内部的View拦截点击事件;
2、判断用户的滑动方向(左右、上下):如果用户手指滑动的水平距离大于垂直距离,则左右滑动,反之,上下滑动;还可以根据角度、速度差来做判断;
场景2的处理规则:
无法根据滑动的角度、距离差、速度差来判断,因为场景2内部、外部的滑动方向一致;这时候一般都能在业务上找到突破点,如业务上规定:当处于某种状态需要外部View响应用户的滑动,而处于另一种状态时则需要内部View响应用户的滑动,所以我们可以根据业务的需求得出相应的处理规则。
场景3的处理规则:场景1的处理规则和场景2的处理规则一起用。
解决方法:
1.外部拦截法:(父容器)
@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;
//父容器不需要拦截,如果拦截的话,子元素的onClick事件就无法执行
case MotionEvent.ACTION_UP:
intercepted = false;
break;
default:
break;
}
//重置手指的起始位置
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
2.内部拦截法:(子视图通知父容器不拦截)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//获得当前的位置坐标
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (ev.getAction()){
case MotionEvent.ACTION_DOWN:
//通知父容器不要拦截事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
if (父容器需要此事件){
//通知父容器拦截此事件
horizontalScrollLayout.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
//重置手指的初始位置
mLastY = y;
mLastX = x;
return super.dispatchTouchEvent(ev);
}
五.如何定义控件
1.自定义属性的声明与获取
2.测量onMeasure
3.布局onLayout(ViewGroup)
4.绘制onDraw
5.OnTouchEvent
6.onInterceptTouchEvent(ViewGroup)
一般就会用到onMeasure()和onDraw()方法
(1)分析需要的自定义属性(2)在res/values/attrs.xml定义声明(3)在layout.xml中使用(4)在View的构造方法中进行获取(会得到TypeArray,最后记得a.recycle())
自定义View有自己的职责,测量自身,决定自身到底需要多大的范围以及自身的样子到底有什么样子(onDraw)
onMeasure
测量有两个因素决定,一个是测量的模式,一个是测量的值。
测量的模式:
UNSPECIFIED 父容器没有对当前View有任何限制(例如ListView,ScrollView),当前View可以任意取尺寸
EXACTLY 当前的尺寸就是当前View应该取的尺寸
AT_MOST 当前尺寸是当前View能取的最大尺寸
MeasureSpec 代表了一组宽度和高度的要求,由大小和模式组成
最后确定控件大小时,需要判断MeasureSpec的mode,不能直接用MeasureSpec的size。在进行一些逻辑处理以后,调用setMeasureDimension()方法,将测量得到的宽高传进去供layout使用,保存结果
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int measuredHeight, measuredWidth;
if (widthMode == MeasureSpec.EXACTLY) {
measuredWidth = widthSize;
} else {
measuredWidth = SIZE;
}
if (heightMode == MeasureSpec.EXACTLY) {
measuredHeight = heightSize;
} else {
measuredHeight = SIZE;
}
setMeasuredDimension(measuredWidth, measuredHeight);
}
requestLayout触发测量,重新布局等,不包括onDraw
onLayout
自定义的view,而不是ViewGroup的话就没有此过程
(1)决定子View显示的位置(2)如果onMeasure执行多次包含耗时操作的话, 可以移动到此方法
requestLayout()触发
onDraw
1.绘制内容区域
2.invalidate()UI线程 postInvalidate()子线程重绘
3.熟练 Canvas.drawXXX 方法 drawLine drawRect drawCircle
4.translate roate scale skew Canvas 变换的方法
5.如过使用了变换的话,记得要save() restore() Canvas的状态
drawText() drawPostText()绘制文本 drawArc drawPath绘制路径
drawBitmap()在指定点绘制位图,绘制动画等
onTouchEvent 处理触摸或者手势,多点触控active point....
onInterceptToucEvent
最后还有onSaveInstanceState onRestoreInstanceState 保存和恢复信息
例如微信里面的自定义View就用到过。
自定义Prograss ,Activity重建时,View就会调用这两个方法
ScaleGestureDetector 要写缩放手势时可以利用系统已有类属性完成
等等....