1.资源对象没关闭造成的内存泄漏

描述:

资源性对象比如(Cursor,File文件等)往往都用了一些缓冲,我们在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。它们的缓冲不仅存在于 java虚拟机内,还存在于java虚拟机外。如果我们仅仅是把它的引用设置为null,而不关闭它们,往往会造成内存泄漏。因为有些资源性对象,比如 SQLiteCursor(在析构函数finalize(),如果我们没有关闭它,它自己会调close()关闭),如果我们没有关闭它,系统在回收它时也会关闭它,但是这样的效率太低了。因此对于资源性对象在不使用的时候,应该调用它的close()函数,将其关闭掉,然后才置为null.在我们的程序退出时一定要确保我们的资源性对象已经关闭。

程序中经常会进行查询数据库的操作,但是经常会有使用完毕Cursor后没有关闭的情况。如果我们的查询结果集比较小,对内存的消耗不容易被发现,只有在常时间大量操作的情况下才会复现内存问题,这样就会给以后的测试和问题排查带来困难和风险。

示例代码:

Cursor cursor = getContentResolver().query(uri...); 

if (cursor.moveToNext()) { 

... ... 

}

修正示例代码:

Cursor cursor = null; 

try { 

cursor = getContentResolver().query(uri...); 

if (cursor != null &&cursor.moveToNext()) { 

... ... 

} 

} finally { 

if (cursor != null) { 

try { 

cursor.close(); 

} catch (Exception e) { 

//ignore this 

} 

} 

}

2.构造Adapter时,没有使用缓存的convertView

描述:

以构造ListView的BaseAdapter为例,在BaseAdapter中提供了方法:

public View getView(int position, ViewconvertView, ViewGroup parent)

来向ListView提供每一个item所需要的view对象。初始时ListView会从BaseAdapter中根据当前的屏幕布局实例化一定数量的 view对象,同时ListView会将这些view对象缓存起来。当向上滚动ListView时,原先位于最上面的list item的view对象会被回收,然后被用来构造新出现的最下面的list item。这个构造过程就是由getView()方法完成的,getView()的第二个形参View convertView就是被缓存起来的list item的view对象(初始化时缓存中没有view对象则convertView是null)。由此可以看出,如果我们不去使用 convertView,而是每次都在getView()中重新实例化一个View对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大。 ListView回收list item的view对象的过程可以查看:

android.widget.AbsListView.java –> voidaddScrapView(View scrap) 方法。

示例代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { 

View view = new Xxx(...); 

... ... 

return view; 

}

修正示例代码:

public View getView(int position, ViewconvertView, ViewGroup parent) { 

View view = null; 

if (convertView != null) { 

view = convertView; 

populate(view, getItem(position)); 

... 

} else { 

view = new Xxx(...); 

... 

} 

return view; 

}

3.Bitmap对象不在使用时调用recycle()释放内存

描述:

有时我们会手工的操作Bitmap对象,如果一个Bitmap对象比较占内存,当它不在被使用的时候,可以调用Bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的,视情况而定。可以看一下代码中的注释:

/** 

•Free up the memory associated with thisbitmap's pixels, and mark the 

•bitmap as "dead", meaning itwill throw an exception if getPixels() or 

•setPixels() is called, and will drawnothing. This operation cannot be 

•reversed, so it should only be called ifyou are sure there are no 

•further uses for the bitmap. This is anadvanced call, and normally need 

•not be called, since the normal GCprocess will free up this memory when 

•there are no more references to thisbitmap. 

*/

4.试着使用关于application的context来替代和activity相关的context

这是一个很隐晦的内存泄漏的情况。有一种简单的方法来避免context相关的内存泄漏。最显著地一个是避免context逃出他自己的范围之外。使用Application context。这个context的生存周期和你的应用的生存周期一样长,而不是取决于activity的生存周期。如果你想保持一个长期生存的对象,并且这个对象需要一个context,记得使用application对象。你可以通过调用 Context.getApplicationContext() or Activity.getApplication()来获得。更多的请看这篇文章如何避免

Android内存泄漏。

5.注册没取消造成的内存泄漏

一些Android程序可能引用我们的Anroid程序的对象(比如注册机制)。即使我们的Android程序已经结束了,但是别的引用程序仍然还有对我们的Android程序的某个对象的引用,泄漏的内存依然不能被垃圾回收。调用registerReceiver后未调用unregisterReceiver。

比如:假设我们希望在锁屏界面(LockScreen)中,监听系统中的电话服务以获取一些信息(如信号强度等),则可以在LockScreen中定义一个 PhoneStateListener的对象,同时将它注册到TelephonyManager服务中。对于LockScreen对象,当需要显示锁屏界面的时候就会创建一个LockScreen对象,而当锁屏界面消失的时候LockScreen对象就会被释放掉。

但是如果在释放 LockScreen对象的时候忘记取消我们之前注册的PhoneStateListener对象,则会导致LockScreen无法被垃圾回收。如果不断的使锁屏界面显示和消失,则最终会由于大量的LockScreen对象没有办法被回收而引起OutOfMemory,使得system_process 进程挂掉。

虽然有些系统程序,它本身好像是可以自动取消注册的(当然不及时),但是我们还是应该在我们的程序中明确的取消注册,程序结束时应该把所有的注册都取消掉。

6.集合中对象没清理造成的内存泄漏

我们通常把一些对象的引用加入到了集合中,当我们不需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

Android用户界面设计:使用片段



Android 3.0引入的新的片断(Fragment)API,让我们更容易地创建动态用户界面。在这个教程中,我们学习如何将一个两屏的ListView转换成WebView流,以适应大屏幕的单屏流设计,比如在平板设备中。

    这篇文章的节奏将比我们的入门教程更快一些。如果你对基本的Android控件或概念不熟悉你可能需要复习这个网站上我们其它的一些教程,甚至是Android API参考。最终的开源代码可以在Google code上下载到。

片段简介

    在我们开始之间,让我们在更高的层次上定义一下什么是片段。通常来说,片段是一大块用户界面,它具有自己的生存周期。如果它听起来像一个Activity,那是因为它确实很像一个Activity。然而,片段与Activity不同,片段必须存在于Activity之内。片段不须要在它每次初始化的时候与同一个Activity配对,这使它具有一些灵活性。与Activity一样,片段也无需包含任何用户界面。

步骤0:开始

    这个教程假设你读过我们的列表视图教程,你可以下载那个教程的代码,并完成一些任务,然后开始,也可以直接下载这个教程的代码直接开始。

步骤1:重新设计界面

    下图示意了我上在列表视图教程中所提到的文章阅读应用,我们还没有考虑并使用片段:

     

     这个流程在相对小屏幕上运行得很不错。然而,在大屏幕上,比如Motorola Xoom平板的10寸屏幕上,在列表视图上却浪费了很多空间。WebView看起来正常,但是有点枯燥。

    这就是要引入片段的地方:在大屏幕上,我们可以提供更有效的用户界面,如果我们可以在同一屏上显示ListView和WebView。当用户点击左边“面板”的列表视图中的某一项时,右边的WebView更新显示相应的内容。这种工作流程经常用于email或文档或RSS阅读器。下图就是重新设计之后的界面示意图:

     

步骤2:转换为基于片段的设计

    现在我们知道了新的流程应该如何设计,我们也知道当前的两个活动必须转换成片段。我们将分几步来完成这个转换。第一步保持界面样子不变,只是使用片段修改每个界面内容。一个片段将包含当前的ListView,另一个包含WebView。然后我们再转到单个屏幕的实现,修改ListView和WebView之间的消息传递。

    首先,将你的程序的项目构建目标改变Android 3.0。在Eclipse中,右键点击项目并选择“属性”。点击Android部分并选中Android 3.0。我们不使用任何Google API,所以Android开源项目版本足够了。然后点击“确定”按钮。

    现在你就可以访问新的API了,包括片段API。

    注意:在将来的教程中,我们将讨论如何使用新的兼容层来使得像片段API这样的技术在更早版本的Android设备上也能工作。但是现在它只能运行在Android 3.0设备上。

步骤3:创建片段类

    创建两个Java类来代表两个片段:ListView界面和WebView界面。将它们命名为TutListFragment和TutViewerFragment。TutListFragment将继承ListFragment类,TutViewerFragment只是继承Fragment类。

    在TutListFragment类中,我们需要重写两个方法: onListItemClick()和onCreate()。这些方法的内容看起来应该很熟悉,它与之前我们讲过的TutListActivity类的代码一致。这个代码很快就要修改,但是现在暂时不需要,下面是当前TutListFragment类的代码:


@Override
public void onListItemClick(ListView l, View v, int position, long id) {
 String[] links = getResources().getStringArray(R.array.tut_links);
 
 String content = links[position];
 Intent showContent = new Intent(getActivity().getApplicationContext(),
 TutViewerActivity.class);
 showContent.setData(Uri.parse(content));
 startActivity(showContent);
}
 
@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setListAdapter(ArrayAdapter.createFromResource(getActivity()
 .getApplicationContext(), R.array.tut_titles,
 R.layout.list_item));
}


    TutViewerFragment类更简单一些。我们基于当前片段运行在同一个活动下并且直接从Fragment类内问部获取目标数据的事实。添加一个重写onCreateView()方法。这个方法的代码应该看起来像这样:


@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
 Bundle savedInstanceState) {
 Intent launchingIntent = getActivity().getIntent();
 String content = launchingIntent.getData().toString();
 
 WebView viewer = (WebView) inflater.inflate(R.layout.tut_view, container, false);
 viewer.loadUrl(content);
 
 return viewer;
}


    直接访问活动实例的能力非常有用,但是在后面会引起一个问题。如果这个片段存在于带有列表片段的界面上会怎么样呢?在那样的情况下,就会没有启动目标来获取URL。类似的在TutListFragment中,只要当用户点击一个列表项时我们都直接启动一个新的Activity。如果TutViewFragment在同一个活动中存在什么怎么样呢?如果这样的话,启动一个新的活动就没有意义了。我们将在这个教程的后面回过头来解决这个问题。

步骤4:添加片段布局资源

    现在创建一个新的名为“tutlist_fragment.xml”的布局文件来表示包含文章列表的片段。片段布局资源使用你创建的Fragment类的标签和引用。



    接下来,创建一个类似的布局文件,叫做tutview_fragment.xml:



步骤5:更新Activity类

    TutListActivity和TutViewerActivity类必须修改。TutListActivity类只有一个方法,onCreate(),现在需要修改它来加载你在前一步创建的合适的片段布局资源,如下:


@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 
 setContentView(R.layout.tutlist_fragment);
}


    TutListActivity应该继承Activity类,而不是ListActivity类。

    TutViewerActivity类也需要类似的修改,它的onCreate()方法现在看起来像这样:


@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.tutview_fragment);
}


步骤6:检查你的进度

    尝试现在运行程序。你会发现它和以前一样。没什么值得兴奋的,不是么?然而,整个用户界面现在使用片段来运行了。这使你需要做的下一步修改更加平滑,我们添加一个新的布局来组合两个片段以在一个界面上显示。然而可能你也注意到了,片段之间的通信的处理和我们文章之间的通信一样。事实上,我们每个片段对应的活动保持不变。当一个活动包含并管理两个片段时,这将不符合需求。首先让我们来修复它。

步骤7:改变TutListFragment通信

    像你在步骤3中学到的一样,从TutListFragment对象直接启动一个活动不再有效了。WebView UI可能与列表是同一个活动的一部分――总之那就是我们对于大屏幕的计划。在那种情况下,我们只想在第二个片段中更新WebView的URL。

    做这些修改,我们需要做几件事情。首先,我们让片段不依赖于它们所在的活动。要做到这一点,在TutListFragment类中添加一个侦听器,如下:


public interface OnTutSelectedListener {
 public void onTutSelected(Uri tutUri);
}


    然后通过更新onListItemClickListener()方法来触发它,如下:


@Override
public void onListItemClick(ListView l, View v, int position, long id) {
 String[] links = getResources().getStringArray(R.array.tut_links);
 
 String content = links[position];
 tutSelectedListener.onTutSelected(Uri.parse(content));
}


    接下来让TutListActivity类实现OnTutSelectedListener接口,如下:


public class TutListActivity extends Activity implements
 TutListFragment.OnTutSelectedListener {
...
@Override
public void onTutSelected(Uri tutUri) {
 Intent showContent = new Intent(getApplicationContext(),
 TutViewerActivity.class);
 showContent.setData(tutUri);
 startActivity(showContent);
}


    现在我们分离了片段的功能,这些功能用于处理用户界面,作为控制器的活动,向下一个活动传递数据。我们后面要修改onTutSelected()方法来决定是否启动一个新的活动实例或者更新现有的片段实例。

步骤8:改变TutViewerFragment通信

    现在让我们把注意力转到TutViewerFragment类上,它的代码也需要修改。片段不再查询启动目标来找出加载哪个URL,而是等待被通知要加载哪个URL。在样,我们可以直接修改WebView而不需要每次加载都重新创建片段。

    首先,修改TutViewerFragment类,让它包含一个叫做updateUrl()的方法:


public void updateUrl(String newUrl) {
 if (viewer != null) {
 viewer.loadUrl(newUrl);
 }
}


    其次,删除所有onCreateView()方法下的功能,除了inflate()的调用。在TutViewerActivity类中,添加这些功能检索Intent然后调用updateUrl()方法,如下:


@Override
public void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.tutview_fragment);
 
 Intent launchingIntent = getIntent();
 String content = launchingIntent.getData().toString();
 
 TutViewerFragment viewer = (TutViewerFragment) getFragmentManager()
 .findFragmentById(R.id.tutview_fragment);
 
 viewer.updateUrl(content);
}


    此时此刻,程序的行为还是没有变化。然而通过进一步的代码,片段现在可以共存在同一个活动中或者分开。

步骤9:添加双片段布局

    现在让我们来创建带有两个片段的布局,以供特定情况使用。在layout-land目录(你可能需要自己创建),粘贴一份tutlist_fragment.xml。它将对横屏和竖屏提供不同的布局。竖屏模式将保持不变。编辑这个文件如下:



    这将界面分隔成水平地两个片段(左右结构)。

步骤10:添加动态选项

    现在我们可以为程序添加一些简单的逻辑,可以在启动一个新的活动(双屏模式)和更新存在的片段(单屏模式)之间切换。

    为了达到这个目的,更新TutListActivity类的onTutSelected()方法如下:


@Override
public void onTutSelected(String tutUrl) {
 TutViewerFragment viewer = (TutViewerFragment) getFragmentManager()
 .findFragmentById(R.id.tutview_fragment);
 
 if (viewer == null || !viewer.isInLayout()) {
 Intent showContent = new Intent(getApplicationContext(),
 TutViewerActivity.class);
 showContent.setData(Uri.parse(tutUrl));
 startActivity(showContent);
 } else {
 viewer.updateUrl(tutUrl);
 }
}


    我们所做的就是获取片段并检查它是否是现存的布局的一部分。如果不是,查看器活动启动,否则更新已存在的片段。

步骤11:运行最新的使用片段的程序

    到此,程序将有两种模式:竖屏保持不变,横屏显示列表位于WebView的左侧。现在可以做几个改进,但是只是做微调,优化。比如,如果你在竖屏WebView模式下并旋转屏幕,结果还是只有WebView界面。你必须点击返回以获得双面视图。程序修正不在这个教程讲述的范围,但是你可以发现,如果使用适当的布局并且加上一些活动逻辑,你可以对于不同的屏幕和设备做到非常强大和灵活。

     

总结

    片段API帮助组织用户界面组件,以使它们可以实现跨活动重用。这样,程序可以在相对少的代码量下,动态地适应它的流程和用户界面。你也能看到基于片段构建的代码更容易重新组织。更值得高兴的是,通过Google提供的兼容库,现在任何程序都可以使用片段了,它甚至兼容到Android 1.6。现在就使用片段来为每一个屏幕大小和形状创建你的程序用户界面吧!



Android的Tween动画的实现框架

在写程序的时候遇到了Tween动画几个问题:

1,  执行动画的时候点击事件仍然在动画开始的位置?

2,  XXXAnimation的构造参数里面的值具体是什么意思?

3,  平移动画中fromXValue和toXValue旋转动画中fromDegrees和toDegrees取负值有什么不同??(相信很多人也有疑惑)

4,  RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四个参数是怎么确定旋转原点的?确定的旋转原点在哪里?

Android动画分为:

Tween Animation  View动画也叫补间动画

Drawable Animation  也叫Frame 帧动画

Property Animation(3.0以后加入)

主要研究Tween动画

我在写程序的时候经常由于参数设置不当(主要是从多少度旋转为多少度,有时是负的度数)得不到想要的效果。因此打算把动画的实现框架研究一下。

研究之前请看这篇文章:Android中图像变换Matrix的原理  了解一下Matrix矩阵的相关知识。明白图形的各种转换就是要得到对应的变换矩阵。

首先说一下动画的大概绘制框架过程,不然由于我写的比较乱可能看晕了。

调用startAnimation会设置与View关联的animation,然后会重绘视图,重绘视图的时候调用到drawChild,这时获取与View绑定的Animation,不为null了,只要动画时间没有结束就会通过绘制的时间获得变换矩阵,然后将画布原点平移到视图的左上角?(是不是这样?)绘制新的一帧。绘制完又会重绘,然后获取新的一帧的转换矩阵…..循环下去,直到动画结束就不再重绘视图了。

回到View的onDraw函数里面,onDraw函数做了如下工作。

1. Draw the background

2. If necessary, save the canvas’ layers toprepare for fading

3. Draw view’s content

4. Draw children

5. If necessary, draw the fading edges andrestore layers

6. Draw decorations (scrollbars forinstance)

当是ViewGroup的时候会执行第四步,dispatchDraw(canvas);

@Override
protected void dispatchDraw(Canvas canvas) {
// LayoutAnimationController比较熟悉,是让ViewGroup的子控件有动画效果,以前没发现竟然也是在这里发生的。
final LayoutAnimationController controller = mLayoutAnimationController;
		...
       // We will draw our child's animation, let's reset the flag
//下面对子View动画进行处理。
        mPrivateFlags &= ~DRAW_ANIMATION;
        mGroupFlags &= ~FLAG_INVALIDATE_REQUIRED;
        boolean more = false;
        final long drawingTime = getDrawingTime();
        if ((flags & FLAG_USE_CHILD_DRAWING_ORDER) == 0) {
            for (int i = 0; i < count; i++) {
                final View child = children[i];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        } else {
            for (int i = 0; i < count; i++) {
                final View child = children[getChildDrawingOrder(count, i)];
                if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) {
                    more |= drawChild(canvas, child, drawingTime);
                }
            }
        }
		...
    }

肯定会执行到drawChild(canvas, child, drawingTime); 在该函数顾名思义就是绘制子控件。

protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
        boolean more = false;

        final int cl = child.mLeft;
        final int ct = child.mTop;
        final int cr = child.mRight;
        final int cb = child.mBottom;

        final int flags = mGroupFlags;

        Transformation transformToApply = null;
//取得该View绑定的动画
        final Animation a = child.getAnimation();
        boolean concatMatrix = false;
//如果该View有了动画那么就会进入if判断执行,没有动画就仅仅绘制该控件。
        if (a != null) {
            if (mInvalidateRegion == null) {
                mInvalidateRegion = new RectF();
            }
            final RectF region = mInvalidateRegion;

            final boolean initialized = a.isInitialized();
            if (!initialized) {
//调用Animation的初始化函数,在这面会解析Animation的各个参数。对不同的xy类型和值进行转换。
                a.initialize(cr - cl, cb - ct, getWidth(), getHeight());
                a.initializeInvalidateRegion(0, 0, cr - cl, cb - ct);
                child.onAnimationStart();
            }

            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
//取得变换(平移,旋转或缩放等)信息,传进去的drawingTime代表了绘制的时间毫秒值,取得的结果放进mChildTransformation里面。
//mChildTransformation是一个图形转换信息的类,包含了一个矩阵Matrix,和alpha值。Matrix就是图形转换矩阵。
//more是该函数的返回值,查看代码很容易分析出来:如果动画没有结束就一只返回true,知道动画结束返回false。
            more = a.getTransformation(drawingTime, mChildTransformation);
            transformToApply = mChildTransformation;
//默认返回true
            concatMatrix = a.willChangeTransformationMatrix();
//more==true进入循环
            if (more) {
			//more==true when the animation is not over
                if (!a.willChangeBounds()) {
                    if ((flags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE)) ==
                            FLAG_OPTIMIZE_INVALIDATE) {
                        mGroupFlags |= FLAG_INVALIDATE_REQUIRED;
                    } else if ((flags & FLAG_INVALIDATE_REQUIRED) == 0) {
                        mPrivateFlags |= DRAW_ANIMATION;
//动画没有结束就会不停调用invalidate函数对动画view进行重绘
                        invalidate(cl, ct, cr, cb);
                    }
                } else {
                    a.getInvalidateRegion(0, 0, cr - cl, cb - ct, region, transformToApply);
                    mPrivateFlags |= DRAW_ANIMATION;
                    final int left = cl + (int) region.left;
                    final int top = ct + (int) region.top;
//动画没有结束就会不停调用invalidate函数对动画view进行重绘
                    invalidate(left, top, left + (int) region.width(), top + (int) region.height());
                }
            }
        } else if ((flags & FLAG_SUPPORT_STATIC_TRANSFORMATIONS) ==
                FLAG_SUPPORT_STATIC_TRANSFORMATIONS) {
            if (mChildTransformation == null) {
                mChildTransformation = new Transformation();
            }
            final boolean hasTransform = getChildStaticTransformation(child, mChildTransformation);
            if (hasTransform) {
                final int transformType = mChildTransformation.getTransformationType();
                transformToApply = transformType != Transformation.TYPE_IDENTITY ?
                        mChildTransformation : null;
                concatMatrix = (transformType & Transformation.TYPE_MATRIX) != 0;
            }
        }
		...
        child.computeScroll();
//分析简单情况下视图都是在可视范围内,sx和sy应该等于0??
        final int sx = child.mScrollX;
        final int sy = child.mScrollY;
		...
        final boolean hasNoCache = cache == null;
        final int restoreTo = canvas.save();
        if (hasNoCache) {
            canvas.translate(cl - sx, ct - sy);
        } else {
        //here translate the canvas's zuobiao???? @auth:qhyuan
//将画布平移到(cl,ct)点,cl和ct是childView的左上角到屏幕(0,0)点的距离。这点非常重要,在重绘动画的时候画布的左边在发生变化!!!并不是一直在屏幕的(0,0)点。
//平移后的坐标体系和最初不一样,一般情况下坐标的原点会移动至View的左上角。
            canvas.translate(cl, ct);
            if (scalingRequired) {
                // mAttachInfo cannot be null, otherwise scalingRequired == false
                final float scale = 1.0f / mAttachInfo.mApplicationScale;
                canvas.scale(scale, scale);
            }
        }
        float alpha = 1.0f;
        if (transformToApply != null) {
            if (concatMatrix) {
                int transX = 0;
                int transY = 0;
                if (hasNoCache) {
                    transX = -sx;
                    transY = -sy;
                }
//两个参数为0
                canvas.translate(-transX, -transY);
// transformToApply是从Animation取得的转换信息类,取得变换矩阵。这个变换矩阵在不同时刻都不一样,因为传过去的drawingTime不一样。
//对画布进行变换矩阵转换,实现动画效果。
                canvas.concat(transformToApply.getMatrix());
                canvas.translate(transX, transY);
                mGroupFlags |= FLAG_CLEAR_TRANSFORMATION;
            }
		...
            if (alpha < 1.0f && hasNoCache) {
                final int multipliedAlpha = (int) (255 * alpha);
                if (!child.onSetAlpha(multipliedAlpha)) {
                    canvas.saveLayerAlpha(sx, sy, sx + cr - cl, sy + cb - ct, multipliedAlpha,
                            Canvas.HAS_ALPHA_LAYER_SAVE_FLAG | Canvas.CLIP_TO_LAYER_SAVE_FLAG);
                } else {
                    child.mPrivateFlags |= ALPHA_SET;
                }
            }
        } else if ((child.mPrivateFlags & ALPHA_SET) == ALPHA_SET) {
            child.onSetAlpha(255);
        }
        ...
        return more;
    }

接下来看一下Animation类:动画类里面有两个重要的函数protected void applyTransformation(float interpolatedTime,Transformation t)

和public boolean getTransformation(long currentTime, TransformationoutTransformation);

Transformation类包含了一个变换矩阵和alpha值。

applyTransformation函数:传入一个差值时间,会填充一个Transformation类。会在getTransformation函数里面调用,Animation类的applyTransformation是个空实现,具体的XXXAnimation在继承自Animation时会实现applyTransformation函数。

getTransformation函数:会在drawChild函数里面调用。

public boolean getTransformation(long currentTime, Transformation outTransformation) {
// currentTime 会在drawChild函数中通过getDrawTime传过来
        if (mStartTime == -1) {
            mStartTime = currentTime;
        }

        final long startOffset = getStartOffset();
        final long duration = mDuration;
        float normalizedTime;
        if (duration != 0) {
//归一化时间,这样normalizedTime是介于0和1之间的值。
            normalizedTime = ((float) (currentTime - (mStartTime + startOffset))) /
                    (float) duration;
        } else {
            // time is a step-change with a zero duration
            normalizedTime = currentTime < mStartTime ? 0.0f : 1.0f;
        }
// expired表示“过期”,如果归一化时间大于1 ,expired == true,即expire表示动画结束了
        final boolean expired = normalizedTime >= 1.0f;
        mMore = !expired;

        if (!mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

        if ((normalizedTime >= 0.0f || mFillBefore) && (normalizedTime <= 1.0f || mFillAfter)) {
            if (!mStarted) {
                if (mListener != null) {
//记录开始动画
                    mListener.onAnimationStart(this);
                }
                mStarted = true;
            }

            if (mFillEnabled) normalizedTime = Math.max(Math.min(normalizedTime, 1.0f), 0.0f);

            if (mCycleFlip) {
                normalizedTime = 1.0f - normalizedTime;
            }
//通过归一化的时间得到插值时间,类似于一个函数f(t)根据归一化的时间得到插值时间。
//插值时间的作用就是得到变化速率改变的效果,例如线性插值就是f(t)=t
            final float interpolatedTime = mInterpolator.getInterpolation(normalizedTime);
//调用applyTransformation函数,具体实现在继承自Animation的类中实现
//简单地说就是传入插值时间,然后该函数根据插值时间填充具体的转换矩阵,不同的时刻对应不同的转换矩阵,通过该转换矩阵就会绘制出在不同位置的图形。
            applyTransformation(interpolatedTime, outTransformation);
        }
//如果动画结束了会执行下面
        if (expired) {
            if (mRepeatCount == mRepeated) {
                if (!mEnded) {
                    mEnded = true;
                    if (mListener != null) {
                        mListener.onAnimationEnd(this);
                    }
                }
            } else {
                if (mRepeatCount > 0) {
                    mRepeated++;
                }

                if (mRepeatMode == REVERSE) {
                    mCycleFlip = !mCycleFlip;
                }

                mStartTime = -1;
                mMore = true;

                if (mListener != null) {
                    mListener.onAnimationRepeat(this);
                }
            }
        }

        if (!mMore && mOneMoreTime) {
            mOneMoreTime = false;
            return true;
        }
//通过分析发现总是返回true,除非动画结束了。
//always return true until the expired==true(the animation is over) @auth:qhyuan
        return mMore;
    }

applyTransformation函数在Animation里面默认是空实现,需要在子类中实现,也就是说自定义动画需要实现applyTransformation函数。

插值类也很简单,是一个接口,只有一个函数。

public interface Interpolator {
   float getInterpolation(float input);
}

常用的子类有:

AccelerateDecelerateInterpolator 在动画开始与结束的地方速率改变比较慢,在中间的时候加速

AccelerateInterpolator  在动画开始的地方速率改变比较慢,然后开始加速

AnticipateInterpolator 开始的时候向后然后向前甩

AnticipateOvershootInterpolator 开始的时候向后然后向前甩一定值后返回最后的值

BounceInterpolator   动画结束的时候弹起

CycleInterpolator 动画循环播放特定的次数,速率改变沿着正弦曲线

DecelerateInterpolator 在动画开始的地方快然后慢

LinearInterpolator   以常量速率改变

OvershootInterpolator    向前甩一定值后再回到原来位置

也可以自定义interpolator,只需要实现getInterpolation函数就可以了。

前面简单的介绍了一下动画绘制所涉及的一些函数,接下来以执行平移动化为例将动画的执行过程走一遍:

1.定义完XXXAnimation后执行View的startAnimation

public void startAnimation(Animation animation) {
        animation.setStartTime(Animation.START_ON_FIRST_FRAME);
//设置Animation域
        setAnimation(animation);
//请求重绘视图
        invalidate();
}

setAnimation函数如下:

public void setAnimation(Animation animation) {
//将animation设置给View的mCurrentAnimation属性
        mCurrentAnimation = animation;
        if (animation != null) {
            animation.reset();
        }
}

2.然后请求重绘视图会执行onDraw函数,最后必然会执行到dispatchDraw函数,又会执行到drawChild(canvas,child, drawingTime); drawingTime函数是这次绘制的时间毫秒值。drawChild函数前面解释过。

3.首先获取View所关联的Animation,然后调用Animation的初始化函数,在这面会解析Animation的各个参数。对不同的xy类型和值进行转换,initialize函数也会在Animation的子类中实现。TranslateAnimation中的initialize函数如下,在该函数里面直接调用resolveSize函数解析构造参数中的值。

public void initialize(int width, int height, int parentWidth, int parentHeight) {
        super.initialize(width, height, parentWidth, parentHeight);
        mFromXDelta = resolveSize(mFromXType, mFromXValue, width, parentWidth);
        mToXDelta = resolveSize(mToXType, mToXValue, width, parentWidth);
        mFromYDelta = resolveSize(mFromYType, mFromYValue, height, parentHeight);
        mToYDelta = resolveSize(mToYType, mToYValue, height, parentHeight);
    }

resolveSize函数如下,根据是绝对坐标、相对控件自身还是相对父控件和具体的值解析出解析后的值,很容易看出绝对坐标,直接返回对应值,相对自身和相对父控件就是用相对值结余0到1之间的值乘以子控件或者父视图宽高的值。

protected float resolveSize(int type, float value, int size, int parentSize) {
        switch (type) {
            case ABSOLUTE:
                return value;
            case RELATIVE_TO_SELF:
                return size * value;
            case RELATIVE_TO_PARENT:
                return parentSize * value;
            default:
                return value;
        }
}

4.然后就根据重绘的时间毫秒值通过getTransformation函数获得对应的转换矩阵。在这个函数里面会先调用interpolatedTime = mInterpolator.getInterpolation(normalizedTime);根据时间获得插值时间。

然后调用applyTransformation函数,TranslateAnimation的该函数具体如下:

protected void applyTransformation(float interpolatedTime, Transformation t) {
        float dx = mFromXDelta;
        float dy = mFromYDelta;
//开始X坐标值和结束值不一样
        if (mFromXDelta != mToXDelta) {
//某个时刻(插值时间)对应的dx,如果是线性插值interpolatedTime和normalizedTime是一样的。
            dx = mFromXDelta + ((mToXDelta - mFromXDelta) * interpolatedTime);
        }
        if (mFromYDelta != mToYDelta) {
            dy = mFromYDelta + ((mToYDelta - mFromYDelta) * interpolatedTime);
        }
//对转换矩阵进行重新设置,将位移差设置进转换矩阵中。平移动化其实也是变换矩阵和原来的坐标点的相乘。
        t.getMatrix().setTranslate(dx, dy);
}

只要该动画没有结束getTransformation函数会一直返回true。然后判断返回true又会调用invalidate函数,接下来就是重复2,3,4的步骤了,但是重复执行的时候绘制时间不一样,于是获得的转换矩阵不一样,得到的新的视图的位置就不一样。如果返回false说明动画执行完成了,就不在重绘视图了。绘制控件时前面说过在绘制视图的时候会调用canvas.translate(cl – sx, ct – sy);简单地说就是将画布的坐标体系从屏幕左上角移动至动画视图的左上角。然后每次动画的时候canvas.concat(transformToApply.getMatrix());对话不的矩阵转换操作,然后绘图就实现了对动画的转换。

整个过程稍微有点复杂,有些函数我还没看的很明白,不过大致的思路就是这样。

回头看开始提出的几个问题:

1,  执行动画的时候其实并不是该控件本身在改变,而是他的父View完成的。startAnimation(anim)其实是给这个View设置了一个animation,而不是进行实际的动画绘制。他的位置其实根本没有改变,还是有layout所指定的位置决定的。

2,  参数的意思在代码里面的注释解释过了。

3,  fromXValue和toXValue旋转动画中fromDegrees和toDegrees取正负值是有区别的,具体要看代码里面,转换矩阵是怎么生成的。比如平移动化里面:

dx = mFromXDelta + ((mToXDelta - mFromXDelta) *interpolatedTime);

插值时间从0变到1,假设现在时相对自身的类型,控件本身宽度为100,mFromXDelta这些值在初始化的时候已经解析成了实际值(从0~1.0这种相和相对自身还是相对父View变成了像素宽高度)

从0变到1.0f的效果是:从当前位置向右平移100,原因是第一帧图像的dx为0,最后一帧dx为100

从-1.0f变到0的效果是:从当前位置的左边100处向右平移到当前位置,原因是第一帧图像的dx为-100,最后一帧的dx为0

旋转动画中通过指定开始结束角度的正负实现顺时针和逆时针旋转是类似的道理。大家可以自行感悟一下下面四个动画的区别,来看一下正负始末值对旋转的影响。

rotateAnimation= new RotateAnimation(0, 90, Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT,0);
rotateAnimation= new RotateAnimation(90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
rotateAnimation= new RotateAnimation(-90, 0, Animation.RELATIVE_TO_PARENT, -0.5f,Animation.RELATIVE_TO_PARENT, 0);
rotateAnimation = new RotateAnimation(0, -90,Animation.RELATIVE_TO_PARENT, -0.5f, Animation.RELATIVE_TO_PARENT, 0);

4,  RotateAnimation的int pivotXType, float pivotXValue, int pivotYType, float pivotYValue四个参数的问题:

由于前面说过了,动画的时候,画布平移到了View的左上角,因此对于旋转动画来说参考的坐标原点始终是View左上角。而旋转的旋转点是可以任意指定的,该旋转点参考的坐标应该是View左上角。

手机屏幕中心处有一个点,现在我们想以屏幕最左边的那一边的中点为圆心旋转360度,代码应该是:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, 0);//注意是-0.5f

以屏幕左上角旋转:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT,-0.5f, Animation.RELATIVE_TO_PARENT, -0.5f);//注意是负的。

以自己中心旋转:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 0.5f);

以自己最下面的中点旋转:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_SELF, 0.5f,Animation.RELATIVE_TO_SELF, 1.0f);

其他旋转点以此类推。我以前只是大致看过这些参数,所以在写围绕屏幕左上角旋转的代码时,想当然写成了:

new RotateAnimation(0, 360, Animation.RELATIVE_TO_PARENT, 0,Animation.RELATIVE_TO_PARENT, 0);

我本来想相对于父View来说(0,0)就是屏幕的左上角那一个点。结果当然不正确,测试一下也可以看出这个时候的旋转点仍然是View的左上角,是不是和RELATIVE_TO_PARENT没有半点关系?

总结一下就是说:后面的四个参数只是能算出来相对于画布坐标的距离,仅此而已,并不能看到是相对父View的值就忘了画布的原点在哪。


Android中级进阶 FAQ

1、Q:Android开发者应该先看什么文档?

      A:对开发者来说,最重要的是概念,而DOCS中的Dev Guide里面对其解释十分清晰,请仔细阅读。并可以到eoeAndroid开发者社区中找到很多的文档和学习资料。


2、Q:Android系统架构有哪些?
     
       A:Android的系统架构和其操作系统一样,采用了分层的架构。从架构图看,android分为四个层,从高层到低层分别是应用程序层、应用程序框架层、系统运行库层和linux核心层。


3、Q:什么是开源?

       A:开源,意为开放源代码,由Bruce Perens(曾是Debian的创始人之一)定义如下: 

          1)自由再散布(Free Distribution):获得源代码的人可自由再将此源代码散布。

           2)源代码(Source Code):程式的可执行档在散布时,必需随附完整源代码或是可让人方便的事后取得源代码。

          3)衍生著作(Derived Works):让人可依此源代码修改后,在依照同一授权条款的情形下再散布。

          4)原创作者程式源代码的完整性(Integrity of The Author’s Source Code):意即修改后的版本,需以不同的版本号码以与原始的程式码做分别,保障原始的程式码完整性。

          5)不得对任何人或团体有差别待遇(No Discrimination Against Persons or Groups):开放源代码软件不得因性别、团体、国家、族群等设定限制,但若是因为法律规定的情形则为例外(如:美国政府限制高加密软件的出口)。

           6)对程式在任何领域内的利用不得有差别待遇(No Discrimination Against Fields of Endeavor):意即不得限制商业使用。

           7)散布授权条款(Distribution of License):若软件再散布,必需以同一条款散布之。

           8)授权条款不得专属于特定产品(License Must Not Be Specific to a Product):若多个程式组合成一套软件,则当某一开放源代码的程式单独散布时,也必需要符合开放源代码的条件。

           9)授权条款不得限制其他软件(License Must Not Restrict Other Software):当某一开放源代码软件与其他非开放源代码软件一起散布时(例如放在同一光碟片),不得限制其他软件的授权条件也要遵照开放源代码的授权。

          10)授权条款必须技术中立(License Must Be Technology-Neutral):意即授权条款不得限制为电子格式才有效,若是纸本的授权条款也应视为有效。


4、Q:如何搭建编程环境?

       A:开发平台推荐Eclipse。VS基本不要想,除非你特别钟爱于传统的vim或其他编程环境并且打算让自己每天敲一堆命令,否则还是用Eclipse吧。它是官方推荐的,目前支持的还算中规中距。而且Linux,windows都能用。
   
            这里拿eoe的技术大牛pcr所使用的开发环境举例,SDK_2.3+Eclipse_3.6.1+ADT 8.0.1。这应该就是目前最普通的开发环境组合了。至于其他的辅助工具,可以在对android有一定了解之后在考虑添加。


5、Q:什么是NDK?

  A:1)NDK是一系列工具的集合。

          NDK 提供了一系列的工具,帮助开发者快速开发C(或C++)的动态库,并能自动将so 和java 应用一起打包成apk。这些工具对开发者的帮助是巨大的。NDK 集成了交叉编译器,并提供了相应的mk 文件隔离CPU、平台、ABI 等差异,开发人员只需要简单修改mk 文件(指出“哪些文件需要编译”、“编译特性要求”等),就可以创建出so。NDK 可以自动地将so 和Java 应用一起打包,极大地减轻了开发人员的打包工作。

          2)NDK提供了一份稳定、功能有限的API头文件声明。

        Google 明确声明该API 是稳定的,在后续所有版本中都稳定支持当前发布的API。从该版本的NDK 中看出,这些API 支持的功能非常有限,包含有:C 标准库(libc)、标准数学库(libm)、压缩库(libz)、Log 库(liblog)


6、Q:怎样提高Android应用程序的速度?

      A:首先, 我们要先明白“加快”是有两层意思的,第一层是代码执行所需要的时间,第二层意思是用户需要等待用户界面响应的时间。下面是提高Android应用程序运行速度的几条原则。

           1)不要让UI线程等待
           2)耗时操作不可取
           3)模拟器和真实的设备有不同
           4)通知用户,要注意用户体验。


7、Q:Android运行库有哪些?
    
      A:Android 包括了一个核心库,该核心库提供了JAVA 编程语言核心库的大多数功能。每一个Android 应用程序都在它自己的进程中运行,都拥有一个独立的Dalvik 虚拟机实例。Dalvik 被设计成一个设备可以同时高效地运行多个虚拟系统。Dalvik 虚拟机执行(.dex)的Dalvik 可执行文件,该格式文件针对小内存使用做了优化。同时虚拟机是基于寄存器的,所有的类都经由JAVA 编译器编译,然后通过SDK中的"dx" 工具转化成.dex 格式由虚拟机执行。Dalvik 虚拟机依赖于linux 内核的一些功能,比如线程机制和底层内存管理机制。


8、Q:Android都支持哪些Java特性

   A:针对于Java SE或Java EE的程序员想转到Android平台上进行开发,有以下几点常规的支持:

           1)目前来看JDK的高级特性均支持,比如说1. Java的反射、2.NIO (New I/O)、3. JNI (Java Native Interface)相对而言 对于OpenGL和SQLite的支持比较强大,但是AWT和JDBC这些东西都不支持。

           2)在Xml解析上,兼容DOM、XmlPull和SAX,同时数据交换格式上,Android虽然不支持LINQ但对于Java来说JSON同样支持。

           3)对于Http处理方面,提供了轻量级的Http处理类,以及更完善的Apache库支持。

           4)音频方面Android比较强大,使用了OpenCore库,很多地方我们可以自己编写编码、解码器进行扩展。

           5)Android在文件系统上基本山和Java是相同的,不过对于高效率的内存影射文件而言提供了android.os.MemoryFile这个类。

          总体而言,Java程序员转入Android开发只需要了解平台特有的,Intent,Service,Receiver和Activity就差不多了,深入了解下AIDL和UI控件和自定义Widget基本上可以胜任常规的工作。

9、Q:Android最简单播放GIF动画方法是什么?

     A:GIF动画的原理就是逐帧播放,在Android中提供了AnimationDrawable类可以实现,GIF89A的解码方法在过去的J2ME平台移植到Android平台也能用,其实在Google Android上面开发目前2.2以后的固件支持的方法除了Flash Player外,更好的兼容方法就是使用万能的webkit浏览器了。直接在工程中内嵌一个webView,当然了路径大家可以换成本地的,对于浏览器使用本地资源url为file://开头。

           不过webView的资源消耗也不小,开个webView对象可能占用了至少8MB的RAM吧,保守估计,当然更多的要看插件和以及html的复杂程度所决定的。

10、Q:线程在Android应用当中的作用?

        A:1)动态更新UI 如AsyncTask 类,在开发Android应用时必须遵守单线程模型的原则: Android UI操作并不是线程安全的并且这些操作必须在UI线程中执行
              2)SOCKET之间的通信
              3)文件的下载 
              4)与服务端之间的交互 
              5)复杂数据和逻辑的处理