JingBin's Home

《Android开发艺术探索》读书笔记

《Android开发艺术探索》 - 一本Android进阶类书籍,采用理论、源码和实践相结合的方式来阐述高水准的Android应用开发要点。

《Android开发艺术探索》从三个方面来组织内容。第一,介绍Android开发者不容易掌握的一些知识点;第二,结合Android源代码和应用层开发过程,融会贯通,介绍一些比较深入的知识点;第三,介绍一些核心技术和Android的性能优化思想。

《Android开发艺术探索》侧重于Android知识的体系化和系统工作机制的分析,通过《Android开发艺术探索》的学习可以极大地提高开发者的Android技术水平,从而更加高效地成为高级开发者。而对于高级开发者来说,仍然可以从《Android开发艺术探索》的知识体系中获益。

完善中,借鉴了他人的读书笔记。

Activity的生命周期和启动模式

用户正常使用情况下的生命周期 & 由于Activity被系统回收或者设备配置改变导致Activity被销毁重建情况下的生命周期。

Activity的生命周期全面分析

典型情况下的生命周期分析

Activity生命周期.png

  • 1.Activity第一次启动:onCreate->onStart->onResume。
  • 2.Activity切换到后台( 用户打开新的Activity或者切换到桌面),onPause->onStop。
  • 3.Activity从后台到前台,重新可见,onRestart->onStart->onResume。
  • 4.用户退出Activity,onPause->onStop->onDestroy。
  • 5.onStart开始到onStop之前,Activity可见。onResume到onPause之前,Activity可以接受用户交互。
  • 6.在新Activity启动之前,栈顶的Activity需要先onPause后,新Activity才能启动。所以不能在onPause执行耗时操作。

异常情况下的生命周期分析

系统配置变化导致Activity销毁重建

例如Activity处于竖屏状态,如果突然旋转屏幕,由于系统配置发生了改变,Activity就会被销毁并重新创建。

  • 在异常情况下系统会在onStop之前调用onSaveInstanceState来保存状态。Activity重新创建后,会在onStart之后调用onRestoreInstanceState来恢复之前保存的数据。
  • 保存数据的流程: Activity被意外终止,调用onSaveIntanceState保存数据-> Activity委托Window,Window委托它上面的顶级容器一个ViewGroup( 书上说很可能就是DecorView) 。然后顶层容器在通知所有子元素来保存数据。 每个View都有 onSaveInstanceState 和 onRestoreInstanceState 方法。查看TextView 源码可以发现保存了文本选中状态和文本内容。
  • 系统只在Activity异常终止的时候才会调用 onSaveInstanceState 和onRestoreInstanceState 方法。其他情况不会触发。
资源内存不足导致低优先级的Activity被回收
  • 1.前台- 可见非前台( 被对话框遮挡的Activity) -后台,这三种Activity优先级从高到低。
  • 2.android:configChanges=”orientation” 在manifest中指定 configChanges 在系统配置变化后不重新创建Activity,也不会执行onSaveInstanceState 和 onRestoreInstanceState 方法,而是调用 onConfigurationChnaged 方法。
  • 3.configChanges 一般常用三个选项:
    • locale 系统语言变化
    • keyborardHidden 键盘的可访问性发生了变化,比如用户调出了键盘
    • orientation 屏幕方向变化

Activity的启动模式

Activity的LaunchMode

Android使用栈来管理Activity。

standard
  • 每次启动都会重新创建一个实例,不管这个Activity在栈中是否已经存在。
  • 谁启动了这个Activity,那么Activity就运行在启动它的那个Activity所在的栈中。
  • 用Application去启动Activity时会报错,提示非Activity的Context没有所谓的任务栈。解决
    办法是为待启动Activity制定FLAG_ACTIVITY_NEW_TASH标志位,这样就会为它创建
    一个新的任务栈。
singleTop
  • 如果新Activity位于任务栈的栈顶,那么此Activity不会被重新创建,同时回调 onNewIntent 方法。
  • 如果新Activity已经存在但不是位于栈顶,那么新Activity仍然会被创建。
singleTask
  • 这是一种单实例模式
  • 只要Activity在栈中存在,那么多次启动这个Activity都不会重新创建实例,同时也会回调 onNewIntent 方法。
  • 同时会导致在Activity之上的栈内Activity出栈。
singleIntance
  • 具有singleTask模式的所有特性,同时具有此模式的Activity只能单独的位于一个任务栈中
TaskAffinity属性

TaskAffinity参数标识了一个Activity所需要的任务栈的名字。为字符串,且中间必须包含包名分隔符“.”。默认情况下,所有Activity所需的任务栈名字为应用包名。TashAffinity属性主要和singleTask启动模式或者 allowTaskReparenting 属性配对使用,其他情况下没有意义。 应用A启动了应用B的某个Activity后,如果Activity的allowTaskReparenting属性为true的话,那么当应用B被启动后,此Activity会直接从应用A的任务栈转移到应用B的任务栈中。 打个比方就是,应用A启动了应用B的ActivityX,然后按Home回到桌面,单击应用B的图标,这时并不会启动B的主Activity,而是重新显示已经被应用A启动的ActivityX。这是因为ActivityX的TaskAffinity值肯定不和应用A的任务栈相同( 因为包名不同) 。所以当应用B被启动以后,发现ActivityX原本所需的任务栈已经被创建了,所以把ActivityX从A的任务栈中转移过来了。

设置启动模式:

  • 1.manifest中 设置下的 android:launchMode 属性。
  • 2.启动Activity的 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 。
  • 3.两种同时存在时,以第二种为准。第一种方式无法直接为Activity添加FLAG_ACTIVITY_CLEAR_TOP标识,第二种方式无法指定singleInstance模式。
  • 4.可以通过命令行 adb shell dumpsys activity 命令查看栈中的Activity信息。

Activity的Flags

这些FLAG可以设定启动模式、可以影响Activity的运行状态。

  • FLAG_ACTIVITY_CLEAR_TOP 具有此标记位的Activity启动时,同一个任务栈中位于它上面的Activity都要出栈,一般和FLAG_ACTIVITY_NEW_TASK配合使用。效果和singleTask一样。
  • FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS 如果设置,新的Activity不会在最近启动的Activity的列表(就是安卓手机里显示最近打开的Activity那个系统级的UI)中保存。

IntentFilter的匹配规则

启动Activity分为两种:

  • 1.显示调用 明确指定被启动对象的组件信息,包括包名和类名
  • 2.隐式调用 不需要明确指定组件信息,需要Intent能够匹配目标组件中的IntentFilter中所设置的过滤信息。
  • 3.IntentFilter中的过滤信息有action、category、data。
  • 4.只有一个Intent同时匹配action类别、category类别、data类别才能成功启动目标Activity。
  • 5.一个Activity可以有多个intent-filter,一个Intent只要能匹配任何一组intent-filter即可成功启动对应的Activity。

action

  • 1.action是一个字符串。
  • 2.一个intent-filter可以有多个aciton,只要Intent中的action能够和任何一个action相同即可成功匹配。匹配是指与action的字符串完全一样。
  • 3.Intent中如果没有指定action,那么匹配失败。

category

  • category是一个字符串。
  • 2.Intent可以没有category,但是如果你一旦有category,不管有几个,每个都能够与
    intent-filter中的其中一个category相同。
  • 3.系统在startActivitystartActivityForResult的时候,会默认为Intent加上 android.intent.category.DEFAULT 这个category,所以为了我们的activity能够接收隐式调用,就必须在intent-filter中加上 android.intent.category.DEFAULT 这个category。

data

  • 1.data的匹配规则与action一样,如果intent-filter中定义了data,那么Intent中必须要定义可匹配的data。
  • 2.intent-filter中data的语法:

    <data android:scheme="string"
        android:host="string"
        android:port="string"
        android:path="string"
        android:pathPattern="string"
        android:pathPrefix="string"
        android:mimeType="string"/>
    
  • 3.Intent中的data有两部分组成:mimeType和URI。mimeType是指媒体类型,比如
    image/jpeg、audio/mpeg4-generic和video/等,可以表示图片、文本、视频等不同的媒
    体格式。

    • URI的结构:<scheme>://<host>:<port>/[<path>|<pathPrefix>|<pathPattern>]
      //实际例子
      content://com.example.project:200/folder/subfolder/etc
      http://www.baidu.com:80/search/info
      • scheme:URI的模式,比如http、file、content等,默认值是 file 。
      • host:URI的主机名
      • port:URI的端口号
      • path、pathPattern和pathPrefix:这三个参数描述路径信息。
        • path、pathPattern可以表示完整的路径信息,其中pathPattern可以包含通配符 * ,表示0个或者多个任意字符。
        • pathPrefix只表示路径的前缀信息。
    • Intent指定data时,必须调用 setDataAndType 方法, setData 和 setType 会清除另一方的值。

隐式调用需注意:

  • 1.当通过隐式调用启动Activity时,没找到对应的Activity系统就会抛出 android.content.ActivityNotFoundException 异常,所以需要判断是否有Activity能够匹配我们的隐式Intent。

    • i. 采用 PackageManager 的 resloveActivity 方法
      public abstract List queryIntentActivityies(Intent intent,int flags);
      public abstract ResolveInfo resloveActivity(Intent intent,int flags);
      以上的第二个参数使用 MATCH_DEFAULT_ONLY ,这个标志位的含义是仅仅匹配那些在intent-filter中声明了 android.intent.category.DEFAULT 这个category的Activity。因为如果把不含这个category的Activity匹配出来了,由于不含DEFAULT这个category的Activity是无法接受隐式Intent的从而导致startActivity失败。
    • ii. 采用 Intent 的 resloveActivity 方法
  • 2.下面的action和category用来表明这是一个入口Activity并且会出现在系统的应用列表中,二者缺一不可。


View的事件体系

view的基础知识

什么是view

View是Android中所有控件的基类,View的本身可以是单个空间,也可以是多个控件组成的一
组控件,即ViewGroup,ViewGroup继承自View,其内部可以有子View,这样就形成了View
树的结构。

View的位置参数

View的位置主要由它的四个顶点来决定,即它的四个属性:top、left、right、bottom,分别表示View左上角的坐标点(top,left)以及右下角的坐标点(right,bottom)。同时,我们可以得到View的大小:

1
2
width = right - left
height = bottom - top

而这四个参数可以由以下方式获取:

  • Left = getLeft();
  • Right = getRight();
  • Top = getTop();
  • Bottom = getBottom();
    Android3.0后,View增加了x、y、translationX和translationY这几个参数。其中x和y是View左上角的坐标,而translationX和translationY是View左上角相对于容器的偏移量。

他们之间的换算关系如下:
x = left + translationX;
y = top + translationY;
注意:View在平移的过程中,top和left不会改变,改变的是x、y、translationX和translaY。

MotionEvent和TouchSlop

MotionEvent

在手指接触到屏幕后会产生乙烯类的点击事件,如

  • 点击屏幕后离开松开,事件序列为DOWN->UP
  • 点击屏幕滑动一会再松开,事件序列为DOWN->MOVE->…->MOVE->UP 通过MotionEven对象我们可以得到事件发生的x和y坐标,我们可以通过getX/getY和getRawX/getRawY得到,它们的区别是:getX/getY返回的是相对于当前View左上角的x和y坐标,getRawX/getRawY返回的是相对于手机屏幕左上角的x和y坐标。
TouchSloup

TouchSloup是系统所能识别出的被认为是滑动的最小距离,这是一个常量,与设备有关,可通过以下方法获得:

1
ViewConfiguration.get(getContext()).getScaledTouchSloup().

VelocityTracker、GestureDetector和Scroller

VelocityTracker

速度追踪,用于追踪手指在滑动过程中的速度,包括水平放向速度和竖直方向速度。 使用方法:
1.在View的onTouchEvent方法中追踪当前单击事件的速度

1
2
VelocityRracker velocityTracker = VelocityTracker.obtain();
velocityTracker.addMovement(event);

2.计算速度,获得水平速度和竖直速度

1
2
3
velocityTracker.computeCurrentVelocity(1000);
int xVelocity = (int)velocityTracker.getXVelocity();
int yVelocity = (int)velocityTracker.getYVelocity();

注意,获取速度之前必须先计算速度,即调用computeCurrentVelocity方法,这里指的速度是指一段时间内手指滑过的像素数,1000指的是1000毫秒,得到的是1000毫秒内滑过的像素数。速度可正可负:

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

3.最后,当不需要使用的时候,需要调用clear()方法重置并回收内存:

1
2
velocityTracker.clear();
velocityTracker.recycle();

GestureDetector

手势检测,用于辅助检测用户的单击、滑动、长按、双击等行为。 使用方法:
1.创建一个GestureDetector对象并实现OnGestureListener接口,根据需要,也可实现
OnDoubleTapListener接口从而监听双击行为:

1
2
3
GestureDetector mGestureDetector = new GestureDetector(this);
//解决长按屏幕后无法拖动的现象
mGestureDetector.setIsLongpressEnabled(false);

2.在目标View的OnTouchEvent方法中添加以下实现:

1
2
boolean consume = mGestureDetector.onTouchEvent(event);
return consume;

3.实现OnGestureListener和OnDoubleTapListener接口中的方法,其中常用的方法有:
onSingleTapUp(单击)、onFling(快速滑动)、onScroll(拖动)、onLongPress(长按)和onDoubleTap(双击)。 建议:如果只是监听滑动相关的,可以自己在onTouchEvent中实现,如果要监听双击这种行为,那么就使用GestureDetector。

Scroller

弹性滑动对象,用于实现View的弹性滑动。其本身无法让View他行滑动,需要和View的computeScroll方法配合使用才能完成这个功能。 使用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
invalidata();
}
@Override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX,mScroller.getCurrY());
postInvalidate();
}
}

原理下节讲。

View的滑动

使用scrollTo/scrollBy

1.scrollBy实际调用了scrollTo,它实现了基于当前位置的相对滑动,而scrollTo则实现了绝对滑动。
2.scrollTo和scrollBy只能改变View的内容位置而不能改变View在布局中的位置。
3.滑动偏移量mScrollX和mScrollY的正负与实际滑动方向相反,即从左向右滑动,mScrollX为负值,从上往下滑动mScrollY为负值。

使用动画

使用动画移动View,主要是操作View的translationX和translationY属性,既可以采用传统的View动画,也可以采用属性动画,如果使用属性动画,为了能够兼容3.0以下的版本,需要采用开源动画库nineolddandroids。 如使用属性动画:(View在100ms内向右移动100像素)

1
ObjectAnimator.ofFloat(targetView,"translationX"0,100).setDuration(100).start();

改变布局属性

通过改变布局属性来移动View,即改变LayoutParams。

各种滑动方式的对比

1.scrollTo/scrollBy:操作简单,适合对View内容的滑动;
2.动画:操作简单,主要适用于没有交互的View和实现复杂的动画效果;
3.改变布局参数:操作稍微复杂,适用于有交互的View。

弹性滑动

使用Scroller

使用Scroller实现弹性滑动的典型使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Scroller scroller = new Scroller(mContext);
//缓慢移动到指定位置
private void smoothScrollTo(int destX,int dextY){
int scrollX = getScrollX();
int deltaX = destX - scrollX;
//1000ms内滑向destX,效果就是缓慢滑动
mScroller.startSscroll(scrollX,0,deltaX,0,1000);
invalidate();
}
@override
public void computeScroll(){
if(mScroller.computeScrollOffset()){
scrollTo(mScroller.getCurrX(),mScroller.getCurrY());
postInvalidate();
}
}

从上面代码可以知道,我们首先会构造一个Scroller对象,并调用他的startScroll方法,该方法并没有让view实现滑动,只是把参数保存下来,我们来看看startScroll方法的实现就知道了:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void startScroll(int startX,int startY,int dx,int dy,int duration){
mMode = SCROLL_MODE;
mFinished = false;
mDuration = duration;
mStartTime = AnimationUtils.currentAminationTimeMills();
mStartX = startX;
mStartY = startY;
mFinalX = startX + dx;
mFinalY = startY + dy;
mDeltaX = dx;
mDeltaY = dy;
mDurationReciprocal = 1.0f / (float)mDuration;
}

可以知道,startScroll方法的几个参数的含义,startX和startY表示滑动的起点,dx和dy表示的是滑动的距离,而duration表示的是滑动时间,注意,这里的滑动指的是View内容的滑动,在startScroll方法被调用后,马上调用invalidate方法,这是滑动的开始,invalidate方法会导致View的重绘,在View的draw方法中调用computeScroll方法,computeScroll又会去向Scroller获取当前的scrollX和scrollY;然后通过scrollTo方法实现滑动,接着又调用postInvalidate方法进行第二次重绘,一直循环,知道computeScrollOffset()方法返回值为false才结束整个滑动过程。 我们可以看看computeScrollOffset方法是如何获得当前的scrollX和scrollY的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public boolean computeScrollOffset(){
...
int timePassed = (int)(AnimationUtils.currentAnimationTimeMills() - 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(y * mDeltaY);
break;
...
}
}
return true;
}

到这里我们就基本明白了,computeScroll向Scroller获取当前的scrollX和scrollY其实是通过计算时间流逝的百分比来获得的,每一次重绘距滑动起始时间会有一个时间间距,通过这个时间间距Scroller就可以得到View当前的滑动位置,然后就可以通过scrollTo方法来完成View的滑动了。

通过动画

动画本身就是一种渐近的过程,因此通过动画来实现的滑动本身就具有弹性。实现也很简单:

1
ObjectAnimator.ofFloat(targetView,"translationX"0,100).setDuration(100).start();

当然,我们也可以利用动画来模仿Scroller实现View弹性滑动的过程:

1
2
3
4
5
6
7
8
9
10
11
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();
mButton1.scrollTo(startX + (int) (deltaX * fraction) , 0);
}
});
animator.start();

上面的动画本质上是没有作用于任何对象上的,他只是在1000ms内完成了整个动画过程,利用这个特性,我们就可以在动画的每一帧到来时获取动画完成的比例,根据比例计算出View所滑动的距离。

使用延时策略

延时策略的核心思想是通过发送一系列延时信息从而达到一种渐近式的效果,具体可以通过Hander和View的postDelayed方法,也可以使用线程的sleep方法。下面以Handler为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static final int MESSAGE_SCROLL_TO = 1;
private static final int FRAME_COUNT = 30;
private static final int DELATED_TIME = 33;
private int mCount = 0;
@suppressLint("HandlerLeak")
private Handler handler = 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);
mHandelr.sendEmptyMessageDelayed(MESSAGE_SCROLL_TO , DELAYED_TIME);
}
break;
default : break;
}
}
}

View的事件分发机制

点击事件的传递规则

首先我们先看看下面一段伪代码,通过它我们可以理解到点击事件的传递规则:

1
2
3
4
5
6
7
8
9
public boolean dispatchTouchEvent (MotionEvent ev){
boolean consume = false;
if (onInterceptTouchEvnet(ev){
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEnvet(ev);
}
return consume;
}

上面代码主要涉及到以下三个方法:

  • public boolean dispatchTouchEvent(MotionEvent ev); 这个方法用来进行事件的分发
  • public boolean onInterceptTouchEvent(MotionEvent ev); 这个方法用来判断是否拦截事件
  • public boolean onTouchEvent(MotionEvent ev); 这个方法用来处理点击事件

下面理一理点击事件的传递规则:对于一个根ViewGroup,点击事件产生后,首先会传递给他,这时候就会调用他的onDispatchTouchEvent方法,如果Viewgroup的onInterceptTouchEvent方法返回true表示他要拦截事件,接下来事件就会交给ViewGroup处理,调用ViewGroup的onTouchEvent方法;如果ViewGroup的onInteceptTouchEvent方法返回值为false,表示ViewGroup不拦截该事件,这时事件就传递给他的子View,接下来子View的dispatchTouchEvent方法,如此反复直到事件被最终处理。

当一个View需要处理事件时,如果它设置了OnTouchListener,那么onTouch方法会被调用,如果onTouch返回false,则当前View的onTouchEvent方法会被调用,返回true则不会被调用,同时,在onTouchEvent方法中如果设置了OnClickListener,那么他的onClick方法会被调用。由此可见处理事件时的优先级关系:
onTouchListener > onTouchEvent > onClickListener

关于事件传递的机制,这里给出一些结论:

  1. 一个事件系列以down事件开始,中间包含数量不定的move事件,最终以up事件结束。
  2. 正常情况下,一个事件序列只能由一个View拦截并消耗。
  3. 某个View拦截了事件后,该事件序列只能由它去处理,并且它的onInterceptTouchEvent不会再被调用。
  4. 某个View一旦开始处理事件,如果它不消耗ACTION_DOWN事件(onTouchEvnet返回false),那么同一事件序列中的其他事件都不会交给他处理,并且事件将重新交由他的父元素去处理,即父元素的onTouchEvent被调用。
  5. 如果View不消耗ACTION_DOWN以外的其他事件,那么这个事件将会消失,此时父元素的onTouchEvent并不会被调用,并且当前View可以持续收到后续的事件,最终消失的点击事件会传递给Activity去处理。
  6. ViewGroup默认不拦截任何事件。
  7. View没有onInterceptTouchEvent方法,一旦事件传递给它,它的onTouchEvent方法会被View的事件体系调用。
  8. View的onTouchEvent默认消耗事件,除非他是不可点击的(clickable和longClickable同时为false)。
  9. View的enable不影响onTouchEvent的默认返回值。
  10. onClick会发生的前提是当前View是可点击的,并且收到了down和up事件。
  11. 事件传递过程总是由外向内的,即事件总是先传递给父元素,然后由父元素分发给子View,通过requestDisallowInterceptTouchEvent方法可以在子元素中干预父元素的分发过程,但是ACTION_DOWN事件除外。

View的滑动冲突

在界面中,只要内外两层同时可以滑动,这个时候就会产生滑动冲突。

常见的滑动冲突场景

  1. 外部滑动和内部滑动方向不一致;
  2. 外部滑动方向和内部滑动方向一致;
  3. 上面两种情况的嵌套。

滑动冲突的处理规则

  1. 对于场景一,处理的规则是:当用户左右(上下)滑动时,需要让外部的View拦截点击事件,当用户上下(左右)滑动的时候,需要让内部的View拦截点击事件。根据滑动的方向判断谁来拦截事件。
  2. 对于场景二,由于滑动方向一致,这时候只能在业务上找到突破点,根据业务需求,规定什么时候让外部View拦截事件,什么时候由内部View拦截事件。
  3. 场景三的情况相对比较复杂,同样根据需求在业务上找到突破点。

滑动冲突的解决方式

  1. 外部拦截法:所谓的外部拦截法是指点击事件都先经过父容器的拦截处理,如果父容器需要此事件就拦截,否则就不拦截。下面是伪代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    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 = flase;
    }
    break;
    case MotionEvent.ACTION_UP:
    intercepted = false;
    break;
    default : break;
    }
    mLastXIntercept = x;
    mLastYIntercept = y;
    return intercepted;

内部拦截法:内部拦截法是指父容器不拦截任何事件,所有的事件都传递给子元素,如果子元素需要此事件就直接消耗,否则就交由父容器进行处理。这种方法与Android事件分发机制不一致,需要配合requestDisallowInterceptTouchEvent方法才能正常工作。下面是伪代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public boolean dispatchTouchEvent ( MotionEvent event ) {
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction) {
case MotionEvent.ACTION_DOWN:
parent.requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - mLastX;
int deltaY = y - mLastY;
if (父容器需要此类点击事件) {
parent.requestDisallowInterceptTouchEvent(false);
}
break;
case MotionEvent.ACTION_UP:
break;
default : break;
}
mLastX = x;
mLastY = y;
return super.dispatchTouchEvent(event);
}

除了子元素需要做处理外,父元素也要默认拦截除了ACTION_DOWN以外的其他事件,这样当子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截所需的事件。因此,父元素要做以下修改:

1
2
3
4
5
6
7
8
public boolean onInterceptTouchEvent (MotionEvent event) {
int action = event.getAction();
if(action == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true;
}
}

至于具体的实现可以根据实际需要去修改拦截成立的条件,开发艺术艺术中也给出了实
例,具体可参考书中P161-P173。


理解RemoteViews

RemoteViews提供了一组基础的操作,用于跨进程更新它的界面。RemoteViews在Andriod中的使用场景有两种:通知栏和桌面小部件(都运行在SystemServer进程)。

RemoteViews的应用

通知栏主要通过NotificationManager的notify方法来实现,除了默认效果外还可以自定义布局。
桌面小工具主要通过AppWidgetProvider来实现,AppWidgetProvider本质上是一个广播。
两者都会用到RemoteViews,两者都运行在其他进程中,准确的说是系统的SystemServer进程。

RemoteViews在通知栏上的应用

关于PendingIntent,它表示的是一种特定的Intent,这个Intent中所包含的意图必须由用户来触发。

RemoteViews在桌面小部件上的应用

AppWidgetProvider实现桌面小工具的类,本质是一个广播即BroadcastReceiver。
具体使用看系统自动生成的桌面小工具。
桌面小部件上不管是初始化界面还是后续的更新界面都必须使用RemoteViews来完成。

PendingIntent概述

PendingIntent和Intent的区别在于,PendingIntent是在将来的某个环节的不确定的时刻发生,而Intent是立刻发生。

RemoteViews的内部机制

  • 大部分的set方法的确是通过发射来完成的。
  • NotificationManager和AppWidgetProvider通过Binder分别和SystemServer进程中的NotificationManagerService以及AppWidgetServer进行通信。
  • RemoteViews会通过Binder传递到SystemServer进程中,这是因为RemoteViews实现了Parcelable接口,因此它可以跨进程传输,系统会根据RemoteViews中的包名等信息去得到该应用的资源。

RemoteViews的意义

模拟通知栏效果实现跨进程跨进程的UI更新。
我们可以像系统一样使用Binder来实现,但是为了简单起见就采用广播。
实际:

  • 现在用两个应用,一个应用需要能够更新另一个应用的某个界面,这个时候我们当然可以选择AIDL去实现(跨应用更新UI),但是如果对界面的更新比较频繁,这个时候就会有效率的问题,如果采用RemoteViews来实现就没有这个问题了。(RemoteViews只支持一些常用的View,对于自定义的View是不支持的。)

Android动画深入分析

Android动画分为三种:

  • 1.View动画(平移、缩放、旋转、透明度)
  • 2.帧动画(图片切换动画)
  • 3.属性动画(动态的改变对象的属性从而达到动画的效果)

View动画

View动画的作用对象是View,支持四种动画效果:

  • 1.平移
  • 2.缩放
  • 3.旋转
  • 4.透明

View动画的种类

上述四种变换效果对应着Animation四个子类: TranslateAnimation 、 ScaleAnimation 、 RotateAnimation 和 AlphaAnimation 。这四种动画皆可以通过XML定义,也可以通过代码来动态创建。

xml定义动画:

  • 1. 标签表示动画集合,对应AnimationSet类,可以包含一个或若干个动画,内部还可以嵌套其他动画集合。两个属性:

    • i. android:interpolator 表示动画集合所采用的插值器,插值器影响动画速度,比如非匀速动画就需要通过插值器来控制动画的播放过程。
    • ii. android:shareInterpolator 表示集合中的动画是否和集合共享同一个插值器,如果集合不指定插值器,那么子动画就需要单独指定所需的插值器或默认值。
  • 2.<translate><scale><rotate><alpha>这几个子标签分别代表四种变换效果。

  • 3.定义完View动画的xml后,通过以下代码应用动画:

    1
    2
    Aniamation anim = AnimationUtils.loadAnimation(context,R.anim.animation_test);
    view.startAnimation(anim);

代码动态创建动画:

1
2
3
AlphaAnimation alphaAnimation = new AlphaAnimation(0,1);
alphaAnimation.setDuration(1500);
view.startAnimation(alphaAnimation);

自定义View动画

需要继承 Animation 这个抽象类,重写它的 initialize 和 applyTransformation 方法。在 initialize 方法中做一些初始化工作,在 applyTransformation 中进行相应的矩阵变换即可,很多时候需要采用 Camera 来简化矩阵变换的过程。自定义View动画的过程主要是矩阵变换的过程。

帧动画

帧动画是顺序播放一组预先定义好的图片,使用简单,但容易引起OOM,所以在使用帧动画时应尽量避免使用过多尺寸较大的图片。

View动画的特殊使用场景

LayoutAnimation

作用于ViewGroup,为ViewGroup指定一个动画,当它的子元素出场时都会具有这种动画效果,一般用在ListView上。

Activity的切换效果

我们可以自定义Activity的切换效果,主要通过在 startActivity 或者 finish 的后面增加overridePendingTransition(int enterAnim , int exitAnim)方法

属性动画

API 11后加入,可以在一个时间间隔内完成对象从一个属性值到另一个属性值的改变。因此与
View动画相比,属性动画几乎无所不能,只要对象有这个属性,它都能实现动画效果。API11
以下可以通过 nineoldandroids 库来兼容以前版本。

属性动画有以下三种使用方法:

  • ObjectAnimator:
    • ObjectAnimator.ofFloat(view,"translationY",values).start();
  • ValueAnimator

    • 1
      2
      3
      4
      5
      6
      ValueAnimator colorAnim = ObjectAnimator.ofInt(view,"backgroundColor",/*red*/0xffff8080,/*blue*/0xff8080ff);
      colorAnim.setDuration(2000);
      colorAnim.setEvaluator(new ArgbEvaluator());
      colorAnim.setRepeatCount(ValueAnimator.INFINITE);
      colorAnim.setRepeatMode(ValueAnimator.REVERSE);
      colorAnim.start();
  • AnimatorSet

    • 1
      2
      3
      AnimatorSet set = new AnimatorSet();
      set.playTogether(animator1,animator2,animator3);
      set.setDuration(3*1000).start();
    • 也可以通过在xml中定义在 res/animator/ 目录下。具体如下:

      • 1
        2
        3
        4
        5
        6
        7
        <?xml version="1.0" encoding="utf-8"?>
        <set xmlns:android="http://schemas.android.com/apk/res/android">
        <objectAnimator
        ....../>
        <animator
        ....../>
        </set>
      • 1
        2
        3
        4
        5
        AnimatorSet set = (AnimatorSet)AnimatorInflater.loadAnimator(context , R.animator.anim);
        set.setTarget(view);
        set.start();
        <set> 标签对应 AnimatorSet,<animator>对应ValueAnimator,
        而<objectAnimator>则对应 ObjectAnimator。

理解差值器和估值器

  • 时间插值器( TimeInterpolator) 的作用是根据时间流逝的百分比来计算出当前属性值改变的百分比,系统预置的有LinearInterpolator(线性插值器:匀速动画),AccelerateDecelerateInterpolator(加速减速插值器:动画两头慢中间快),DecelerateInterpolator(减速插值器:动画越来越慢)。

  • 估值器( TypeEvaluator) 的作用是根据当前属性改变的百分比来计算改变后的属性值。
    系统预置有IntEvaluator 、FloatEvaluator 、ArgbEvaluator。

  • 具体来说 对于一个作用在view上改变其宽度属性、持续40ms的属性动画来说,就是当时间t=20ms时,时间流逝了50%,那么view的宽度属性应该改变了多少呢?这个就由Interpolator和Evaluator的算法来决定。

属性动画的监听器

1
2
3
4
5
6
public static interface AnimatorListener {
void onAnimationStart(Animator animation); //动画开始
void onAnimationEnd(Animator animation); //动画结束
void onAnimationCancel(Animator animation); //动画取消
void onAnimationRepeat(Animator animation); //动画重复播放
}

为了方便开发,系统提供了AnimatorListenerAdapter类,它是AnimatorListener的适配器类,可以有选择的实现以上4个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* Implementors of this interface can add themselves as update listeners
* to an <code>ValueAnimator</code> instance to receive callbacks on every animation
* frame, after the current frame's values have been calculated for that
* <code>ValueAnimator</code>.
*/
public static interface AnimatorUpdateListener {
/**
* <p>Notifies the occurrence of another frame of the animation.</p>
* *
@param animation The animation which was repeated.
*/
void onAnimationUpdate(ValueAnimator animation);
}

AnimatorUpdateListener会监听整个动画的过程,动画由许多帧组成的,每播放一帧,onAnimationUpdate就会调用一次。

对任意属性做动画

  • 1.属性动画要求作用的对象提供该属性的get和set方法,属性动画根据外界传递的该属性的初始值和最终值,通过多次调用set方法来实现动画效果。

  • 2.如果被作用的对象没有set/get方法,可以:

    • i.请给你的对象加上get和set方法,如果你有权限的话( 对于SDK或者其他第三方类库
      的类无法加上的)
    • ii.用一个类来包装原始对象,间接为其提供get和set方法
    • 1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      //包装View类 用于给属性动画调用 从而包装了set get
      public class ViewWrapper {
      private View target;
      public ViewWrapper(View target) {
      this.target = target;
      }
      public int getWidth() {
      return target.getLayoutParams().width;
      }
      public void setWidth(int width) {
      target.getLayoutParams().width = width;
      target.requestLayout();
      }
      }
      //使用:
      ViewWrapper wrapper = new ViewWrapper(mButton);
      ObjectAnimator.ofInt(mButton,"width",500).setDuration(3000).start();
    • iii.采用ValueAnimator,监听动画过程,自己实现属性的改变;

      • 1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        private void performAnimate(final View target, final int start, final int end) {
        ValueAnimator valueAnimator = ValueAnimator.ofInt(1, 100);
        valueAnimator.addUpdateListener(new AnimatorUpdateListener() {
        // 持有一个IntEvaluator对象,方便下面估值的时候使用
        private IntEvaluator mEvaluator = new IntEvaluator();
        @Override
        public void onAnimationUpdate(ValueAnimator animator) {
        // 获得当前动画的进度值,整型,1-100之间
        int currentValue = (Integer) animator.getAnimatedValue();
        Log.d(TAG, "current value: " + currentValue);
        // 获得当前进度占整个动画过程的比例,浮点型,0-1之间
        float fraction = animator.getAnimatedFraction();
        // 直接调用整型估值器通过比例计算出宽度,然后再设给Button
        target.getLayoutParams().width = mEvaluator.evaluate(fraction, start, end);
        target.requestLayout();
        }
        });
        valueAnimator.setDuration(5000).start();
        }

属性动画的工作原理

属性动画需要运行在有Looper的线程中,系统通过反射调用被作用对象get/set方法。

使用动画的注意事项

  • 1.使用帧动画时,当图片数量较多且图片分辨率较大的时候容易出现OOM,需注意,尽量
    避免使用帧动画。
  • 2.使用无限循环的属性动画时,在Activity退出时即使停止,否则将导致Activity无法释放从而造成内存泄露。
  • 3.动画在3.0以下的系统存在兼容性问题,特殊场景可能无法正常工作,需做好适配工作。
  • 4.View动画是对View的影像做动画,并不是真正的改变了View的状态,因此有时候会出现动画完成后View无法隐藏( setVisibility(View.GONE) 失效),这时候调用 view.clearAnimation() 清理View动画即可解决。
  • 5.不要使用px,使用px会导致不同设备上有不同的效果。
  • 6.View动画是对View的影像做动画,View的真实位置没有变动,动画完成后的新位置是无法触发点击事件的。属性动画是真实改变了View的属性,所以动画完成后的位置可以接受触摸事件。
  • 7.使用动画的过程中,使用硬件加速可以提高动画的流畅度。

Android的线程和线程池

  • 1.在Android系统,线程主要分为主线程和子线程,主线程处理和界面相关的事情,而子线程一般用于执行耗时操作。
  • 2.在Android中,线程的形态有很多种:
    • i.AsyncTask封装了线程池和Handler。
    • ii.HandlerThread是具有消息循环的线程,内部可以使用handler
    • iii.IntentService是一种Service,内部采用HandlerThread来执行任务,当任务执行完毕后IntentService会自动退出。由于它是一种Service,所以不容易被系统杀死
  • 3.操作系统中,线程是操作系统调度的最小单元,同时线程又是一种受限的系统资源,其创建和销毁都会有相应的开销。同时当系统存在大量线程时,系统会通过时间片轮转的方式调度每个线程,因此线程不可能做到绝对的并发,除非线程数量小于等于CPU的核心数。
  • 4.频繁创建销毁线程不明智,使用线程池是正确的做法。线程池会缓存一定数量的线程,通过线程池就可以避免因为频繁创建和销毁线程所带来的系统开销。

主线程和子线程

  • 1.主线程主要处理界面交互逻辑,由于用户随时会和界面交互,所以主线程在任何时候都需要有较高响应速度,则不能执行耗时的任务;
  • 2.android3.0开始,网络访问将会失败并抛出NetworkOnMainThreadException这个异常,这样做是为了避免主线程由于被耗时操作所阻塞从而现ANR现象。

Android中的线程形态

AsyncTask

1.三个参数(都可为Void):

  • i. Params:参数
  • ii. Progress:执行进度
  • iii. Result:返回值

2.四个方法 :

  • i. onPreExecute() 主线程执行,异步方法执行前调用。
  • ii. doInBackground(Params…params) 线程池中执行,用于执行异步任务;在方法内部用publishProgress 来更新任务进度。
  • iii. onProgressUpdate(Progress…value) 主线程执行,后台任务进度状态改变时被调用。
  • iv. onPostExecute(Result result) 主线程执行,异步任务执行之后被调用执行顺序: onPreExecute->doInBackground->onPostExecute 如果取消了异步任务,会回调onCancelled(),onPostExecute则不会被调用。

AsyncTask的类必须在主线程加载,Android4.1及以上已经被系统自动完成了;AsyncTask对象必须在主线程创建;execute方法需要在UI线程调用;一个AsyncTask对象只能调用一次;Android1.6之前串行执行,Android1.6采用线程池并行处理任务,Android3.0开始,又采用一个线程来串行执行任务,但也可以通过 executeOnExecutor() 方法来并行执行任务。

AsyncTask的工作原理

  • 1.AsyncTask中有两个线程池( SerialExecutor 和 THREAD_POOL_EXECUTOR )和一个 InternalHandler ,其中线程池SerialExecutor用于任务排队,THREAD_POOL_EXECUTOR用于真正执行任务,InternalHandler用于将执行环境切换到主线程。
  • 2.AsyncTask的排队过程:系统首先会把AsyncTask的Params参数封装成FutureTask对象,它充当Runnable的作用,接下来这个FutureTask会交给SerialExecutor的 execute() 方法处理,execute()方法首先会把FutereTask对象插入到任务队列 mTasks 中去;如果没有正在活动的AsyncTask任务,就会执行下一个AsyncTask任务;同时当一个AsyncTask任务执行完成后,AsyncTask会继续执行其他任务直到所有任务都执行为止,可以看出默认情况,AsyncTask是串行执行的(Android3.0后)。

HandlerThread

  • 1.HandlerThread继承了Thread,是一种可以使用Handler的Thread
  • 2.在run方法中通过 looper.prepare() 来开启消息循环,这样就可以在HandlerThread中创建Handler了
  • 3.外界可以通过一个Handler的消息方式来通知HandlerThread来执行具体任务;确定不使用之后,可以通过 quit 或 quitSafely 方法来终止线程执行
  • 4.具体使用场景是IntentService

IntentService

IntentSercie是一种特殊的Service,继承了Service并且是抽象类,任务执行完成后会自动停止,优先级远高于普通线程,适合执行一些高优先级的后台任务; IntentService封装了 HandlerThread 和 Handler。

  • 1.onCreate 方法自动创建一个HandlerThread
  • 2.然后用它的Looper构造了一个Handler对象 mServiceHandler ,这样通过mServiceHandlerAndroid的线程和线程池发送的消息都会在HandlerThread执行;
  • 3.IntentServiced的 onHandlerIntent 方法是一个抽象方法,需要在子类实现,onHandlerIntent方法执行后,stopSelt(int startId)就会停止服务,如果存在多个后台任务,执行完最后一个stopSelf(int startId)才会停止服务。

Android线程池

优点:

  1. 重用线程池的线程,减少线程创建和销毁带来的性能开销
  2. 控制线程池的最大并发数,避免大量线程互相抢系统资源导致阻塞
  3. 提供定时执行和间隔循环执行功能

ThreadPoolExecutor(熟悉后可自定义线程池)

Executor是一个接口,线程池的具体实现在ThreadPoolExecutor;它提供了一系列的参数来配置线程池;Android的线程池 大部分都是通 过Executor提供的工厂方法创建的。

ThreadPoolExecutor常见构造参数
  1. corePoolSize: 线程池的核心线程数,默认情况下,核心线程会一直存活(设置了超时机制除外, allowCoreThreadTimeOut属性为true时开启)
  2. maxinmumPoolSize: 线程池能容纳的最大线程数,当活动的线程达到这个数值之后,后续新任务会被阻塞
  3. keepAliveTime: 非核心线程闲置的超时时长,超过这个时长,非核心线程就会被回收,当allowCoreThreadTimeOut为true时,keepAliveTime同样作用于核心线程。
  4. unit:keepAliveTime的时间单位,这是一个枚举,常用TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分钟)
  5. workQueue: 线程池中的任务队列,通过execute方法提交的Runnable对象会存储在这个参数中
  6. threadFactory: 线程工厂,为线程池提供创建线程的功能,是个接口,提供ThreadnewThread(Runnable r)方法
  7. RejectedExecutionHandle:当线程池无法执行新任务时,可能由于线程队列已满或无法成功执行任务,这时候 ThreadPoolExecutor会调用handler的 rejectedExecution的方法,默认会抛出RejectedExecutionException
ThreadPoolExecutor执行任务大致遵循如下规则:
  1. 如果线程池中的线程数量未达到核心线程的数量,那么会直接启动一个核心线程来执行任务Android的线程和线程池
  2. 如果线程池中的线程数量已经达到或超过核心线程数量,那么任务会被插入到任务队列中排队等待执行
  3. 如果步骤2中无法将任务插入到任务队列中,往往是因为任务队列已满,这个时候如果线程数量未达到线程池规定的最大值,那么会立刻启动一个非核心线程来执行任务
  4. 如果步骤3中线程数量达到线程池规定的最大值,线程池会拒绝执行任务,并会调用RejectedExecutionHandler的rejectedExecution方法来通知调用者
AsyncTask的THREAD_POOL_EXECUTOR线程池配置:
  1. 核心线程数等于CPU核心数+1
  2. 线程池最大线程数为CPU核心数的2倍+1
  3. 核心线程无超时机制,非核心线程的闲置超时时间为1秒
  4. 任务队列容量是128

常见的4个线程池

  • 1、FixedThreadPool :线程数量固定的线程池,当所有线程都处于活动状态时,新任务会处于等待状态,只有核心线程并且不会回收(无超时机制),能快速的响应外界请求。
  • 2、CachedThreadPool :线程数量不定的线程池,最大线程数Integer.MAX_VALUE(相当于任意大),当所有线程都处于活动状态时,会创建新线程来处理任务;线程池的空闲进程超时时长为60秒,超过就会被回收;任何任务都会被立即执行,适合执行大量的耗时较少的任务。
  • 3、ScheduledThreadPool :核心线程数量固定,非核心线程数量无限制,非核心线程闲置时会被立刻回收,用于执行定时任务和具有固定周期的重复任务。
  • 4、SingleThreadExecutor :只有一个核心线程,所有任务都在这个线程中串行执行,不需要处理线程同步问题。

相关资料