自定义控件常用对象及方法
常用对象:
VelocityTracker:
在android中主要应用于touch event。 VelocityTracker通过跟踪一连串事件实时计算出当前的速度。
android.view.VelocityTracker主要用跟踪触摸屏事件(flinging事件和其他gestures手势事件)的速率。用addMovement(MotionEvent)函数将Motion event加入到VelocityTracker类实例中.你可以使用getXVelocity()或getXVelocity()获得横向和竖向的速率到速率时,但是使用它们之前请先调用computeCurrentVelocity(int)来初始化速率的单位。
用法:
@Override
public boolean onTouchEvent(MotionEventevent) {
if (mVelocityTracker==null) {
mVelocityTracker =VelocityTracker.obtain();
}
mVelocityTracker.addMovement(event);
case MotionEvent.ACTION_UP:
mVelocityTracker.computeCurrentVelocity(1000);
float yVelocity =mVelocityTracker.getYVelocity();
if (yVelocity>standerSpeed|| ((getScrollY() +mHeight/ 2) /mHeight< mStartScreen)) {
mState =State.ToPre;
}else if (yVelocity< -standerSpeed|| ((getScrollY() + mHeight /2) /mHeight > mStartScreen)) {
changeByState(yVelocity);
}
if (mVelocityTracker!=null) {
mVelocityTracker.recycle();
mVelocityTracker=null;
}
break;
}
Region
Region rgn = new Region();
rgn.setPath(ovalPath,new Region(50, 50, 200, 200));
region这个对象很好用,我们可以用他来判断并获取几个region的相交的部分,同时还可以判断点击的x,y是不是在region区域内。对于不规则控件的点击就很好用。
http://www.gcssloop.com/customview/touch-matrix-region
这个demo讲的很不错哦。
我们可以再一个大区域里面获取小区域:
right_p =new Path();
right =new Region();
@Override
protectedvoidonSizeChanged(int w, int h,int oldw,int oldh){
super.onSizeChanged(w, h, oldw, oldh);
//注意这个区域的大小
Region globalRegion =new Region(-w, -h, w, h);
right_p.addArc(bigCircle, -40, bigSweepAngle);
right_p.arcTo(smallCircle,40, smallSweepAngle);
right_p.close();
right.setPath(right_p, globalRegion);
}
在获取区域后我们可以在ontouch中判断点击的x,y是不是在region区域内:
int x = (int) event.getX();
int y = (int) event.getY();
right.contains(x,y)
注意:
开启硬件加速情况下 event.getX() 和不开启情况下 event.getRawX() 等价,获取到的是屏幕(物理)坐标 (本文的锅)。
开启硬件加速情况下 event.getRawX() 数值是一个错误数值,因为本身就是全局的坐标
又叠加了一次 View 的偏移量,所以肯定是不正确的 (本文的锅)。
两个region可以做如下的操作:
//构造两个Region
Region region = new Region(rect1);
Region region2= new Region(rect2);
//取两个区域的交集
region.op(region2, Op.INTERSECT);
Scroller
Scroller是计算结果来影响scrollTo,scrollBy,从而使得滑动发生改变。同时还可以判断当前滑动的状态。
用法:
1. 创建Scroller的实例
2. 调用startScroll()方法来初始化滚动数据并刷新界面
3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
这里scroller仅仅是通过startScroll()设置一个滑动的开始点和结束点的一个目标。然后每次在draw之后去重新确定我们下一个目标点在哪里,然后真正的执行者是scrollTo,scrollBy。
public ScrollerLayout(Context context, AttributeSet attrs) {
super(context, attrs);
// 第一步,创建Scroller的实例
mScroller = new Scroller(context);
}
@Override
publicboolean onTouchEvent(MotionEvent event) {
………
caseMotionEvent.ACTION_UP:
//当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
inttargetIndex = (getScrollX() + getWidth() /2) /getWidth();
int dx =targetIndex * getWidth() - getScrollX();
//第二步,调用startScroll()方法来初始化滚动数据并刷新界面
mScroller.startScroll(getScrollX(),0, dx,0);
invalidate();
break;
}
returnsuper.onTouchEvent(event);
}
@Override
publicvoid computeScroll() {
// 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
if (mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
invalidate();
}
}
加入scroller是的控件的滑动更加平滑!
这里computeScrollOffset()方法判断滑动是否结束,他的内部代码通过:1、是否在滑动的标志flag;2、是否到达指定的duration;3、目标坐标是否和当前坐标一致;来判断滑动是否结束。
在一个对象中用到scroll,是对这个对象进行滑动!如果想要单独对对象里面的子控件进行拖动,可以考虑下dragViewHelper。
ViewConfiguration
包含了方法和标准的常量用来设置UI的超时、大小和距离 。
Camera
在graphics包下,Camera用来计算3D转换、生成矩阵,然后应用在画布上。
Camera的坐标系是左手坐标系。
public Rotate3DAnimation(Contextcontext,float fromDegrees,float toDegrees,
float centerX,floatcenterY,float depthZ,boolean reverse) {
mFromDegrees = fromDegrees;
mToDegrees = toDegrees;
mCenterX = centerX;
mCenterY = centerY;
mDepthZ = depthZ;
mReverse = reverse;
// 像素密度
scale =context.getResources().getDisplayMetrics().density;
}
@Override
public voidinitialize(int width,intheight,int parentWidth,int parentHeight) {
super.initialize(width,height, parentWidth, parentHeight);
mCamera = newCamera();
}
@Override
protectedvoidapplyTransformation(float interpolatedTime, Transformation t) {
finalfloat fromDegrees = mFromDegrees;
floatdegrees = fromDegrees + ((mToDegrees - fromDegrees) * interpolatedTime);
finalfloat centerX = mCenterX;
finalfloat centerY = mCenterY;
finalCamera camera = mCamera;
finalMatrix matrix = t.getMatrix();
camera.save();
// 调节深度
if(mReverse) {
camera.translate(0.0f, 0.0f,mDepthZ * interpolatedTime);
} else{
camera.translate(0.0f, 0.0f,mDepthZ * (1.0f - interpolatedTime));
}
// 绕y轴旋转
camera.rotateY(degrees);
camera.getMatrix(matrix);
camera.restore();
// 修正失真,主要修改 MPERSP_0和 MPERSP_1
float[] mValues = newfloat[9];
matrix.getValues(mValues); //获取数值
mValues[6] = mValues[6]/scale; //数值修正
mValues[7] =mValues[7]/scale; //数值修正
matrix.setValues(mValues); //重新赋值
// 调节中心点
matrix.preTranslate(-centerX,-centerY);
matrix.postTranslate(centerX, centerY);
}
}
最经典的例子只能是官方示例-Rotate3dAnimation。
Matrix
参考:http://www.cnblogs.com/zhouyang209117/p/5100977.html
set、pre 与 post
对于四种基本变换平移(translate)、缩放(scale)、旋转(rotate)、错切(skew) 它们每一种都三种操作方法,分别为设置(set)、前乘(pre) 和后乘 (post)。而它们的基础是Concat,通过先构造出特殊矩阵然后用原始矩阵Concat特殊矩阵,达到变换的结果。
在Camera类的例子里有一个:
// 调节中心点
matrix.preTranslate(-centerX,-centerY);
matrix.postTranslate(centerX,centerY);
preTranslate是指在setScale前,平移,postTranslate是指在setScale后平移注意他们参数是平移的距离,而不是平移目的地的坐标!
由于缩放是以(0,0)为中心的,所以为了把界面的中心与(0,0)对齐,就要preTranslate(-centerX, -centerY),
setScale完成后,调用postTranslate(centerX,centerY),再把图片移回来,这样看到的动画效果就是activity的界面图片从中心不停的缩放了。
canvas.concat的作用可以理解成对matrix的变换应用到canvas上的所有对象(是用matrix画上去的多有对象).
可以参考下面:
GestureDetector
(这他喵的是2.2的方法,我到6.0才发现,相见恨晚啊)
Android提供了一个手势监听类GestureDetecto,对触屏事件的处理。
GestureDetector mGestureDetector = newGestureDetector(this,newMyOnGestureListener());
mButton.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEventevent) {
Log.i(getClass().getName(), "onTouch-----" +getActionName(event.getAction()));
mGestureDetector.onTouchEvent(event);
//但只有返回true才能继续接收move,up等事件,也才能响应ScaleGestureDetector事件及GestureDetector中与move,up相关的事件
return true;
}
});
mGestureDetector.onTouchEvent(event);
class MyOnGestureListener extendsSimpleOnGestureListener {
@Override
public boolean onSingleTapUp(MotionEvente) {
Log.i(getClass().getName(), "onSingleTapUp-----" +getActionName(e.getAction()));
return false;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i(getClass().getName(), "onLongPress-----" +getActionName(e.getAction()));
}
@Override
public boolean onScroll(MotionEvent e1,MotionEvent e2,float distanceX,float distanceY) {
Log.i(getClass().getName(),
"onScroll-----" + getActionName(e2.getAction()) +",(" + e1.getX() + "," + e1.getY() + ") ,("
+ e2.getX() + ","+ e2.getY() + ")");
return false;
}
@Override
public boolean onFling(MotionEvent e1,MotionEvent e2,float velocityX,float velocityY) {
Log.i(getClass().getName(),
"onFling-----" + getActionName(e2.getAction()) +",(" + e1.getX() + "," + e1.getY() + ") ,("
+ e2.getX() + ","+ e2.getY() + ")");
return false;
}
@Override
public void onShowPress(MotionEvent e) {
Log.i(getClass().getName(), "onShowPress-----" +getActionName(e.getAction()));
}
@Override
public booleanonDown(MotionEvent e) {
Log.i(getClass().getName(), "onDown-----" +getActionName(e.getAction()));
return false;
@Override
public boolean onDoubleTap(MotionEvent e){
Log.i(getClass().getName(), "onDoubleTap-----" +getActionName(e.getAction()));
return false;
}
@Override
public booleanonDoubleTapEvent(MotionEvent e) {
Log.i(getClass().getName(),"onDoubleTapEvent-----" + getActionName(e.getAction()));
return false;
}
@Override
public booleanonSingleTapConfirmed(MotionEvent e) {
Log.i(getClass().getName(), "onSingleTapConfirmed-----" +getActionName(e.getAction()));
return false;
}
}
}
这里的返回值应该是是否将点击事件消耗掉的。
对于fling手势的操作,为了让控件有fling的效果,我们是在得到fling速度之后,计算最终要滑动到的位置,然后进行移动。感觉用scroller来处理fling感觉效果好很多。
DragViewhelper
参考:https://www.kancloud.cn/digest/fastdev4android/109672
创建方法:
参数1.一个ViewGroup,也就是拖动子View的父控件(ViewGroup)
参数2.灵敏度一般设置成1.0f,表示灵敏度最敏感
参数3.拖拽回调,用来处理拖动的位置等相关操作
mDragHelper=ViewDragHelper.create(this, 1.0f, newDragHelperCallback());
继承ViewGragHelper.Callback类实现一个抽象方法:tryCaptureView()然后在内部进行处理需要捕获的子View(用于拖拽操作和移动)
/**
* 进行捕获拦截,那些View可以进行drag操作
* @param child
* @param pointerId
* @return 直接返回true,拦截所有的VIEW
*/
@Override
publicbooleantryCaptureView(Viewchild,int pointerId){
returntrue;
}
这样表示捕捉所有的子View,表示所有的子View都可以进行拖拽操作。
/**
* 在边界滑动的时候同时滑动dragView2
* @param edgeFlags
* @param pointerId
*/
@Override
publicvoidonEdgeDragStarted(int edgeFlags, int pointerId){
mDragHelper.captureChildView(view_two, pointerId);
}
我们还可以这样调用tryCaptureView方法。
重写onInterceptTouchView和onTouchEvent方法来拦截事件以及让ViewDragHelper来进行处理拦截到得事件
因为ViewDragHelper的内部是根据触摸等相关事件来实现拖拽的。
/**
* 事件分发
* @param ev
* @return
*/
@Override
publicbooleanonInterceptTouchEvent(MotionEvent ev){
returnmDragHelper.shouldInterceptTouchEvent(ev);
}
@Override
publicbooleanonTouchEvent(MotionEvent ev){
mDragHelper.processTouchEvent(ev);
returntrue;
}
拖动行为处理
例如我们现在需要处理横向的拖拽的,那么我们需要实现clampViewPositionHorizontal方法,并且返回一个适当的数值表示横向拖拽的效果。 一般返回第二个参数即可,不过需要对边界值做一下判断这样子View不要拖出屏幕。
【注】要实现横向滑动这个方法必须要重写,因为ViewGragHelper内部该方法的时候直接返回了0,看下内部实现代码:
public intclampViewPositionHorizontal(Viewchild, int left, int dx) {
return 0;
}
我们重写的代码如下:
/**
* 水平滑动控制left
* @param child
* @param left
* @param dx
* @return
*/
@Override
public int clampViewPositionHorizontal(View child, int left, int dx) {
Log.d("DragLayout","clampViewPositionHorizontal " +left + "," + dx);
final int leftBound =getPaddingLeft();
final int rightBound = getWidth() -view_one.getWidth();
final int newLeft =Math.min(Math.max(left, leftBound), rightBound);
return newLeft;
}
这了的返回的left值就是最后拖动要到达的left值。上面的代码是防止我们拖动出显示的屏幕。
同样对于垂直方向拖拽的重写clampViewPositionVertical()基本方法也差不多如下:
/**
* 垂直滑动,控制top
* @param child
* @param top
* @param dy
* @return
*/
@Override
public int clampViewPositionVertical(View child, int top, int dy) {
Log.d("DragLayout","clampViewPositionVertical " +top + "," + dy);
final int topBound =getPaddingTop();
final int bottomBound = getHeight()- view_one.getHeight();
final int newTop =Math.min(Math.max(top, topBound), bottomBound);
return newTop;
}
其他方法:
dragHelper.smoothSlideViewTo(mMainContent,finalLeft,0);
这里不同于scroll,draghelper直接由自己的平滑滑动的方法,很方便。有点像scroll和scrollto+computscroll的关系,很微妙。
其实去看源码,发现dragViewHelper里面的滑动平滑实际上还是用的是scroller来实现的,所以一定要去computscroll再去处理一下。
正确结合:
if(isSmooth){ // 平滑动画 // Scroller // 1. 触发一个平滑动画 if(dragHelper.smoothSlideViewTo(mMainContent, finalLeft, 0)){ // true 当前动画还没有结束, 没有指定位置, 需要重绘界面. ViewCompat.postInvalidateOnAnimation(this); } }else { mMainContent.layout(finalLeft, 0, finalLeft + mWidth, 0 + mHeight); }
}
在做平滑处理之后既然用到了动画和重绘,那么需要借助computscroll的力量
@Override public void computeScroll() { super.computeScroll(); if(dragHelper.continueSettling(true)){ ViewCompat.postInvalidateOnAnimation(this); } }
通过判断是否是平滑动画作出的策略。一般在release的时候去做这个操作!
在这里Draghelper有一个continueSettling的方法
这个方法是:通过当前一列的动作移动我们当前捕获到的View模块。如果返回true调用者将在下一次的的框架中被调用。同时,它的内部其实是封装的scrollto方法,帮助之前的平滑移动中的scroller做真正的移动。
如果你需要调用computeScroll()方法或者类似的方法作为重绘布局的一部分,要设置它为true。
ViewCompat
在DragViewHelper中提到了ViewCompat,那么我们就把他给弄明白。
ViewCompat类主要是用来解决向下兼容性问题的。实际上在dragViewHelper中将其换做invalidate实际上并没有什么问题。为了让draghelper的动画能够向下兼容下所以还是用了ViewCompat对象。
实际上没怎么用到过,就是看demo的时候会看到有人用。所以仅仅是了解下。
PathMesure:
参考:http://blog.csdn.net/u013831257/article/details/51565591
PathMeasure是一个用来测量Path的类,相对于path来说,path仅仅用于存储路径,,获取路径的具体信息,需要通过PathMesure来搞定,其主要方法有:
具体使用:
以path为参数,生成pathMeasure对象
pathMeasure和值动画结合,获取对应时间的pos
在onDraw方法中根据点进行绘制。
常用方法
View可改变属性
可改变的属性包括:平移(setTranslationX),缩放(setScale),旋转(setRotate),透明度(setAlpha)。
computeScroll
首先需要更正下错误概念!computeScroll和Scroller都是计算结果来影响scrollTo,scrollBy,从而使得滑动发生改变。也就是Scroller不会调用computeScroll,反而是computeScroll调用Scroller。而computeScroll是由draw方法调用的。
@Override
publicvoid computeScroll() {
if(mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
invalidate();
}
}
这里computeScrollOffset()方法判断滑动是否结束,他的内部代码通过:1、是否在滑动的标志flag;2、是否到达指定的duration;3、目标坐标是否和当前坐标一致;来判断滑动是否结束。
判断是否可移动
if(Math.abs(moveY-mDownX) >mTouchSlop&& (Math.abs(moveY-mDownY)> (Math.abs(moveX-mDownX)))){
returntrue;
}
这个方法比较厉害!当我们做单方向滑动时防止手滑造成另外一个方向。
获取自定义属性
在使用自定义属性的时候,我们有两种方式拿到自定义属性
1、 将属性定义在attrs中。参考:http://blog.csdn.net/iispring/article/details/50708044
2、将属性定义在styables中。参考:http://www.jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0421/2768.html
在attrs中的属性的使用
定义属性:
<declare-styleable name="ntm"> <attr name="haha" format="string" /> <attr name="caca" format="string" /> </declare-styleable>
使用属性:
<reemii.a3dtouch_view.custom.SwitchCanvasBtn android:layout_width="120dp" android:layout_height="50dp" android:background="@color/colorAccent" app:slideButtonBackground="@drawable/slide_bg_btn" app:switchState = "false" app:haha = "haha"/>
对属性进行赋值:
int count = attrs.getAttributeCount(); //遍历AttributeSet中的XML属性 for(int i = 0; i < count; i++){ //获取attr的资源ID int attrResId = attrs.getAttributeNameResource(i); switch (attrResId){ case R.attr.haha: //customText属性 attributeName = attrs.getAttributeValue(i); break; } }
这里的属性使用姿势需要牢记,不然感觉很容易out of index。
在styable中的属性的使用:
定义自定义属性:
<declare-styleable name="SlideSwitchView"> <!-- 开关背景图片,值的格式为引用 --> <attr name="switchBackground" format="reference" /> <!-- 滑动块背景图片,值的格式为引用 --> <attr name="slideButtonBackground" format="reference" /> <!-- 开关的状态,值的格式为boolean --> <attr name="switchState" format="boolean" /> </declare-styleable>
我之前定义在attrs中感觉找不到,然后定义到styable中就找到了。还需要看下。
使用属性:
<reemii.a3dtouch_view.custom.SwitchCanvasBtn android:layout_width="120dp" android:layout_height="50dp" android:background="@color/colorAccent" app:slideButtonBackground="@drawable/slide_bg_btn" app:switchState = "false"/>
对属性赋值
public SwitchCanvasBtn(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); // 初始化默认的参数 initDefaultConfiguration(); TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.SlideSwitchView); Drawable mSlideButtonBackground = ta.getDrawable(R.styleable.SlideSwitchView_slideButtonBackground); Drawable mSwitchBackground= ta.getDrawable(R.styleable.SlideSwitchView_switchBackground); currentSwitchState = ta.getBoolean(R.styleable.SlideSwitchView_switchState,true);
}
两者的区别
其实放在哪里都无所谓,重点是看怎么获取这个很重要。目前项目还没有碰到有太多冲突的地方。
一些易混淆概念
Invalidate和postinvalidate的区别
invalidate()得在UI线程中被调动,在工作者线程中可以通过Handler来通知UI线程进行界面更新。
而postInvalidate()在工作者线程中被调用。
GetMeasureWidth()、getwidth()、getRawX()、getX()、getLeft()的区别
getWidth得到是某个view的实际尺寸。
getMeasuredWidth是得到某view想要在parent view里面占的大小.
getMeasuredWidth():先看一下API裡面怎麼说的
The width of this view as measured in the mostrecent call to measure(). This should be used during measurement and layoutcalculations only.
得到的是在最近一次调用measure()方法测量后得到的view的宽度,它仅仅用在测量和layout的计算中。
所以此方法得到的是view的内容佔据的实际宽度。
getWidth(): View在设定好布局后整个View的宽度。
getMeasuredWidth(): 对View上的内容进行测量后得到的View内容佔据的宽度,前提是你必须在父布局的onLayout()方法或者此View的onDraw()方法裡调 用measure(0,0);(measure 参数的值你可以自己定义),否则你得到的结果和getWidth()得到的结果一样。
这两个方法的区别就是看你有没有用measure()方法,当然measure()的位置也是很重要的。
其实上面几个得到的数据的位置也是不一样的。
getMeasuredWidth是在ommeasure获得的。
getWidth()方法要在layout()过程结束后才能获取到,他的返回值是mRight-mLeft。
getScrollX():返回的是左边滑动之后的x坐标位置;返回值是pixels
getScrollY():返回的是顶部滑动之后的y坐标位置,返回值是pixels
是个坐标意义的值。
onmeasure中的widthMeasureSpec
用int类型占有32位,它将其高2位作为mode,后30为作为size这样用32位就解决了size和mode的问题。
Eg:
mode是EXACTLY,而size=101(5)那么size+mode的值为:
假如是自定义一个View的话,测量一下其大小就行了,如果是ViewGroup呢,则需要遍历其所有的子View来,并为每个子View测量它的大小
在测量子View时需要两个参数,measureWidth和measureHeight这两个值是根布局传过来的,也就是说是父View和子View本身共同决定子View的大小。
dispatchdraw和ondraw的区别
draw过程会调用onDraw(Canvas canvas)方法,然后就是dispatchDraw(Canvas canvas)方法, dispatchDraw()主要是分发给子组件进行绘制,我们通常定制组件的时候重写的是onDraw()方法。值得注意的是ViewGroup容器组件的绘制,当它没有背景时直接调用的是dispatchDraw()方法,而绕过了draw()方法,当它有背景的时候就调用draw()方法,而draw()方法里包含了dispatchDraw()方法的调用。因此要在ViewGroup上绘制东西的时候往往重写的是dispatchDraw()方法而不是onDraw()方法。
自定义控件绘制步骤
setContentView(resourceID)过程