您的位置:首页 > 移动开发 > Android开发

Android开发艺术探索3

2016-08-07 19:20 531 查看

Android开发艺术探索3

该系列文章为《Android开发艺术探索》读书笔记,仅作为学记录,勿喷。

View基础知识

1 View是Android中所有控件的基类,是一种界面层的控件的一种抽象,它代表了一个控件

2 viewgroup 继承 view

3 top、left、right、bottom,分别对应View的左上角和右下角相对于父容器的横纵坐标值



x = left + translationX
y = top + translationY
Left = getLeft()
Right = getRight()
Top = getTop()
Bottom = getBottom()


4 从Android 3.0 开始,View增加了额外的几个参数:x、y、translationX和translationY,其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于父容器的偏移量。这几个参数也是相对于父容器的坐标,并且translationX和translationY的默认值为0

x = left + translationX
y = top + translationY


5 View在平移的过程中,top和left表示的是原始左上角的位置信息,其值并不会发生改变,此时发生改变的是x、y、translationX和translationY这四个参数

6 MotionEvent 手指接触屏幕后所产生的一系列事件,类型有
ACTION_UP
ACTION_DOWN
ACTION_MOVE


一次手指触摸屏幕的行为会触发一系列点击事件

* 点击屏幕后离开松开,事件序列为DOWN –> UP

* 点击屏幕滑动一会再松开,事件序列为DOWN –> MOVE –> … –> MOVE –> UP

通过MotionEvent对象我们可以得到点击事件发生的x和y坐标,getX/getY返回的是相对于当前View左上角的x和y坐标,而getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标

7 TouchSlope 是系统所能识别出的可以被认为是滑动的最小距离

获取方式 ViewConfiguration.get(getContext()).getScaledTouchSlop()

8 VelocityTracker用于追踪手指在滑动过程中的速度,包括水平和垂直方向上的速度。

使用过程

//在View的onTouchEvent方法中追踪当前单击事件的速度
VelocityTracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);
//想知道当前的滑动速度时,这个时候可以采用如下方法来获得当前的速度
velocityTracker.computeCurrentVelocity(100);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();
//当不需要使用它的时候,需要调用clear方法来重置并回收内存
velocityTracker.clear();//一般在MotionEvent.ACTION_UP的时候调用
velocityTracker.recycle();//一般在onDetachedFromWindow中调用


速度 = (终点位置 - 起点位置) / 时间段

速度可以为负,computeCurrentVelocity这个方法的参数表示的是一个时间单元或者说时间间隔,单位是毫秒(ms),计算速度时得到的速度就是在这个时间间隔内手指在水平或竖直方向上所滑动的像素数。针对上面的例子,如果我们通过velotictyTracker.computeCurrentVelocity(100)来获取速度,那么得到的速度就是手指在100ms内所滑过的像素数,因此水平速度就成了10像素/每100ms(这里假设滑动过程是匀速的),即水平速度为10

9 GestureDetector 用于辅助检测用户的单击、滑动、长按、双击等行为。

使用方式

//创建一个GestureDetector对象并实现OnGestureListener接口,根据需要还可以实现OnDoubleTapListener从而能够监听双击行为
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);
//接管目标View的onTouchEvent方法,在待监听View的onTouchEvent方法中添加如下实现
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;


做完了上面两步,我们就可以有选择地实现OnGestureListener和OnDoubleTapListener中的方法了,这两个接口中的方法介绍

如图所示:



建议:如果只是监听滑动相关的事件在onTouchEvent中实现;如果要监听双击这种行为的话,那么就使用GestureDetector

10 Scroller 弹性滑动对象,用于实现View的弹性滑动。View的scrollTo/scrollBy滑动过程是瞬间完成,没有过渡效果体验不好。Scroller实现有过渡效果的滑动,不是瞬间完成的,是在一定的时间间隔内完成的。Scroller本身无法让View弹性滑动,它需要和View的computeScroll方法配合使用。

Scroller scroller = new Scroller(mContext);

//缓慢滚动到指定位置
private void smoothScrollTo(int destX, int destY) {
int scrollX = getScrollX();
int delta = destX - scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
mScroller.startScroll(scrollX, 0, delta, 0, 1000);
invalidate();
}
public void computeScroll() {
if(mScroller.computeScrollOffset()) {
scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
postInvalidate();
}
}


View的滑动

实现view的滑动的方式有三种:

第一种是通过view本身提供的scrollTo和scrollBy方法:操作简单,适合对view内容的滑动;

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的mScrollX和mScrollY可以通过getScrollX和getScrollY得到。滑动中,mScrollX的值总等于View左边缘和View内容左边缘在水平方向上的距离,mScrollY的值总等于View上边缘和View内容上边缘在竖直方向上的距离。

scrollTo/scrollBy只能改变View内容的位置而不能改view在布局中的位置

mScrollX和mScrollY的单位为像素,并且当view左边缘在view内容左边缘的右边时,mScrollX为正值,反之为负值;当view上边缘在view内容上边缘的下边时,mScrollY为正值,反之为负值。即从左往右滑动,mScrollX为负值,反正为正值;从上往下滑动,mScrollY为负值,反之为正值。这里为好理解从网上取个例子,参考地址

scrollTo(int x, int y) 是将View中内容滑动到相应的位置,参考的坐标系原点为parent View的左上角。这里图中的scrollX(mScrollX,mScrollY)这样更好理解

同理,scrollTo(0, 100)的效果如下图所示:



scrollTo(100, 100)的效果图如下:



若函数中参数为负值,则子View的移动方向将相反。



第二种是通过动画给view施加平移效果来实现滑动:操作简单,适用于没有交互的view和实现复杂的动画效果;

View动画是对view的影像做操作,它并不是能真正的改变view的位置参数,包括宽/高,并且要是动画后的状态保留必须将fillAfter设置为true,否则动画完成后结果会消失

属性动画没有上述问题

Android3.0以下无法使用属性动画,可以通过nineoldandroids实现,本质任然是view动画

第三种是通过改变view的LayoutParams使得view重新布局从而实现滑动:操作稍微复杂,适用于有交互的view

弹性滑动

1 使用Scroller

典型使用

Scroller scroller=new Scroller(context);
//缓慢滚动到指定位置
private void smoothScrollTo(int destX,int destY){
int scrollx=getScrollX();
int deltaX=destX-scrollX;
//1000ms内滑向destX,效果就是慢慢滑动
scroller.startScroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@Override
public void computeScroll(){
if(scroller.computeScrollOffset()){
scrollTo(scroller.getCurrX(),scroller.getCurrY());
postInvalidate();
}
}


Scroller的工作机制:Scroller本身并不能实现View的滑动,它需要配合View的computeScroll方法才能完成弹性滑动的效果,它不断的让View重绘,而每一次重绘距滑动起始时间会有一个时间间隔,通过这个时间间隔Scroller就可以得出View当前的滑动位置,知道了滑动位置就可以通过scrollTo方法来View的滑动。就这样,View的每一次重绘都会导致View进行小幅度的滑动,而多次的小幅度滑动就组成了弹性滑动。

2 通过动画

final int startX = 0;
final int deltaX = 100;
ValueAnimator animator = ValueAnimator.ofInt(0,1).setDuration(1000);
animator.addUpdateListener(new AnimatorUpdateListener(){
@Override
public void onAnimationUpdate(ValueAnimator animator){
float fraction = animator.getAnimatedFraction();
mButton.scrollTo(startX+(int)(deltaX*fraction),0);
}
});
animator.start();


动画本质没有作用月任何对象上,只是在1000ms内完成了整个动画过程。利用这个特性,就可以在动画的每一帧到来时获取动画完成的比例,然后根据比例计算成当前view所要滑动的距离。这里的的滑动针对的是view的内容而非view本身。该方法思想和scroller比较相似,都是通过改变一个百分比配合scrollTo方法来完成view的滑动。这种方式也可完成其他动画效果。

3. 使用延时策略

通过发一些列延时消息达到一种渐进式的效果,具体可以使用handler或view的postDelay方法,也可以使用线程的sleep方法。postDelay方法可以通过它来发一个消息在消息中进行view的滑动,接连不断的发就可以实现弹性滑动的效果。sleep可以在while循环中不断的滑动view和sleep,就可以实现弹性滑动的效果。

View的事件分发机制

1 点击事件的传递规则

点击事件的分发过程主要由三个方法来完成。

dispatchTouchEvent() :

用来进行事件的分发,如果事件能够传递给当前View,此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法影响,表示是否消耗当前事件。

onInterceptTouchEvent() :

在dispatchTouchEvent方法内部调用,判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列中,此方法不会在被调用,返回结果表示是否拦截当前事件。

onTouchEvent() :

在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到其他事件。

三个方法的关系如下:

public boolean dispatchTouchEvent(MotionEvent ev){
boolean consume=false;
if(onInterceptTouchEvent(ev)){
consume=onTouchEvent(ev);
}else{
consume=child.dispatchTouchEvent(ev);
}
return consume;
}


具体传递规则:对于一个根ViewGroup来说,点击事件产生以后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用,如果这个ViewGroup的onInterceptTouchEvent返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。

当一个点击事件产生后,它的传递过程遵循如下顺序:Activity->Window->View,即事件总是先传递给Activity,Activity在传递给Window,最后Window在传递给顶级View。顶级View接收到事件后,就会按照事件分发机制去分发事件。考虑一种情况,如果一个View的onTouchEvent返回false,那么它的父容器的onTouchEvent将会被调用,依此类推。如果所用的元素都不处理这个事件,那么这个事件将会最终传递给Activity处理,即Activity的onTouchEvent方法被调用。

几个重要结论:

事件序列是指从手指接触屏幕的那一刻起,到手指离开屏幕的那一刻结束,在这个过程中所产生的一系列事件,这个事件序列以down事件开始,中间含有数量不固定的move事件,最终以up事件结束。

正常情况下,一个事件序列只能被一个VIew拦截且消耗,因为一旦一个元素拦截了某个事件,那么同一个事件序列内的所有事件都会直接交给它处理,因此同一个事件序列中的事件不能分别有两个View同时处理,但是通过特殊手段可以做到,比如一个View将本该自己处理的事件通过onTouchEvent强行传递给其他View处理。

某个View一旦决定拦截,那么这一个事件序列都只能由它处理,并且它的onInterceptTouchEvent不会再被调用。

某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvent返回了false),那么同一事件序列中的其他事件都不会再交给它处理,并且事件将重新交由它的父元素去处理,即父元素的onTouchEvent会被调用。意思就是事件一旦交给一个View处理,那么它就必须消耗掉,否则同一事件序列中剩下的事件就不再交给它来处理了。

如果View不消耗出ACTION_DOWN以外的其他事件,那么这个点击事件会消失,最终这些消失的点击事件会传递给Activity处理。

ViewGroup默认不拦截任何事件。

View没有onInterceptTouchEvent方法,一旦有事件传递给它,那么它的onTouchEvent方法就会被调用。

View的OnTouchEvent方法默认都会消耗事件(返回true)。

View的enable属性不影响onTouchEvent的默认返回值。

onClick会发生的前提是当前View是可点击的,并且它收到了down和up的事件。

事件的传递过程是由外向内的,即事件总是项传递给父元素,然后再由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的事件分发过程,但是ACTION_DOWN事件除外。(requestDisallowInterceptTouchEvent方法主要设置父元素中的FLAG_DISALLOW_INTERCEPT标记位,一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件)

2 View的滑动冲突

常见的滑动冲突场景



解决滑动冲突的方式:外部拦截法和内部拦截法。

外部拦截法

 点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,如果不需要此事件就不拦截。

 实现方法主要是重写父容器的onInterceptTouchEvent方法,代码如下:

public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercepted=false;
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.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;
}


内部拦截法

父容器不拦截任何事件,所用的事件都传递给子元素,如果子元素需要此事件就直接消耗掉,否则就交由父容器进行处理。需要配合requestDisallowInterceptTouchEvent方法才能正常工作,同时重写子元素的dispatchTouchEvent方法

子元素:

public boolean dispatchTouchEvent(MotionEvent event) {
int x=(int)event.getX();
int y=(int)event.getY();
switch (event.getAction()){
case MotionEvent.ACTION_DOWN:
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX=x-mLastX;
int deltaY=y-mLastY;
if(父容器需要当前点击事件){
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastX=x;
mLastY=y;
return super.dispatchTouchEvent(event);
}


父元素:

public boolean onInterceptTouchEvent(MotionEvent event) {
int action=event.getAction();
if (action==MotionEvent.ACTION_DOWN){
return false;
}else{
return true;
}
}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  android android开发