本文为Android开发艺术探索的笔记,仅供学习
首先View虽然不是四大组件,但是它的作用和重要性甚至比Receiver和Provider要重要的多。View是Android提供的控件的基类,然而这些控件远远不能满足我们日常开发的需求,所以我们需要工具需求去自定义新的空间。一个典型的场景就是滑动屏幕,很多情况下我们的应多都支持滑动操作,当处于不同级别的View都去相应用户的滑动操作的时候,就会带来一个问题。滑动冲突!想要处理这个问题我们就要去理解View的分发机制,如何去分发?如何去拦截?我们会在后续中去讲解。
1 View的基本知识
现在我们先来了解一下View的一些基本知识,以为后面更好的介绍做铺垫。View的位置参数,MotionEvent和TouchSlop对象,VelocityTracker和Scroll对象,以便大家更好的去了解去理解一些复杂的内容。
1.1 什么是View
View,在前面也说了是所有Android控件的基类,不管是Button,TextView还是复杂的RelativeLayout它们的基类都是View。除了View,还有ViewGroup,从名字上看ViewGroup里面会有很多个View,对ViewGroup就是一个控件组,它里面可以有很多个View,But ViewGroup也继承了View。关于ViewGroup我们可以这么理解,它里面可以有很多个View,这种关系就是一种View的树的结构。LinearLayout它既是一个View,也可以是一个ViewGroup。
我们图来表示一下,因为图是最直观的
再附上一张控件的结构层次表
]( http://upload-images.jianshu.io/upload_images/3986342-c67678af1a40f908.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
1.2 View的位置参数
View的位置有四属性来决定,top left right bottom,top对应的是左上角的纵坐标,ldft对应的是左上角的横坐标,right对应的是右下角的横坐标,bottom对应的是右下角的纵坐标。(注意这些坐标都是相对与父控件的。)还是如此我们来花个图来解释一下吧
从上图,我们可以得到View的高是bottom-top View的宽是right-left,那么你们会问如何获取到这四个属性的值呢,其实只要通过getTop getLeft 就可以获取相应的值。现在我们再提四个参数,X,Y,translationX,translationiY。X,Y是View左上角的左上角的坐标,而translationX和translationY,则表示可偏移量,这几个参数相当于父控件的坐标,而且translationX和translationY的初始值都是0,同样View也为他们提供了Get和Set的方法。
这几个参数的关系式 X = left + translationX Y = top + translationY
需要注意的是View在平移的时候,top和left表示的是原始左上角的位置,其值并不会该表,此时发送改变的是X,Y,translationX,translationY.
1.3 MotionEvent 和 TouchSlop
1.MotionEvent
在手指接触屏幕后发生的一系列事件,典型的有以下几种
- ACTION_DOWN------手指刚接触屏幕的时候
- ACTION_MOVE-------手指在屏幕上移动的时候
- ACTION_UP-------手指离开屏幕的时候
正常情况下,一次手指接触屏幕会出发两种情况
- 第一种,DOWN-->UP 当点击屏幕马上离开
- 第二种,DOWN-->MOVE-->...-->MOVE-->UP 当点击屏幕并且在屏幕上移动在离开
- 第三种,DOWN-->MOVE-->...-->MOVE 就是点击屏幕,并且移动,移动到屏幕外面
上述三种是典型的事件顺序,同时我们可以通过MotionEvent去获取点击时间发送的X,Y的坐标。系统提供了两组方法,getX\getY, getRawX\getRawY,其中第一组是用来返回当前View的左上角的x y坐标,第二组方法是用来返回手机屏幕左上角的x y坐标。那么我们还是看图说话吧
2 TouchSlop
TouchSlop就是系统所能识别的最小滑动距离,也就是说当用于滑动的距离小于该值则视为无效滑动。这是一个常量,不同的设备是不一样的。我们可以通过ViewConfiguration.get(getApplicationContext()).getScaledTouchSlop(); 来获取最小滑动距离。为了让大家更好的理解附上源码
可以看到最小识别的距离为8dp
1.4 VelocityTracker和Scroll
1 VelocityTracker
VelocityTracker通过跟踪一连串事件实时计算出当前的速度,通过它我们可以得数水平滑动和竖直滑动的速率,一般作用在onTouchEvent方法中,主要用到下面几个方法
addMovement(MotionEvent)函数将Motion event加入到VelocityTracker类实例中
getXVelocity() 或getXVelocity()获得横向和竖向的速率到速率时,computeCurrentVelocity(int)来初始化速率的单位 。
VelocityTracker.obtain();获得VelocityTracker类实例
话不多说直接上代码。
onTouchEvent(MotionEvent ev){
if (mVelocityTracker == null) {
mVelocityTracker = VelocityTracker.obtain();//获得VelocityTracker类实例
}
mVelocityTracker.addMovement(ev);//将事件加入到VelocityTracker类实例中
//判断当ev事件是MotionEvent.ACTION_UP时:计算速率
// 1000 provides pixels per second
velocityTracker.computeCurrentVelocity(1, (float)0.01); //设置maxVelocity值为0.1时,速率大于0.01时,显示的速率都是0.01,速率小于0.01时,显示正常
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());
velocityTracker.computeCurrentVelocity(1000); //设置units的值为1000,意思为一秒时间内运动了多少个像素
Log.i("test","velocityTraker"+velocityTracker.getXVelocity());
}
2 Scroll
弹性滑动对象,用于实现View的弹性滑动。我们知道,当使用View的scrollTo/scrollBy方法来进行滑动时,其过程是瞬间完成,没有过渡效果的滑动用户体验不好。这个时候就需要使用Scroller来实现有过渡效果的滑动,大致实现过程后面会详细介绍,下面就附上实现代码。
Scroller scroller;
scroller = new Scroller(context);
//调用此方法滚动到目标位置
public void smoothScrollTo(int fx, int fy, boolean back) {
int dx = fx;
int dy = fy;
smoothScrollBy(dx, dy);
}
//调用此方法设置滚动的相对偏移
public void smoothScrollBy(int dx, int dy) {
//设置scroller的滚动偏移量
scroller.startScroll(scroller.getFinalX(), scroller.getFinalY(), dx, dy);
invalidate();//这里必须调用invalidate()才能保证computeScroll()会被调用,否则不一定会刷新界面,看不到滚动效果}
}
@Override
public void computeScroll() {
//先判断scroller滚动是否完成
if (scroller.computeScrollOffset()) {
//这里调用View的scrollTo()完成实际的滚动
scrollTo(scroller.getCurrX(), scroller.getCurrY());
//必须调用该方法,否则不一定能看到滚动效果
postInvalidate();
}
super.computeScroll();
}
2 View的滑动
在View的事件体系(1)中,我们已经了解到View的基本知识,这一节要来讲解很重要的东西就是View的滑动。在Android的设备上,滑动可以说以一种标配,不管是下拉刷新还是什么,他们的基础都是滑动。不管是任何酷炫的滑动效果,归根结底他们都是由不同的滑动和一些动画组成。所以我们还有必要去了解滑动的基础,接下来我们来了解三种实现滑动的方法。
1.View通过自生的scrollTo/scrollBy来实现滑动
2.通过动画来给View添加平移的效果来实现滑动
3.改变View的LayoutParams使得View重新布局来实现滑动。
2.1使用scrollTo/scrollBy
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);
}
从源码上可以看出,scrollBy实际上也是调用了scrollTo方法,它实现了基于当前位置的相对滑动,而scrollTo是实现了基于所传参数的绝对滑动。利用这两个方法我们就可以实现View的滑动,但是我们要明白滑动过程中View的两个内部属性的作用mScrollX mScrollY,这两个参数我们可以通过getScrollX getScrollY去获取。mScrollX的总值等于View内容原始左边缘到View内容现在左边缘的水平方向的距离,mScrollY的总值等于View内容原始上边缘到View内容现在上边缘的水平方向的距离。记住一句话上正下负右正左负,意思就是内容的上边缘在View的上边缘的上面,mScrollY为正,其他同理,给大家一个图便于理解
切记,再怎么滑动不能将View的位置进行改变,只能改变View内容的位置,比如TextView改变里面的文字
在 使用 getScrollY() 方法的时候,就是 getScrollY()的值 一直是 1.0
解决:通过查看 getScrollY() 方法 ,发现它有两个 返回值 一个 int , 一个 float , 后 将值 赋值给 int 类型后,就可以使用了;而直接 相加的为 float 类型;
2.2 使用动画
通过动画我们可以让View进行平移,而平移也是一种滑动。我们可以使用View动画也可以使用属性动画。
//View动画
Animation animation = AnimationUtils.loadAnimation(this, R.anim.demo);
tv1.startAnimation(animation);
//属性动画
ObjectAnimator.ofFloat(tv1,"translationY",150).start();
ValueAnimator animator = ObjectAnimator.ofInt(tv1, "backgroundColor", 0xFFFF8080, 0xFF8080FF);
animator.setDuration(3000);
animator.setEvaluator(new ArgbEvaluator());
animator.setRepeatCount(5);
animator.setRepeatMode(ValueAnimator.REVERSE);
animator.start();
切记,我们对通过动画对View的移动其实是对View的影像的移动,若我们不把fillAfter设为true的话,移动完后又会回到起点,若为true则会保留不动。但我们也View设置一个点击事件的时候,就要区分动画的类型,若是View动画则点击移动后的View却触发不了点击事件,若是属性动画则点击移动后的View却触发点击事件。针对View动画的解决方案,我们需要在移动后的位置再建立一个通向的View。
2.3 改变布局参数
我们可以改变布局参数LayoutParams ,让我们想让一个Button向右移动100dp,那么我们只要设施其marginleft就可以了,还可以这这个Button设置一个宽度为0的View,改变其的宽度为,那个Button就会自动被挤到右边。
2.4各种滑动方式的对比
scrollTo/scrollBy,这种方法其实是View提供的原生的滑动方式,他可以实现View的滑动也不影响其内部的点击事件,缺点就是只能滑动View的内容
动画滑动,如果是android3.0以上的话可以采用属性动画,这种方法并没有什么缺点,如果是3.0一下的话就绝不能改变View本生的属性。实际上如果动画不需要响应用户的交互,那么这种滑动方式是比较合适的。但是动画有一个明显的有点,就是一些复杂的效果必须通过动画来实现。
改变布局的方式,除了使用起来麻烦以外就没有什么明显的缺点了,非常适合对象具有交互的View,因为这些View是要与用户交互,直接通过动画会有问题。
总结一下就是
scrollTo/scrollBy:操作简单,适合对View内容的滑动
动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果
改变布局参数:操作稍微复杂,适用于有交互的View
下面附上一个拖动的Demo
public class Move_textview extends TextView {
String TAG = "move";
public Move_textview(Context context) {
this(context, null);
}
private int mScaledTouchSlop;//可识别的最小滑动距离
// 分别记录上次滑动的坐标
private int mLastX = 0;
private int mLastY = 0;
public Move_textview(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public Move_textview(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init();
}
private void init() {
mScaledTouchSlop = ViewConfiguration.get(getContext())
.getScaledTouchSlop();
Log.d(TAG, "sts:" + mScaledTouchSlop);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int x = (int) event.getRawX();
int y = (int) event.getRawY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN: {
break;
}
case MotionEvent.ACTION_MOVE: {
int move_x = x - mLastX;
int move_y = y - mLastY;
int translationX = (int) ViewHelper.getTranslationX(this) + move_x;
int translationY = (int)ViewHelper.getTranslationY(this) + move_y;
ViewHelper.setTranslationX(this, translationX);
ViewHelper.setTranslationY(this, translationY);
}
case MotionEvent.ACTION_UP: {
break;
}
default:
break;
}
mLastX = x;
mLastY = y;
return true;
}
}
3.弹性滑动
知道了View的滑动,但是这样的滑动有时候是比较生硬的,用户体验太差了,所以我们要去了解弹性滑动,就是将一次滑动分成若干个小滑动。主要是通过Scroller,handler #postDelaked,Thread#sleep
3.1Scroller的使用
我们先来看看Scroller的最基本的使用
Scroller scroller = new Scroller(context);
private void smoothScrollTo(int destX, int destY) {//自己写的方法
int scrollX = getScrollX();//View的内容的左边缘到View左边缘的距离
int deltaX = destX + scrollX;//加上要移动的距离后的位置
scroller.startScroll(scrollX, 0, deltaX, 0);
invalidate();注解1
}
@Override
public void computeScroll() {
if (scroller.computeScrollOffset()) {
scrollTo(scroller.getCurrX(), scroller.getCurrY());
postInvalidate();
}
}
我们可以看到显示构造了一个Scroller对象,在调用它的startScroll方法,其实Scroller内部什么都没做就是用来保存几个参数
public void startScroll(int startX, int startY, int dx, int dy, int duration) {
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAnimationTimeMillis();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float) mDuration;
}
startX,startY是起始位置,dx dy是要滑动的距离,duration就是在规定的时间内滑动
那么Scroller到底是怎么进行弹性滑动的呢?注解1invalidate()
大致的流程是这样子的,invalidate方法会导致View去重绘,ondraw是绘制的方法,改方法又会去调用ComputeScroll方法,此时我们需要对ComputeScroll进行重写,在里面去判断弹性滑动是否结束,没结束就再获取Scroller当前的位置,在去进行第二次绘制,直至弹性滑动结束。
那我们来看看Scroller的computeScrolloffset方法
public boolean computeScrollOffset() {
if (mFinished) {
return false;
}
int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
if (timePassed < mDuration) {
switch (mMode) {
case SCROLL_MODE:
final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
mCurrX = mStartX + Math.round(x * mDeltaX);
mCurrY = mStartY + Math.round(x * mDeltaY);
break;
case FLING_MODE:
final float t = (float) timePassed / mDuration;
final int index = (int) (NB_SAMPLES * t);
float distanceCoef = 1.f;
float velocityCoef = 0.f;
if (index < NB_SAMPLES) {
final float t_inf = (float) index / NB_SAMPLES;
final float t_sup = (float) (index + 1) / NB_SAMPLES;
final float d_inf = SPLINE_POSITION[index];
final float d_sup = SPLINE_POSITION[index + 1];
velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
distanceCoef = d_inf + (t - t_inf) * velocityCoef;
}
mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
// Pin to mMinX <= mCurrX <= mMaxX
mCurrX = Math.min(mCurrX, mMaxX);
mCurrX = Math.max(mCurrX, mMinX);
mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
// Pin to mMinY <= mCurrY <= mMaxY
mCurrY = Math.min(mCurrY, mMaxY);
mCurrY = Math.max(mCurrY, mMinY);
if (mCurrX == mFinalX && mCurrY == mFinalY) {
mFinished = true;
}
break;
}
}
else {
mCurrX = mFinalX;
mCurrY = mFinalY;
mFinished = true;
}
return true;
}
该方法就是根据流逝时间的百分比会算去scrollX和scrollY,当返回true的时候表示弹性滑动还没结束,false就表示弹性滑动结束
那么现在来总结一下,scroller本生是不能滑动的,它需要配合computeScroll来实现弹性滑动,它会不断的去重绘View,每次重绘是有时间间隔的,通过这个时间间隔和Scroller的computeScrolloffset方法来返回相应的位置,通过View自身的scrollTo和返回来的位置去移动View。这个思想很巧妙,竟然连计时器都没有用到。
注意:滑动的还是View的内容而不是View
3.2 通过动画
动画本来就是一种渐进的过程,因此通过它来实现的滑动天然就具有弹性效果,比如以下代码可以让一个Button实现一个宽度的变化动画。
private static class View_button {
View view;
public View_button(View view) {
this.view = view;
}
public int getWidth() {
return view.getLayoutParams().width;
}
public void setWidth(int w) {
view.getLayoutParams().width = w;
view.requestLayout();
}
}
ObjectAnimator.ofInt(new View_button(tv1),"width",500).setDuration(3000).start();
至于属性动画的详情,在后续中会详细介绍
3.3 使用延时策略
通过Handler里去改变控件的位置。
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;
}
};
};