Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android基础——初学者必知的AIDL在应用层上的Binder机制

$
0
0

初学者必知的AIDL在应用层上的Binder机制


事先说明:

本人也是个初学者,所以本文是从初学者的角度入手,如果有不妥的地方请留言教导我,谢谢。

本篇文章主要针对讲解AIDL的使用AIDL在应用层上原理解析,其中也会提到在写AIDL服务中的一些错误

首先得理解几个概念:

IPC:Inter-Process Communication,进程间的通信或跨进程通信。简单点理解,一个应用可以存在多个进程,但需要数据交换就必须用IPC;或者是二个应用之间的数据交换。

Binder:Binder是Android的一个类,它实现了IBinder接口。从IPC角度来说,Binder是Android中的一种跨进程通信方式。通过这个Binder对象,客户端就可以获取服务端提供的服务或数据,这里的服务包括普通服务和基于AIDL的服务。

AIDL:Android Interface Definition language,它是一种Android内部进程通信接口的描述语言。

欢迎关注我的CSDN博客,Hensen_的博客,http://blog.csdn.net/qq_30379689


步骤一:AIDL的使用(Android Studio中)

首先我们来看一个我画的图:



服务端:

创建一个服务端工程,在工程中点击右键New->AIDL->AIDL File,默认直接点确定,这时会在工程中出现一个aidl文件:


我们打开这个aidl文件,我们创建一个我们需要测试的方法:


由于Android Studio是要手动编译才能生成对应AIDL的java文件,既然aidl文件是个接口,那就必须存在着实现这个接口的类,点击编译,系统自动生成一个java类,该java类的代码就是整个Binder机制的原理所在(会在下面第二步骤介绍原理):



既然是个服务端,那么我们就要开始写服务了,创建一个类,继承Service:

import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.os.RemoteException;
import android.support.annotation.Nullable;
import android.util.Log;

import com.handsome.boke.IMyAidlInterface;

public class MyService extends Service {
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return myS;
    }

    private IBinder myS = new IMyAidlInterface.Stub() {

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }

        @Override
        public int add(int num1, int num2) throws RemoteException {
            Log.i("Hensen", "从客户端发来的AIDL请求:num1->" + num1 + "::num2->" + num2);
            return num1 + num2;
        }
    };
}

既然是个服务,就必须在manifests文件中配置:

<!--exported:允许外界访问该服务,AIDL必备条件-->
        <service
            android:name=".Aidl.MyService"
            android:exported="true"/>
到现在服务端写好了,开启模拟器启动这个程序,记得在代码中开启服务:

public class LoginActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        startService(new Intent(this, MyService.class));
    }
}


客户端:

在工程中点击右键New->Module,按默认确定,finish:


关键的一步来了,
复制服务端的aidl整个文件夹(包括里面的包、aidl文件、完整无缺)粘贴到客户端对应放aidl的地方


不要忘了,客户端还要
手动编译


好了我们来写客户端的代码(我们在MainActivity中放一个”AIDL“的按钮,先绑定服务,然后点击按钮调用):

public class MainActivity extends AppCompatActivity {

    IMyAidlInterface iMyAidlInterface;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //绑定服务
        Intent intent = new Intent();
        intent.setComponent(new ComponentName("com.handsome.boke", "com.handsome.boke.Aidl.MyService"));
        bindService(intent, conn, BIND_AUTO_CREATE);
    }

    /**
     * 点击“AIDL”按钮事件
     *
     * @param view
     */
    public void add(View view) {
        try {
            int res = iMyAidlInterface.add(1, 2);
            Log.i("Hensen", "从服务端调用成功的结果:" + res);
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }

    /**
     * 服务回调方法
     */
    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iMyAidlInterface = null;
        }
    };

    @Override
    protected void onDestroy() {
        super.onDestroy();
        //解绑服务,回收资源
        unbindService(conn);
    }
}


测试结果(先开启服务端,开启服务后,接着开启客户端,绑定远程服务):

08-19 10:59:34.548 6311-6328/com.handsome.boke I/Hensen: 从客户端发来的AIDL请求:num1->1::num2->2
08-19 10:59:34.550 7052-7052/com.handsome.app2 I/Hensen: 从服务端调用成功的结果:3

步骤二:AIDL的Binder机制原理分析

还是来看一下我画的图:

分析原理:

/*
 * This file is auto-generated.  DO NOT MODIFY.
 * Original file: D:\\workspace5\\Boke\\app\\src\\main\\aidl\\com\\handsome\\boke\\IMyAidlInterface.aidl
 */
package com.handsome.boke;
// Declare any non-default types here with import statements

public interface IMyAidlInterface extends android.os.IInterface {
    /**
     * Local-side IPC implementation stub class.
     */
    public static abstract class Stub extends android.os.Binder implements com.handsome.boke.IMyAidlInterface {
        private static final java.lang.String DESCRIPTOR = "com.handsome.boke.IMyAidlInterface";

        /**
         * Construct the stub at attach it to the interface.
         */
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        /**
         * Cast an IBinder object into an com.handsome.boke.IMyAidlInterface interface,
         * generating a proxy if needed.
         */
        public static com.handsome.boke.IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.handsome.boke.IMyAidlInterface))) {
                return ((com.handsome.boke.IMyAidlInterface) iin);
            }
            return new com.handsome.boke.IMyAidlInterface.Stub.Proxy(obj);
        }

        @Override
        public android.os.IBinder asBinder() {
            return this;
        }

        @Override
        public boolean onTransact(int code, android.os.Parcel data, android.os.Parcel reply, int flags) throws android.os.RemoteException {
            switch (code) {
                case INTERFACE_TRANSACTION: {
                    reply.writeString(DESCRIPTOR);
                    return true;
                }
                case TRANSACTION_basicTypes: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    long _arg1;
                    _arg1 = data.readLong();
                    boolean _arg2;
                    _arg2 = (0 != data.readInt());
                    float _arg3;
                    _arg3 = data.readFloat();
                    double _arg4;
                    _arg4 = data.readDouble();
                    java.lang.String _arg5;
                    _arg5 = data.readString();
                    this.basicTypes(_arg0, _arg1, _arg2, _arg3, _arg4, _arg5);
                    reply.writeNoException();
                    return true;
                }
                case TRANSACTION_add: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
            }
            return super.onTransact(code, data, reply, flags);
        }

        private static class Proxy implements com.handsome.boke.IMyAidlInterface {
            private android.os.IBinder mRemote;

            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }

            @Override
            public android.os.IBinder asBinder() {
                return mRemote;
            }

            public java.lang.String getInterfaceDescriptor() {
                return DESCRIPTOR;
            }

            /**
             * Demonstrates some basic types that you can use as parameters
             * and return values in AIDL.
             */
            @Override
            public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(anInt);
                    _data.writeLong(aLong);
                    _data.writeInt(((aBoolean) ? (1) : (0)));
                    _data.writeFloat(aFloat);
                    _data.writeDouble(aDouble);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public int add(int num1, int num2) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(num1);
                    _data.writeInt(num2);
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }

        static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
    }

    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;

    public int add(int num1, int num2) throws android.os.RemoteException;
}

欢迎关注我的CSDN博客,Hensen_的博客,http://blog.csdn.net/qq_30379689


我们来分析一下这个类:

首先本身继承Iinterface,所以他也是个接口,接口中必须有方法,代码定位到结尾有2个方法。

public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException;
    public int add(int num1, int num2) throws android.os.RemoteException;
这两个方法就是basicTypes和add,就是我们服务端的2个方法。

接着发现该接口中有1个内部类Stub,继承自本身(IMyAidlInterface)接口,代码定位到Stub类。

这个Stub有个构造方法、asInterface、asBinder、onTransact(先不介绍)。

接着发现该内部类Stub还有一个内部类,代码定位到Proxy(我们把它称为代理)类,也是继承自本身(IMyAidlInterface)接口,所以实现该接口的两个方法。

/**
             * Demonstrates some basic types that you can use as parameters
             * and return values in AIDL.
             */
            @Override
            public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, java.lang.String aString) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(anInt);
                    _data.writeLong(aLong);
                    _data.writeInt(((aBoolean) ? (1) : (0)));
                    _data.writeFloat(aFloat);
                    _data.writeDouble(aDouble);
                    _data.writeString(aString);
                    mRemote.transact(Stub.TRANSACTION_basicTypes, _data, _reply, 0);
                    _reply.readException();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
            }

            @Override
            public int add(int num1, int num2) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(num1);
                    _data.writeInt(num2);
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
在这个类里面我们会发现有2个标识:用来区分两个方法,到底你远程请求哪个方法的唯一标识,代码定位到代理类的结尾

 static final int TRANSACTION_basicTypes = (android.os.IBinder.FIRST_CALL_TRANSACTION + 0);
        static final int TRANSACTION_add = (android.os.IBinder.FIRST_CALL_TRANSACTION + 1);
回过头来,还记得我们客户端做了什么吗?答案:绑定一个服务,在回调方法获取一个接口(iMyAidlInterface),它是直接静态使用IMyAidlInterface里面的静态类Stub的asInterface的方法:(好了我们去跟踪到Stub类asInterface这个方法)

 /**
     * 服务回调方法
     */
    private ServiceConnection conn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            iMyAidlInterface = IMyAidlInterface.Stub.asInterface(service);
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {
            iMyAidlInterface = null;
        }
    };
代码定位到Stub类asInterface方法

public static com.handsome.boke.IMyAidlInterface asInterface(android.os.IBinder obj) {
            if ((obj == null)) {
                return null;
            }
            android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
            if (((iin != null) && (iin instanceof com.handsome.boke.IMyAidlInterface))) {
                return ((com.handsome.boke.IMyAidlInterface) iin);
            }
            return new com.handsome.boke.IMyAidlInterface.Stub.Proxy(obj);
        }
前面只是做一些判断、看一下最后一句话:我们将传过来的obj还是传给了它的代理类来处理,返回的是代理类的对象

return new com.handsome.boke.IMyAidlInterface.Stub.Proxy(obj);
所以在客户端的iMyAidlInterface = ……,则是拿到它的代理类,好了,这个时候就看客户端调用代理类干嘛了

int res = iMyAidlInterface.add(1, 2);
他调用了代理类的add方法,代码定位到代理类的add方法
@Override
            public int add(int num1, int num2) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(num1);
                    _data.writeInt(num2);
                    mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
你会发现,它把数据写进了_data里面,最后调用transact方法,传入_data数据,唯一标识Stub.TRANSACTION_add。

mRemote.transact(Stub.TRANSACTION_add, _data, _reply, 0);
然后这个transact方法就是通过底层了,通过底层结束后,这些参数送到哪了?答案:底层会走到stub类中的onTransact方法,通过判断唯一标识,确定方法:
case TRANSACTION_add: {
                    data.enforceInterface(DESCRIPTOR);
                    int _arg0;
                    _arg0 = data.readInt();
                    int _arg1;
                    _arg1 = data.readInt();
                    int _result = this.add(_arg0, _arg1);
                    reply.writeNoException();
                    reply.writeInt(_result);
                    return true;
                }
在这个地方将传过来的参数解包,readInt方法。然后调用this.add方法,this指的就是服务端,调用服务端的add的方法:

int _result = this.add(_arg0, _arg1);
将得到的结果,写入reply

reply.writeNoException();
                    reply.writeInt(_result);
最后一句话,最后返回系统的ontransact方法,传入结果reply:

return super.onTransact(code, data, reply, flags);
所以我们在上面获得的结果就是reply(答案:3):

int res = iMyAidlInterface.add(1, 2);
最后还是看我的画图,加深一下理解:欢迎关注我的CSDN博客,Hensen_的博客,http://blog.csdn.net/qq_30379689


步骤三:AIDL编写时候的一些错误

错误一:

Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'int com.handsome.boke.IMyAidlInterface.add(int, int)' on a null object reference
这个错误很有可能是你写的服务端,忘记返回myS了,返回的是个null

@Override
    public IBinder onBind(Intent intent) {
        return null;
    }
错误二:
 Caused by: java.lang.SecurityException: Not allowed to bind to service Intent { cmp=com.handsome.boke/.Aidl.MyService }
这个错误很有可能是的服务端在manifests文件中少了exported="true"的属性

<!--exported:允许外界访问该服务,AIDL必备条件-->
        <service
            android:name=".Aidl.MyService"/>














作者:qq_30379689 发表于2016/8/20 14:11:06 原文链接
阅读:68 评论:0 查看评论

iOS 调试

$
0
0

调试
对于开发人员来说调试是不可少的。iOS开发目前用的调试器是LLDB,其是用LLVM中可重用组件构建的下一代高性能调试器,包括完成的LLVM编译器。对于我们开发人员来说,这就意味着LLDB能理解编译器所能理解的语法。

在用LLDB之前我们再来看看还有哪些调试的方法。

dSYM(调试信息文件)

dSYM中存储着和目标有关的调试信息。任何一种编程语言写的代码都需要一个编译器,将这些代码翻译成可被运行时环境理解的某种中间语言,或者是可在机器的体系结构上直接运行的原生机器码。
调试器一般在集成开发环境中,开发环境通常支持放置断点使应用停止运行,从而查看变量的值。一般有两类重要的调试器:符号调试器和机器调试器。前者能够在调试代码时显示应用中使用的符号或变量。后者则能够在运行断点时显示逆向过来的汇编代码。符号调试器允许观察代码中的符号而不是寄存器和内存地址。

Xcode的调试信息是dSYM。
这里写图片描述

Build Settings -> Build Options->debug Information Format那栏,我们还可以用命令工具dsymutil创建dSYM。

符号化

很多编译器都是用来将源代码转换成汇编代码。所有汇编代码都有一个基地址,学过汇编语言的同学应该都清楚这个过程,而我们用到的变量,栈和堆都是依赖这个基地址。每次运行,基地址都会变,我已经测试过了,大家也可以写一个小程序测试一下,看看地址。符号化使用方法名或者变量名(符号)来替换基地址的过程。基地址是应用的入口地址,通常都是main方法。除了是一个静态库。方法是计算它们的相对基地址偏移,然后将它们映射到dSYM文件,符号化过程在用Xcode调试应用时才会进行,或者用Instruments做性能分析时进行。

xcarchiver内部机制
xcarchiver包含目录:dSYMs,Products以及一个Info.plist文件。dSYMs包含工程中包含的目标/静态库对应的所有dSYM文件。Products包含所有可执行的二进制文件。Info.plist文件与工程中的plist文件相同,对于识别xcarchiver中的target/dsym版本很重要。我们可以将从iTunes Connect中得到的.crash文件拖到Xcode中时,Xcode内部会查找归档文件,找出与崩溃报告匹配的Info.plist文件,然后从那个归档文件的dSYMs目录获取.dSYM 文件。这也是为什么我们不能删除已提交归档文件的原因。

断点
像上面提到的符号化,dSYM,xcarchiver 我在项目开发中很少用到,而断点是我调试程序离不开的。

异常断点
代码抛出异常是很常见的事,特别是做一个企业级的APP,因为逻辑业务的复杂,数据量较大,在开发过程中,难免有考虑不到的地方,异常也是在所难免的。我们导入的库的一些方法会在不能满足特定条件的情况下抛出异常。像数组越界,返回值为NULL等。调试异常是比较容易的,但是查找原因,有时候是相当复杂的,一般应用崩溃的时候可能只会在日志文件中显示造成崩溃的异常,如果我们不设置断点,仅仅依靠看日志文件发现根源是很困难。

首先我们打开断点导航版,然后选择Add Exception Breakpoint

这里写图片描述
点击‘+’

这里写图片描述

可以看在已经添加了一个异常。当运行我们的项目时,应用程序就停在抛出异常的那行。

符号异常

符号断点会在执行到特定符号时暂停程序。符号可能是一个方法名,类方法,或者C方法(objc_msg_Send)。

这里写图片描述
跟添加异常断点类似,但是选择的是符号断点。

这里写图片描述
编辑我们关注的符号。

不管是异常还是符号断点,我们都可以进行编辑,图片也显示出编辑的效果,大家可以自己尝试一下。

这里写图片描述
我们不仅可以编辑还可以选择共享。点击Share。我们设置的断点就会保存到工程问价报的xcshareddata目录。将该目录提交到版本控制系统,就可以跟团队的所有人共享断点。

观察点

断点可以帮助我们暂停程序的执行,而观察点则帮助我们在某个变量中保存的值发生变化时暂停程序的执行。帮助我们解决了与全局变量有关的问题。可以追踪到具体是哪个方法改变了特定的全局变量。跟断点工作原理类似,不同的是,是在数据被修改时停止执行。

做法如下:

设置观察点

在初始化变量的时候,打一个断点,来初始化这个观察点
当程序运行到这个断点时,我们通过 lldb 命令 watchpoint set v string_weak_ 设置观察点,其中 string_weak_ 是变量的名字。观察点设置成功之后,可以看到相关的日志提示。

观察点设置成功之后,当观察的变量的值发生变化之后,xcode 就会自动断点到修改的位置,暂停执行,可以捕获到该事件,进行相应的分析。
这里写图片描述
配合断点,选择编辑
这里写图片描述
填上我们的触发条件即可。注意不能在程序还没开始运行时添加观察点。

LLDB

Xcode的调试控制台窗口是一个功能完备的LLDB调试控制台。当暂停应用时调试控制台会显示在LLDB命令提示符
这里写图片描述

下面那栏,我这里忘记设置断点,程序运行成功,所以没有打印错误信息。

打印变量

po(print object)是LLDB的一个命令,其主要功能是输出objective-c中对象(objects)的信息,与之相似的另外一个命令是 p(print),其主要功能是输出原生类型(boolean、integer、float、etc)的信息。

控制台输入

p (int)[[[self view] subviews] count]
结果如下
(int) $2 = 1

注意这个使用了类型转换告知调试器应该如何处理返回值。

打印寄存器

在控制台输入:
register read
即可

另外,LLDB调试器的设计由底至上都支持API和插件接口。支持导入Phython脚本调试。

NSZombieEnable标志

这个变量用来调试与内存相关的问题。跟踪对象的释放过程。启用这个变量,会用一个僵尸实现替换默认的dealloc,在引用计数降到0时,该僵尸实现会将该对象转换成僵尸对象,僵尸对象的作用是向它发送消息时,会显示一段日志并自动跳入调试器。

Xcode崩溃类型

1)EXC_BAD_ACCESS
在访问一个已经释放的对象或向它发送消息时,EXC_BAD_ACCESS就会出现。造成EXC_BAD_ACCESS最常见的原因是,在初始化方法中初始化变量时用错了所有权修饰符,这会导致对象被释放。举个例子,在 viewDidLoad 方法中 UITableViewController 创建了一个包含元素的 NSMutableArray,却将该数组的所有权修饰符设成了 unsafe_unretained 或 assign 而不是 strong 。现在在 cellForRowAtIndexPath: 中,若要访问已经释放掉的对象时,就会得到名为 EXC_BAD_ACCESS 的崩溃。

2)SIGSEGV
段错误信号(SIGSEGV) 是操作系统产生的一个更严重的问题。当硬件出现错误、访问不可读的内存地址或向受保护的内存地址写入数据时,就会发生这个错误。
硬件错误这一情况并不常见。当要读取保存在RAM中的数据,而该位置的RAM硬件有问题时,你就会收到SIGSEGV。SIGSEGV更多是出现在后两种情况。默认情况下,代码页不允许进行写操作,而数据页不允许进行执行操作。当应用中的某个指针指向代码页并试图修改指向位置的值时,你就会受到SIGSEGV。当要读取一个指针的值,而它被初始化成指向无效内存地址的垃圾值时,你也会收到SIGSEGV。
SIGSEGV 错误调试起来更困难,而导致SIGSEGV的最常见原因是不正确的类型转换。要避免过度使用指针或尝试手动修改指针来读取私有数据结构。如果你那样做了,而在修改指针时没有注意内存对齐和填充问题,就会收到SIGSEGV。

3)SIGBUS
总线错误信号(SIGBUS)代表无效内存访问,即访问的内存是一个无效的内存地址。也就是说,那个地址指向的位置根本不是物理内存地址(他可能是某个硬件芯片的地址)。SIGBUS 和 SIGSEGV 都属于 EXC_BAD_ACCESS 的子类型。

4)SIGTRAP
SIGTRAP 代表陷阱信号。它并不是一个真正的崩溃信号。它会在处理器执行 trap 指令时发送。LLDB调试器通常会处理此信号,并在指定的断点处停止运行。如果你收到了原因不明的 SIGTRAP,先清除上次的输出,然后重新进行构建通常能解决这个问题。

5)EXC_ARITHMETIC
当要除零时,应用会收到 EXC_ARITHMETIC 信号。这个错误应该很容易解决

6)SIGILL
SIGILL 代表 SIGILL ILLEGAL INSTRUCTION (非法指令信号)。 当在处理器上执行非法指令时,它就会发生。执行非法指令是指,将函数指针传给另外一个函数时,该函数指针由于某种原因是坏的,指向了一段已经释放的内存或是一个数据段。有时你收到的是 EXC_BAD_INSTRUCTION 而不是 SIGILL 。虽然它们是一回事,不过 EXC_* 等同于此信号不依赖体系结构。

7)SIGABRT
SIGABRT代表 SIGNAL ABORT (中止信号)。当操作系统发现不安全的情况时,它能够对这种情况进行更多的控制,必要的话,它能要求进程进行清理工作。在调试造成此信号的底层错误时,并没有什么妙招。 cocos2d 或 UIKit 等框架通常会在特定的前提条件没有满足或一些糟糕的情况出现时调用 C 函数 abort (由它来发送此信号)。当 SIGABRT 出现时,控制台通常会输出大量的信息,说明具体哪里出错了。由于它是可控制的崩溃,所以可以在LLDB控制台上键入 bt命令打印出回溯信息。

8)0x8badf00d (看门狗超时)
这种崩溃通常比较容易分辨,因为错误码是固定的 0x8badf00d。在iOS上,他经常出现在执行一个同步网络调用而阻塞主线程的情况。因此,永远不要进行同步网络调用。

9)自定义错误信号处理程序
大家可以参考这个大神:
http://www.cocoawithlove.com/2010/05/handling-unhandled-exceptions-and.html

渣渣水平,我也云里雾里的。

关于调试还有怎样收集崩溃报告,第三方崩溃报告服务等,我做项目的时候用过Bugly。

作者:SkySuperWL 发表于2016/8/20 14:29:04 原文链接
阅读:56 评论:0 查看评论

Media Data之多媒体扫描过程分析(一)

$
0
0

此分析过程基于Android 6.0源码,转载请注明来源地址http://blog.csdn.net/lemon_blue/article/details/52261758

目录
1.概述
2.多媒体扫描过程分析
3.如何使用多媒体扫描
4.常见问题

1.概述

在Android系统中,多媒体文件通常在开机和SD卡挂载的时候进行扫描操作,目的是为了让多媒体应用便捷地使用和管理多媒体文件。设想一下如果进入多媒体应用才开始扫描,应用的可用性就很差,所以Android系统将这些媒体相关的信息扫描出来保存在数据库中,当打开应用的时候直接去数据库读取(或者所通过MediaProvider去从数据库读取)并展示给用户,这样用户体验会好很多。
下面是其具体的分析过程,分析了两种不同扫描方式的具体实现,和如何使用多媒体扫描,最后对常见的问题讲解。

2.多媒体扫描过程分析

多媒体扫描过程分为两种方式,一种是接收广播的方式,另一种是通过IPC方式。其中通过IPC的方式在底层实现的逻辑与前一种方式部分重合,所以不再重复介绍。
分析的代码层次为:
(1)Java层
(2)JNI层
(3)Native层
这里写图片描述
根据层级,结合流程图,逐渐深入底层进行分析,最终得出整套关于扫描过程的分析结论。

2.1 接收广播方式

在扫描的具体实现中涉及到java层、JNI层和native层,其中MediaScanner.java对应java层,android_media_MediaScanner.cpp对应JNI层,MediaScanner.cpp对应Native层。下面进行逐层分析。

2.1.1 流程图

启动过程
分析过程

2.1.2 MediaScannerReceiver.java

在清单文件中注册的广播:
MediaScannerReceiver
android.intent.action.BOOT_COMPLETED 开机广播
android.intent.action.MEDIA_MOUNTED 外部存储挂载
android.intent.action.MEDIA_UNMOUNTED 外部存储卸载
android.intent.action.MEDIA_SCANNER_SCAN_FILE 扫描单独的文件

接收开机广播的操作:

// Scan both internal and external storage
scan(context, MediaProvider.INTERNAL_VOLUME);
scan(context, MediaProvider.EXTERNAL_VOLUME);

对其他广播的操作。获取外部存储设备的路径,监听两种广播
一种是监听外部存储设备的挂载,另一种是接收指定文件的扫描。

// handle intents related to external storage
                String path = uri.getPath();
                //从log中的值为/storage/emulated/0
                String externalStoragePath =
                           Environment.getExternalStorageDirectory().getPath();
                //从log中的值为/sdcard
                String legacyPath =
                           Environment.getLegacyExternalStorageDirectory().getPath();
                try {
                // An absolute path is one that begins at the root of the file system.
                //A canonical path is an absolute path with symbolic links
                    path = new File(path).getCanonicalPath();
                } catch (IOException e) {
                    return;
                }
                if (path.startsWith(legacyPath)) {
                    path = externalStoragePath + path.substring(legacyPath.length());
                }
                //对其他广播进行的处理
                if (Intent.ACTION_MEDIA_MOUNTED.equals(action)||
 ACTION_MEDIA_SCANNER_SCAN_ALL.equals(action)) {
                    //接收到外部存储挂载的广播之后扫描外部存储
                    // scan whenever any volume is mounted
                    scan(context, MediaProvider.EXTERNAL_VOLUME);
                } else if (Intent.ACTION_MEDIA_SCANNER_SCAN_FILE.equals(action) &&
                        path != null && path.startsWith(externalStoragePath + "/")) {
                    //接收扫描单一文件的广播,扫描单一文件
                    scanFile(context, path);
                }

在调用的scan方法去启动MediaScannerService,并且装填所对应的存储卷

private void scan(Context context, String volume) {
        Bundle args = new Bundle();
        args.putString("volume", volume);
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }

scanFile装填的参数是对应要扫描的路径

private void scanFile(Context context, String path) {
        Bundle args = new Bundle();
        args.putString("filepath", path);
        context.startService(
                new Intent(context, MediaScannerService.class).putExtras(args));
    }    

至此,MediaScannerReceiver分析完毕,内容较少,其作用主要就是:
(1) 接收广播
(2) 构造对应的扫描路径
(3) 启动MediaScannerService

2.1.3 MediaScannerService.java

分析Service首先分析其生命周期中所作的相关操作。先看onCreate函数中有哪些操作:

@Override
    public void onCreate(){
        PowerManager pm = (PowerManager)getSystemService(Context.POWER_SERVICE);
        //新建电源锁,保证扫描过程中系统不会休眠
        mWakeLock = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, TAG);
        StorageManager storageManager =
                (StorageManager)getSystemService(Context.STORAGE_SERVICE);
        //获取外部存储路径
        mExternalStoragePaths = storageManager.getVolumePaths();

        // Start up the thread running the service.  Note that we create a
        // separate thread because the service normally runs in the process's
        // main thread, which we don't want to block.
        Thread thr = new Thread(null, this, "MediaScannerService");
        thr.start();
    }
... ...
public void run(){
        // reduce priority below other background threads to avoid interfering
        // with other services at boot time.
        Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND +
                Process.THREAD_PRIORITY_LESS_FAVORABLE);
        //开启消息队列
        Looper.prepare();
        mServiceLooper = Looper.myLooper();
        //创建Handler,在线程中处理相关操作
        mServiceHandler = new ServiceHandler();
        Looper.loop();
    }

在正常情况下,Android系统会让程序和服务进入休眠状态以节约电量使用或者降低CPU消耗,而扫描任务可能会耗时较长,为了不让在扫描过程中出现系统休眠状态,要保证此时CPU一直不会休眠。
WakeLock是一种锁机制,只要有拿着这把锁,系统就无法进入休眠阶段。既然要保持应用程序一直在后台运行,那自然要获得这把锁才可以保证程序始终在后台运行。如果需要持有锁,需要调用acquire()方法,在不需要的时候即使释放,调用release()方法。
将工作线程的优先级降低是由于扫描过程中会很耗时,如果CPU一直被MediaScannerService占用就会影响其他的线程使用。
在onCreate中的操作有:
1. 获取WakeLock锁和外部存储路径
2. 新建工作线程
在service的生命周期中,onCreate只能调用一次,但是onStartCommand可以重复调用,也就是说每当启动一次startService,就会调用一次onStartCommand,下面分析onStartCommand函数。

@Override
    public int onStartCommand(Intent intent, int flags, int startId){
        //确保mServiceHandler已经被启动
        while (mServiceHandler == null) {
            synchronized (this) {
                try {
                    wait(100);
                } catch (InterruptedException e) {
                }
            }
        }
... ...
        Message msg = mServiceHandler.obtainMessage();
        msg.arg1 = startId;
        msg.obj = intent.getExtras();
        //向mServiceHandler发送消息
        mServiceHandler.sendMessage(msg);
        // Try again later if we are killed before we can finish scanning.
        return Service.START_REDELIVER_INTENT;
    }

在onStartCommand中主要的操作就是获取启动Intent的相关参数,并且发送给工作线程进行处理。
接下来分析mServiceHandler在接收消息之后是如何处理的:

public void handleMessage(Message msg) {
        Bundle arguments = (Bundle) msg.obj;
        String filePath = arguments.getString("filepath");
        try {
            if (filePath != null) {
                //处理扫描指定路径的操作
                IBinder binder = arguments.getIBinder("listener");
                IMediaScannerListener listener = 
                    (binder == null ? null : IMediaScannerListener.Stub.asInterface(binder));
                Uri uri = null;
                try {
                    uri = scanFile(filePath, arguments.getString("mimetype"));
                } catch (Exception e) {
                    Log.e(TAG, "Exception scanning file", e);
                }
                if (listener != null) {
                    listener.scanCompleted(filePath, uri);
                }
            } else {
                //如果没有指定路径,就直接扫描对应的存储卷
                String volume = arguments.getString("volume");
                String[] directories = null;
                if (MediaProvider.INTERNAL_VOLUME.equals(volume)) {
                    // scan internal media storage
                    //分别获取根目录和OEM分区的media
                    directories = new String[] {
                            Environment.getRootDirectory() + "/media",
                            Environment.getOemDirectory() + "/media",
                    };
                    if (RegionalizationEnvironment.isSupported()) {
                        final List<File> regionalizationDirs = RegionalizationEnvironment
                                .getAllPackageDirectories();
                        if (regionalizationDirs.size() > 0) {
                            String[] mediaDirs =
                                new String[directories.length + regionalizationDirs.size()];
                            for (int i = 0; i < directories.length; i++) {
                                mediaDirs[i] = directories[i];
                            }
                            int j = directories.length;
                            for (File f : regionalizationDirs) {
                                mediaDirs[j] = f.getAbsolutePath() + "/system/media";
                                j++;
                            }
                            directories = mediaDirs;
                        }
                    }
                }
                else if (MediaProvider.EXTERNAL_VOLUME.equals(volume)) {
                    // scan external storage volumes
                    directories = mExternalStoragePaths;
                }
                if (directories != null) {
                    //调用scan函数,开始扫描文件
                    scan(directories, volume);
                }
            }
        } catch (Exception e) {
            Log.e(TAG, "Exception in handleMessage", e);
        }
        //停止掉对应的service的id
        stopSelf(msg.arg1);
    }

handleMessage方法中主要的操作就是调用scan方法进行扫描。

private void scan(String[] directories, String volumeName) {
    Uri uri = Uri.parse("file://" + directories[0]);
    // don't sleep while scanning
    mWakeLock.acquire();
    try {
        ContentValues values = new ContentValues();
        values.put(MediaStore.MEDIA_SCANNER_VOLUME, volumeName);
        //从 getContentResolver获得一个ContentResover,然后直接插入
        //根据AIDL,这个ContentResover的另一端是MediaProvider。作用是让其做一些准备工作
        Uri scanUri = getContentResolver().insert(MediaStore.getMediaScannerUri(), values);
        //发送开始扫描的广播 
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_STARTED, uri));
        try {
            if (volumeName.equals(MediaProvider.EXTERNAL_VOLUME)) {
                openDatabase(volumeName);
            }
            //创建MediaScanner对象并开启扫描操作
            MediaScanner scanner = createMediaScanner();
            scanner.scanDirectories(directories, volumeName);
        } catch (Exception e) {
            Log.e(TAG, "exception in MediaScanner.scan()", e);
        }
        //通过特殊的Uri进行相关的清理工作
        getContentResolver().delete(scanUri, null, null);
    } finally {
        //发送扫描完成的广播,释放锁
        sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_FINISHED, uri));
        mWakeLock.release();
    }
}
... ...
private void openDatabase(String volumeName) {
    try {
        ContentValues values = new ContentValues();
        values.put("name", volumeName);
        //调用MediaProvider的insert方法,进行插值
        getContentResolver().insert(Uri.parse("content://media/"), values);
    } catch (IllegalArgumentException ex) {
        Log.w(TAG, "failed to open media database");
    }         
}

private MediaScanner createMediaScanner() {
    MediaScanner scanner = new MediaScanner(this);
    //获取语言信息,将文件转化成此时的语言
    Locale locale = getResources().getConfiguration().locale;
    if (locale != null) {
        String language = locale.getLanguage();
        String country = locale.getCountry();
        String localeString = null;
        if (language != null) {
            if (country != null) {
                //设置语言
                scanner.setLocale(language + "_" + country);
            } else {
                scanner.setLocale(language);
            }
        }    
    }
    return scanner;
}

在MediaScannerService中的onCreate和onStartCommand已经分析完成了,剩下的onDestory只是将Looper退出。

2.1.4 MediaScanner.java

在上面的分析中,MediaScannerService的createMediaScanner方法实例化MediaScanner对象,并且配置语言的。下面先从MediaScanner的创建分析,并且介绍相关的具体方法。
对于MediaScanner的初始化过程,首先执行的是静态代码块,然后是构造函数。

static {
    //加载libmedia_jni.so
    System.loadLibrary("media_jni");
    native_init();
}
public MediaScanner(Context c) {
    native_setup();
    mContext = c;
    mPackageName = c.getPackageName();
    mBitmapOptions.inSampleSize = 1;
    mBitmapOptions.inJustDecodeBounds = true;
    setDefaultRingtoneFileNames();
    mExternalStoragePath = Environment.getExternalStorageDirectory().getAbsolutePath();
    mExternalIsEmulated = Environment.isExternalStorageEmulated();
}

在初始化的过程中native_init();和native_setup();方法放在JNI层分析。
在MediaScannerService中调用了MediaScanner的scanDirectories方法,此方法是java层具体的扫描实现。

public void scanDirectories(String[] directories, String volumeName) {
    try {
        long start = System.currentTimeMillis();
        //扫描之前的初始化
        initialize(volumeName);
        //扫描之前的预处理
        prescan(null, true);
        long prescan = System.currentTimeMillis();
        if (ENABLE_BULK_INSERTS) {
            // create MediaInserter for bulk inserts
            //A MediaScanner helper class which enables us to do lazy insertion on the given provider. 
            //参数500是每条Uri所占的buffer大小
            mMediaInserter = new MediaInserter(mMediaProvider, mPackageName, 500);
        }
        for (int i = 0; i < directories.length; i++) {
            //此方法是native方法,用来扫描文件,参数directories[i]是传入的路径数组 
            //mClient是MyMediaScannerClient的实例,之后会继续分析
            processDirectory(directories[i], mClient);
        }

        if (ENABLE_BULK_INSERTS) {
            // flush remaining inserts
            // Note that you should call flushAll() after using this class.
            mMediaInserter.flushAll();
            mMediaInserter = null;
        }
        long scan = System.currentTimeMillis();
        //处理扫描完成之后的操作
        postscan(directories);
        long end = System.currentTimeMillis();
    }//catch各种异常
    } finally {
        // release the DrmManagerClient resources
        releaseResources();
    }
}
private void initialize(String volumeName) {
    //获取MediaProvider对象
    mMediaProvider = mContext.getContentResolver().acquireProvider("media");
    //初始化不同类型数据的Uri,供之后根据不同的表进行插值
    mAudioUri = Audio.Media.getContentUri(volumeName);
    mVideoUri = Video.Media.getContentUri(volumeName);
    mImagesUri = Images.Media.getContentUri(volumeName);
    mThumbsUri = Images.Thumbnails.getContentUri(volumeName);
    mFilesUri = Files.getContentUri(volumeName);
    mFilesUriNoNotify = mFilesUri.buildUpon().appendQueryParameter("nonotify", "1").build();
    //如果是外部存储,则可以获得播放列表的Uri
    if (!volumeName.equals("internal")) {
        // we only support playlists on external media
        mProcessPlaylists = true;
        mProcessGenres = true;
        mPlaylistsUri = Playlists.getContentUri(volumeName);
        mCaseInsensitivePaths = true;
    }
}
private void prescan(String filePath, boolean prescanFiles) throws RemoteException {
    Cursor c = null;
    String where = null;
    String[] selectionArgs = null;
    if (mPlayLists == null) {
        // mPlayLists的初始化
        mPlayLists = new ArrayList<FileEntry>();
    } else {
        mPlayLists.clear();
    }
    if (filePath != null) {
        // query for only one file
        //拼接where语句
        where = MediaStore.Files.FileColumns._ID + ">?" +
            " AND " + Files.FileColumns.DATA + "=?";
        selectionArgs = new String[] { "", filePath };
    } else {
        where = MediaStore.Files.FileColumns._ID + ">?";
        selectionArgs = new String[] { "" };
    }
    // Tell the provider to not delete the file.
    // If the file is truly gone the delete is unnecessary, and we want to avoid
    // accidentally deleting files that are really there (this may happen if the
    // filesystem is mounted and unmounted while the scanner is running).
    Uri.Builder builder = mFilesUri.buildUpon();
    builder.appendQueryParameter(MediaStore.PARAM_DELETE_DATA, "false");
    MediaBulkDeleter deleter = new MediaBulkDeleter(mMediaProvider, mPackageName,
            builder.build());
    // Build the list of files from the content provider
    try {
        if (prescanFiles) {
            // First read existing files from the files table.
            // Because we'll be deleting entries for missing files as we go,
            // we need to query the database in small batches, to avoid problems
            // with CursorWindow positioning.
            long lastId = Long.MIN_VALUE;
            //指定查询1000条数据
            Uri limitUri = 
                   mFilesUri.buildUpon().appendQueryParameter("limit", "1000").build();
            mWasEmptyPriorToScan = true;
            while (true) {
                //拼装where查询的参数
                selectionArgs[0] = "" + lastId;
                if (c != null) {
                    c.close();
                    c = null;
                }
                //开始查询
                c = 
              mMediaProvider.query(mPackageName, limitUri, FILES_PRESCAN_PROJECTION,
                        where, selectionArgs, MediaStore.Files.FileColumns._ID, null);
                if (c == null) {
                    break;
                }
                int num = c.getCount();
                if (num == 0) {
                    break;
                }
                mWasEmptyPriorToScan = false;
                while (c.moveToNext()) {
                    //获取查询的数据
                    long rowId = c.getLong(FILES_PRESCAN_ID_COLUMN_INDEX);
                    String path = c.getString(FILES_PRESCAN_PATH_COLUMN_INDEX);
                    int format = c.getInt(FILES_PRESCAN_FORMAT_COLUMN_INDEX);
                    long lastModified =
                          c.getLong(FILES_PRESCAN_DATE_MODIFIED_COLUMN_INDEX);
                    lastId = rowId;
                    // Only consider entries with absolute path names.
                    // This allows storing URIs in the database without the
                    // media scanner removing them.
                    if (path != null && path.startsWith("/")) { 
                        boolean exists = false;
                        try {
                            //获取此路径下是否有文件
                            exists = Os.access(path, android.system.OsConstants.F_OK);
                        } catch (ErrnoException e1) {
                        }
                        if (!exists && !MtpConstants.isAbstractObject(format)) {
                            // do not delete missing playlists, since they may have been
                            // modified by the user.
                            // The user can delete them in the media player instead.
                            // instead, clear the path and lastModified fields in the row
                            MediaFile.MediaFileType mediaFileType =
                                  MediaFile.getFileType(path);
                            int fileType = (mediaFileType == null ? 0 :
                                  mediaFileType.fileType);
                            if (!MediaFile.isPlayListFileType(fileType)) {
                                //删除掉指定的数据
                                deleter.delete(rowId);
                               if (path.toLowerCase(Locale.US).endsWith("/.nomedia")) {
                                    deleter.flush();
                                    String parent = new File(path).getParent();
**
 * The method name used by the media scanner and mtp to tell the media provider to
 * rescan and reclassify that have become unhidden because of renaming folders or
 * removing nomedia files
 * @hide
 */
                                    mMediaProvider.call(mPackageName,
                                            MediaStore.UNHIDE_CALL,parent, null);
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    finally {
        if (c != null) {
            c.close();
        }
        deleter.flush();
    }
    // compute original size of images
    mOriginalCount = 0;
    c = mMediaProvider.query(mPackageName, mImagesUri, ID_PROJECTION, null, null, null, null);
    if (c != null) {
        mOriginalCount = c.getCount();
        c.close();
    }
}

private void postscan(String[] directories) throws RemoteException {

    // handle playlists last, after we know what media files are on the storage.
    if (mProcessPlaylists) {
        processPlayLists();
    }
    //如果图片的数目为0,并且是外部存储,则清除掉无效的略缩图文件
    if (mOriginalCount == 0 && mImagesUri.equals(Images.Media.getContentUri("external")))
        pruneDeadThumbnailFiles();
    // allow GC to clean up
    mPlayLists = null;
    mMediaProvider = null;
}

至此,关于java层的分析已经完成,剩下几个比较重要的JNI函数需要分析,分别是native_init,native_setup和processDirectory。接下来就开始分析JNI层。

Media Data之多媒体扫描过程分析(二)
Media Data之多媒体扫描过程分析(三)

作者:lemon_blue 发表于2016/8/20 15:42:27 原文链接
阅读:16 评论:0 查看评论

Android 仿微信联系人Demo(自定义View,Viewgroup)

$
0
0


上周在某博客发现博主分享了一篇很经典的程序---------联系人效果。感觉很神秘很强大,但在阅读和理解博主的demo的同时也发现了一些冗余和不完美。于是带着宝宝的痛一咬牙自己开工了,大约花了一周的时间(当然我白天还得上班的),做出了这种效果。如下图:




now跟着我的思路分析开发过程。


一、界面的数据列表是recyclerview做的,或许listview也可以,但是没试过。

在xml中定义recyclerview,然后在activity中获取对象,创建适配器,设置数据给recyclerview。

数据是我自定义的静态数据

/**
 * @Author: duke
 * @DateTime: 2016-08-12 17:15
 * @Description:
 */
public class Data {
    //模拟数据
    public static final String[] data = {
            "安刚", "Android Studio", 
            "杜科", "杜科>", "杜科》", "董卓", "达尔文", "董卓", "段誉",
            .........
            "周家大湾", "章鱼", "张三", "支那",
            "2哥", "4爷", "6+1", "0^_^0", "@126.com", "(!@#$%^&*)"};
}

那么recyclerview的核心在于适配器:

/**
 * @Author: duke
 * @DateTime: 2016-08-12 15:34
 * @Description:
 */
public class ContactAdapter extends RecyclerView.Adapter<ContactAdapter.ContactViewHolder> {
    

    @Override
    public void onBindViewHolder(final ContactViewHolder holder, final int position) {
        if (list == null || list.size() <= 0)
            return;
        final Contact contact = list.get(position);
        holder.tvHeader.setText(contact.firstPinYin);
        holder.tvName.setText(contact.name);
        holder.tvName.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                onItemClickListener.onItemClick(holder.getLayoutPosition(), contact);
            }
        });
        if (position == 0) {
            holder.tvHeader.setText(contact.firstPinYin);
            holder.tvHeader.setVisibility(View.VISIBLE);
        } else {
            if (!TextUtils.equals(contact.firstPinYin, list.get(position - 1).firstPinYin)) {
                holder.tvHeader.setVisibility(View.VISIBLE);
                holder.tvHeader.setText(contact.firstPinYin);
                holder.itemView.setTag(SHOW_HEADER_VIEW);
            } else {
                holder.tvHeader.setVisibility(View.GONE);
                holder.itemView.setTag(DISMISS_HEADER_VIEW);
            }
        }
        holder.itemView.setContentDescription(contact.firstPinYin);
    }

    public interface OnItemClickListener {
        void onItemClick(int position, Contact contact);
    }
}
item布局文件:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical">

    <TextView
    android:id="@+id/tv_header"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="#FF9933"
    android:gravity="center_vertical"
    android:paddingLeft="10dp"
    android:text="A"
    android:textColor="@android:color/white"
    android:textSize="18sp" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="match_parent"
        android:layout_height="40dp"
        android:layout_centerVertical="true"
        android:background="@drawable/item_tv_name_selector"
        android:gravity="center_vertical"
        android:paddingLeft="10dp"
        android:text="name"
        android:textColor="@android:color/black"
        android:textSize="15sp" />
</LinearLayout>

每个item都包含头部的字母栏和下面的名字栏。如果是第一个item当然需要显示header栏,然后后面的每个item设置数据的时候都需要判断当前所属首字母组和前面是否相同。不相同则说明是新的组,需要显示header;否则说明是相同的组,就隐藏header了。至此,即可实现带header栏的listview效果了。

所以java bean至少需要2个属性,名称和首字母。

recyclerview默认没有带item之间的分割线,需要自己实现,还好我已经为你准备好了万能分割线工具类,文章地址:http://blog.csdn.net/fesdgasdgasdg/article/details/52003701

再来分析中间的提示view是字母弄的呢?我知道你们肯定会说:简单,弄个textview什么的,设置背景为一个圆角即可。

是,不过我这儿有点犯贱了,弄了自定义view,不要怕,后续文章我会根据我的理解发一系列的自定义view,自定义viewgroup文章,随时关注我。

代码:

1、属性文件:

<!-- properties for CenterTipView -->
    <declare-styleable name="CenterTipView">
        <attr name="bgColor" format="color|reference" />
        <attr name="textColor" format="color|reference" />
        <attr name="textSize" format="dimension|reference" />
        <attr name="text" format="string" />
        <attr name="type">
            <enum name="round" value="0" />
            <enum name="circle" value="1" />
        </attr>
    </declare-styleable>

可以设置类型,即中间的view背景可以是圆形或者圆角矩形,可以设置背景、字体等信息。

2、类代码:

/**
 * @Author: duke
 * @DateTime: 2016-08-12 16:40
 * @Description: 中间提示view, 圆角矩形或者圆形背景
 */
public class CenterTipView extends View {
    //画笔
    private Paint mPaint;
    //画笔防锯齿
    private PaintFlagsDrawFilter paintFlagsDrawFilter;
    //图形背景颜色
    private int bgColor;
    //文本内容
    private String text;
    //文本颜色
    private int textColor;
    //字体大小
    private int textSize;
    //类型
    private int type;
    //圆角矩形或者圆形
    public static final int TYPE_ROUND = 0;
    public static final int TYPE_CIRCLE = 1;

    private int mWidth;//宽
    private int mHeight;//高
    private int mMin;//宽高中的最小值

    //文本边界
    private Rect mBound;

    /**
     * 设置文本,重绘界面
     *
     * @param text
     */
    public void setText(String text) {
        this.text = text;
        postInvalidate();
    }

    public String getText() {
        return text;
    }

    public CenterTipView(Context context) {
        this(context, null);
    }

    public CenterTipView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public CenterTipView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    public void init(Context context, AttributeSet attrs) {
        mPaint = new Paint();
        //画笔防锯齿
        paintFlagsDrawFilter = new PaintFlagsDrawFilter(0, Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);

        //获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.CenterTipView);
        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.CenterTipView_bgColor:
                    //背景颜色
                    bgColor = typedArray.getColor(attr, Color.BLACK);
                    break;
                case R.styleable.CenterTipView_textColor:
                    //文本颜色
                    textColor = typedArray.getColor(attr, Color.WHITE);
                    break;
                case R.styleable.CenterTipView_text:
                    //文本内容
                    text = typedArray.getString(attr);
                    break;
                case R.styleable.CenterTipView_type:
                    //图形类型
                    type = typedArray.getInt(R.styleable.CenterTipView_type, 0);
                    break;
                case R.styleable.CenterTipView_textSize:
                    //字体大小
                    textSize = typedArray.getDimensionPixelSize(attr,
                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
            }
        }
        //回收属性数组
        typedArray.recycle();
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        mWidth = getMeasuredWidth();
        mHeight = getMeasuredHeight();
        mMin = Math.min(mWidth, mHeight);
        //依据最小的边,方便画圆
        setMeasuredDimension(mMin, mMin);
    }

    @Override
    public void draw(Canvas canvas) {
        //防锯齿
        canvas.setDrawFilter(paintFlagsDrawFilter);
        mPaint.setColor(bgColor);
        if (type == TYPE_ROUND) {
            //画圆角矩形
            RectF rectF = new RectF(0, 0, mWidth, mHeight);
            canvas.drawRoundRect(rectF, 10, 10, mPaint);
        } else if (type == TYPE_CIRCLE) {
            //画圆
            canvas.drawCircle(mMin >> 1, mMin >> 1, mMin >> 1, mPaint);
        }
        //设置文本颜色
        mPaint.setColor(textColor);
        mPaint.setTextSize(textSize);
        if (mBound == null)
            mBound = new Rect();
        mPaint.getTextBounds(text, 0, text.length(), mBound);
        canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint);
        super.draw(canvas);
    }
}

代码并不多,也简单。首先继承view,重写必要的构造方法,在初始化方法中读取属性文件,获取相应的属性值。

在测量方法中做了简单处理,防止画圆时变形。

核心方法为onDraw。在里面来绘制界面需要显示的内容,根据xml设置的属性判断是画圆还是画圆角矩形。

然后绘制传递进来的字母索引文本。画文本时需要注意下面方法:

mBound = new Rect();
mPaint.getTextBounds(text, 0, text.length(), mBound);
canvas.drawText(text, (mWidth - mBound.width()) >> 1, (mHeight + mBound.height()) >> 1, mPaint);

调用paint.getTextBounds方法,传递rect对象进去。然后没有返回值,字母rect就有值了呢?以前我一直过不去这儿。

其实涉及到值引用和地址引用问题。比喻下面代码:

public class Test {
	public static void main(String[] args) {
		int[] arr = {1,2,3};
		int a = 4;
		//修改值
		update(arr,a);
		//这儿打印值,有变化吗?
		System.out.println(arr[0]+"--"+a);
	}
	
	public static void update(int[] tempArr,int tempA){
		tempArr[0] = 0;
		tempA = 0;
	}
}

以前面试碰到这类问题,去试试。

继续说,paint.getTextBounds方法调用之后,文本的宽高范围数据已经保存到了rect对象中了。

然后根据width、height以及rect信息,来确定在canvas上怎么画text。有一点值得注意,默认画出的文本不是在当前view的(0,0)点。


工作完成了一半,剩下的就是右侧索引导航和上边的固定头怎么弄?先看右边的导航

其实也简单,发现有人用自定义view,纵向迭代绘制首字母集合即可,注意换行。在touch时根据按下处的高度计算出索引位置,确定文本是什么,然后重绘view。

然而,我有点逆火,正好不会自定义viewgroup,那就试试呗。

自定义viewgroup吧,去集成Linearlayout很简单的,设置好线性布局的方向为纵向,剩下的只负责添加child即可,不关心测量和布局了。

然后我又犯贱了一回,我继承的是viewgroup,也就意味着我需要自己去写操蛋的onMeasure和onLayout方法,以及自定义LayoutParams类等。

/**
 * @Author: duke
 * @DateTime: 2016-08-12 16:40
 * @Description: 右边索引导航view
 */
public class RightIndexView extends ViewGroup {
    private Context mContext;
    private ArrayList<String> list = new ArrayList<>();

    //自定义属性(item的背景默认透明)
    private int rootBgColor;
    private int rootTouchBgColor;
    private int itemTouchBgColor;
    private int itemTextColor;
    private int itemTextTouchBgColor;
    private int itemTextSize;

    public RightIndexView(Context context) {
        this(context, null);
    }

    public RightIndexView(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public RightIndexView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs);
    }

    private void init(Context context, AttributeSet attrs) {
        //获取自定义属性
        TypedArray typedArray = context.obtainStyledAttributes(attrs, R.styleable.RightIndexView);
        int count = typedArray.getIndexCount();
        for (int i = 0; i < count; i++) {
            int attr = typedArray.getIndex(i);
            switch (attr) {
                case R.styleable.RightIndexView_rootBgColor:
                    //容器的背景颜色,没有则使用指定的默认值
                    rootBgColor = typedArray.getColor(attr, Color.parseColor("#80808080"));
                    break;
                case R.styleable.RightIndexView_rootTouchBgColor:
                    //容器touch时的背景颜色
                    rootTouchBgColor = typedArray.getColor(attr, Color.parseColor("#EE808080"));
                    break;
                case R.styleable.RightIndexView_itemTouchBgColor:
                    //item项的touch时背景颜色(item的背景默认透明)
                    itemTouchBgColor = typedArray.getColor(attr, Color.parseColor("#000000"));
                    break;
                case R.styleable.RightIndexView_itemTextColor:
                    //item的文本颜色
                    itemTextColor = typedArray.getColor(attr, Color.parseColor("#FFFFFF"));
                    break;
                case R.styleable.RightIndexView_itemTextTouchBgColor:
                    //item在touch时的文本颜色
                    itemTextTouchBgColor = typedArray.getColor(attr, Color.parseColor("#FF0000"));
                    break;
                case R.styleable.RightIndexView_itemTextSize:
                    //item的文本字体(默认16)
                    itemTextSize = typedArray.getDimensionPixelSize(attr,
                            (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 16, getResources().getDisplayMetrics()));
                    break;
            }
        }
        //回收属性数组
        typedArray.recycle();
        this.mContext = context;
        //设置容器默认背景
        setBackgroundColor(rootBgColor);
        //获取系统指定的最小move距离
        mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
    }

    /**
     * 测量子view和自己的大小
     * @param widthMeasureSpec
     * @param heightMeasureSpec
     */
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        //获取系统测量的参数
        mWidth = MeasureSpec.getSize(widthMeasureSpec);
        mHeight = MeasureSpec.getSize(heightMeasureSpec);
        //第一个元素的top位置
        int top = 5;
        //item个数
        int size = 0;
        if (list != null && list.size() > 0) {
            //获取子孩子个数
            size = list.size();
            //上下各减去5px,除以个数计算出每个item的应有height
            mItemHeight = (mHeight - marginTop - marginBottom) / size;
        }
        /**
         * 此循环只是测量计算textview的上下左右位置数值,保存在其layoutParams中
         */
        for (int i = 0; i < size; i++) {
            TextView textView = (TextView) getChildAt(i);
            RightIndexView.LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
            layoutParams.height = mItemHeight;//每个item指定应有的高度
            layoutParams.width = mWidth;//宽度为容器宽度
            layoutParams.top = top;//第一个item距上边5px
            top += mItemHeight;//往后每个item距上边+mItemHeight距离
        }
        /**
         * 由于此例特殊,宽度指定固定值,高度也是占满屏幕,不存在wrap_content情况
         * 故不需要根据子孩子的宽高来改动 mWidth 和 mHeight 的值。故最后直接保存初始计算的值
         */
        setMeasuredDimension(mWidth, mHeight);
    }

    /**
     * 根据计算的子孩子值,在容器中布局排版子孩子
     */
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        //获取子孩子个数
        int size = getChildCount();
        for (int i = 0; i < size; i++) {
            //得到特性顺序的子孩子
            TextView textView = (TextView) getChildAt(i);
            //拿到孩子中保存的数据
            RightIndexView.LayoutParams layoutParams = (LayoutParams) textView.getLayoutParams();
            //给子孩子布局位置(左,上,右,下)
            textView.layout(0, layoutParams.top, layoutParams.width, layoutParams.top + layoutParams.height);
        }
        /**
         * 结束了 onMeasure 和 onLayout 之后,当前容器的职责完成,onDraw 由子孩子自己画
         */
    }

    public void setData(ArrayList<String> list) {
        if (list == null || list.size() <= 0)
            return;
        int size = list.size();
        this.list.addAll(list);
        for (int i = 0; i < size; i++) {
            addView(list.get(i), i);
        }
        //requestLayout();//重新measure和layout
        //invalidate();//重新draw
        //postInvalidate();//重新draw
    }

    private void addView(String firstPinYin, int position) {
        TextView textView = new TextView(mContext);
        textView.setText(firstPinYin);
        textView.setBackgroundColor(Color.TRANSPARENT);
        textView.setTextColor(itemTextColor);
        textView.setTextSize(itemTextSize);
        textView.setGravity(Gravity.CENTER);
        textView.setTag(position);
        addView(textView, position);
    }

    
    /**
     * 必须重写的方法
     *
     * @return
     */
    @Override
    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
        return new RightIndexView.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
    }

    @Override
    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new RightIndexView.LayoutParams(p);
    }

    @Override
    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new RightIndexView.LayoutParams(getContext(), attrs);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof RightIndexView.LayoutParams;
    }

    public static class LayoutParams extends ViewGroup.LayoutParams {
        public int left;
        public int top;

        public LayoutParams(Context c, AttributeSet attrs) {
            super(c, attrs);
        }

        public LayoutParams(int width, int height) {
            super(width, height);
        }

        public LayoutParams(ViewGroup.LayoutParams source) {
            super(source);
        }
    }
}

1、上面代码中的onMeasure方法,是测量孩子们需要的总宽高,在告诉我自己该给多少宽高,这种情况是针对在xml中你只给我宽高都为wrap-content属性时。如果你给我了定值,比喻100dp,或者match_parent参数时,那onMeasure方法就几乎不要处理了。

2、onLayout方法就是根据测量好的宽高范围,来摆放这些孩子们。

上面2点也是viewgroup的核心代码,后续文章详解。

有了这些右边的效果几乎没啥问题了。

然而还有剩下的重点:

1、按下右边索引的某处时,被按下的child有背景色和文本颜色,真个右侧容器也有背景色。

2、在右边按下然后滑动时,滑到的child会跟着变色,滑过的会复原。

3、滑动到的位子,会在屏幕中间的view中体现出来。

4、滑动到的位置处的字母索引,会定位recyclerview的位置。

这些就得在右侧自定义viewgroup的ontouchevent方法中处理了。略需了解事件分发机制。

@Override
    public boolean onTouchEvent(MotionEvent event) {
        //当前手指位置的y坐标
        int y = (int) event.getY();
        //根据当前的y计算当前所在child的索引位置
        int tempIndex = computeViewIndexByY(y);
        if (tempIndex != -1) {
            //两头我留了点距离,不等于-1就代表没出界
            switch (event.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    yDown = y;
                    drawTextView(mOldViewIndex, false);
                    drawTextView(tempIndex, true);
                    mOldViewIndex = tempIndex;
                    if (onRightTouchMoveListener != null) {
                        onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
                    }
                    //设置root touch bg
                    setBackgroundColor(rootTouchBgColor);
                    break;
                case MotionEvent.ACTION_MOVE:
                    yMove = y;
                    int distance = yDown - yMove;
                    if (Math.abs(distance) > mTouchSlop) {
                        //移动距离超出了一定范围
                        if (mOldViewIndex != tempIndex) {
                            //移动超出了当前元素
                            drawTextView(mOldViewIndex, false);
                            drawTextView(tempIndex, true);
                            mOldViewIndex = tempIndex;
                            setBackgroundColor(rootTouchBgColor);
                            if (onRightTouchMoveListener != null) {
                                onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), true);
                            }
                        }
                    }
                    break;
                case MotionEvent.ACTION_UP:
                    drawTextView(mOldViewIndex, false);
                    drawTextView(tempIndex, false);
                    mOldViewIndex = tempIndex;
                    setBackgroundColor(rootBgColor);
                    if (onRightTouchMoveListener != null) {
                        onRightTouchMoveListener.showTip(tempIndex, ((TextView) getChildAt(tempIndex)).getText().toString(), false);
                    }
                    break;
            }
        } else {
            //出界了,可能是上边或者下边出界,恢复上边的元素
            if (list != null && list.size() > 0) {
                drawTextView(mOldViewIndex, false);
                setBackgroundColor(rootBgColor);
                if (onRightTouchMoveListener != null) {
                    onRightTouchMoveListener.showTip(mOldViewIndex, ((TextView) getChildAt(mOldViewIndex)).getText().toString(), false);
                }
            }
        }
        return true;
    }

在自定义viewgroup中布局child时,你当然知道每个child的高度是多少,总高度减去两头的间距,再除以child个数即可。

那么在touch的时候你能拿到y值,在计算出child的索引。然后修改child的背景色等属性,拿到text再回调到main界面。

可以通过下面的代码,根据y值计算index:

/**
     * 依据y坐标、子孩子的高度和容器总高度计算当前textview的索引值
     */
    private int computeViewIndexByY(int y) {
        int returnValue;
        if (y < marginTop || y > (marginTop + mItemHeight * list.size())) {
            returnValue = -1;
        } else {
            int times = (y - marginTop) / mItemHeight;
            int remainder = (y - marginTop) % mItemHeight;
            if (remainder == 0) {
                returnValue = --times;
            } else {
                returnValue = times;
            }
        }
        return returnValue;
    }

在activity这边设置回调

//右侧字母索引容器注册touch回调
rightContainer.setOnRightTouchMoveListener(this);

在回调里面做你想做的吧,显示中间的view,回显带回来的text,定位recyclerview索引位置:

/**
     * 右侧字母表touch回调
     *
     * @param position 当前touch的位置
     * @param content  当前位置的内容
     * @param isShow   显示与隐藏中间的tip view
     */
    @Override
    public void showTip(int position, final String content, boolean isShow) {
        if (isShow) {
            tipView.setVisibility(View.VISIBLE);
            tipView.setText(content);
        } else {
            tipView.setVisibility(View.INVISIBLE);
        }
        for (int i = 0; i < list.size(); i++) {
            if (list.get(i).firstPinYin.equals(content)) {
                recyclerView.stopScroll();
                int firstItem = layoutManager.findFirstVisibleItemPosition();
                int lastItem = layoutManager.findLastVisibleItemPosition();
                if (i <= firstItem) {
                    recyclerView.scrollToPosition(i);
                } else if (i <= lastItem) {
                    int top = recyclerView.getChildAt(i - firstItem).getTop();
                    recyclerView.scrollBy(0, top);
                } else {
                    recyclerView.scrollToPosition(i);
                }
                break;
            }
        }
    }

此处有一bug,我已修复。就是下面这个方法:

recyclerView.scrollToPosition(i);
他的问题在于:如果当前的i位置已经出现在屏幕内了,但是不是在头部,再调用此方法时无效。除非你需要定位的position不在可见范围之内。我这用scrollBy处理了。

剩下最后一个问题就是固定头了。其实就是在recyclerview的上层放了一个header view:

main.xml部分代码:

<!-- 列表,占满屏幕 -->
    <android.support.v7.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:overScrollMode="never"
        android:scrollbars="none" />

    <!-- 固定头item,浮在recyclerview上层的顶部 -->
    <include layout="@layout/header" />

header.xml代码:

<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/tv_header"
    android:layout_width="match_parent"
    android:layout_height="40dp"
    android:background="#FF9933"
    android:gravity="center_vertical"
    android:paddingLeft="10dp"
    android:text="A"
    android:textColor="@android:color/white"
    android:textSize="18sp" />

在滑动的时候根据item的header位置来让上层的header发生平移和赋值即可,当某item向上滑动时,header顶到上层的header,即让上层的header上移,顶多少就移动多少。

当item的header完全占据了上层的header位置时就让上层的header复位同时赋值。遇到下一个时重复上面的操作。没什么可神秘的。

依据分析得知需要在recyclerview的onscroll回调里面处理了:

recyclerView.addOnScrollListener(new OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                /**
                 * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px
                 * recyclerview上层的header所在的位置
                 */
                View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);

                /**
                 * recyclerview中如果有item占据了这个位置,那么header的text就为item的text
                 * 很显然,这个tiem是recyclerview的任意item
                 * 也就是说,recyclerview每滑过一个item,tvHeader就被赋了一次值
                 */
                if (itemView != null && itemView.getContentDescription() != null) {
                    tvHeader.setText(String.valueOf(itemView.getContentDescription()));
                }

                /**
                 * 指定可能印象外层header位置的item范围[-tvHeader.getMeasuredHeight()+1, tvHeader.getMeasuredHeight() + 1]
                 * 得到这个item
                 */
                View transInfoView = recyclerView.findChildViewUnder(
                        tvHeader.getMeasuredWidth() >> 1, tvHeader.getMeasuredHeight() + 1);

                if (transInfoView != null && transInfoView.getTag() != null) {
                    int transViewStatus = (int) transInfoView.getTag();
                    int dealtY = transInfoView.getTop() - tvHeader.getMeasuredHeight();
                    if (transViewStatus == ContactAdapter.SHOW_HEADER_VIEW) {
                        /**
                         * 如果这个item有tag参数,而且是显示header的,正好是我们需要关注的item的header部分
                         */
                        if (transInfoView.getTop() > 0) {
                            //说明item还在屏幕内,只是占据了外层header部分空间
                            tvHeader.setTranslationY(dealtY);
                        } else {
                            //说明item已经超出了recyclerview上边界,故此时外层的header的位置固定不变
                            tvHeader.setTranslationY(0);
                        }
                    } else if (transViewStatus == ContactAdapter.DISMISS_HEADER_VIEW) {
                        //如果此项的header隐藏了,即与外层的header无关,外层的header位置不变
                        tvHeader.setTranslationY(0);
                    }
                }
            }
        });

里面有一个方法需要了解,否则不好做:

/**
                 * 查找(width>>1,1)点处的view,差不多是屏幕最上边,距顶部1px
                 * recyclerview上层的header所在的位置
                 */
                View itemView = recyclerView.findChildViewUnder(tvHeader.getMeasuredWidth() >> 1, 1);

根据window中某一个指定的点,来查找recyclerview中的item,看谁在这个点上面。


到此分析完了,核心思想记这些。剩下的就靠你依据我的分析和源码去理解和尝试了。

demo地址:http://download.csdn.net/detail/fesdgasdgasdg/9607705




作者:fesdgasdgasdg 发表于2016/8/20 15:57:39 原文链接
阅读:65 评论:1 查看评论

友盟集成系列之用友盟SDK解决产品经理,营销经理的各种需求

$
0
0

在上一篇文章中我们已经完成对友盟SDK的基本集成,但是要满足产品经理对一个APP的期望,基本集成的功能远远是不够的,此时我们需要更进一步的集成友盟SDK的功能

1.账号统计功能

友盟在统计用户时以设备为标准,如果需要统计应用自身的账号(),请使用以下接口:

public static void onProfileSignIn(String ID) ;
public static void onProfileSignIn(String Provider, String ID) ;
  • ID:用户账号ID,长度小于64字节
  • Provider:账号来源。如果用户通过第三方账号登陆,可以调用此接口进行统计。支持自定义,不能以下划线"_"开头,使用大写字母和数字标识,长度小于32 字节; 如果是上市公司,建议使用股票代码。
public static void onProfileSignOff();
  • 账号登出时需调用此接口,调用之后不再发送账号相关内容。

示例:

当用户使用自有账号登录时,可以这样统计:

MobclickAgent.onProfileSignIn("userID");

当用户使用第三方账号(如新浪微博)登录时,可以这样统计:

MobclickAgent.onProfileSignIn("WB""userID");

集成账号统计功能后,请到,选择启动使用账号统计报表,如下图

image

如果您集成了新版SDK的账号接口,可以通过启动账号统计开关,来开启账号报表的展现。账号统计报表开关仅作用于报表的展现,不影响数据的计算。您可以根据实际的需要来启动或者暂停该功能。可在下图所示位置查看

image

image

2.页面统计功能,产品经理往往需要你统统APP中哪个页面打开最频繁

页面统计不需要再添加其他代码。

统计应用中包含Fragment的情况比较复杂,首先要明确一些概念。

1.  和 方法是用来统计应用时长的(也就是Session时长,当然还包括一些其他功能)

2.方法是用来统计页面跳转的

在仅有Activity的应用中,SDK 自动帮助开发者调用了  中的方法,并把Activity 类名作为页面名称统计。但是在包含fragment的程序中我们希望 统计更详细的页面,所以需要自己调用方法做更详细的统计。

首先,需要在程序入口处,调用  禁止默认的页面统计方式,这样将不会再自动统计 Activity。

然后需要手动添加以下代码:

1. 使用  和 方法统计时长, 这和基本统计中的情况一样(针对Activity)

2. 使用  和  方法统计页面(针对页面,页面可能是Activity 也可能是Fragment或View)

如果页面是直接由Activity实现的,统计代码大约是这样:

public void  {
    super.onResume();
    MobclickAgent.; //统计页面(仅有Activity的应用中SDK自动调用,不需要单独写。"SplashScreen"为页面名称,可自定义)
    MobclickAgent.onResume(this);          //统计时长
}
public void  {
    super.onPause();
    MobclickAgent.; // (仅有Activity的应用中SDK自动调用,不需要单独写)保证 onPageEnd 在onPause 之前调用,因为 onPause 中会保存信息。"SplashScreen"为页面名称,可自定义
    MobclickAgent.onPause(this);
}

如果页面是使用FragmentActivity + Fragment实现的,需要在 FragmentActivity 中统计时长:

public void  {
    super.onResume();
    MobclickAgent.onResume(this);       //统计时长
}
public void  {
    super.onPause();
    MobclickAgent.onPause(this);
}

并在其包含的 Fragment 中统计页面:

public void onResume() {
    super.onResume();
    MobclickAgent.; //统计页面,"MainScreen"为页面名称,可自定义
}
public void onPause() {
    super.onPause();
    MobclickAgent.; 
}

注意:这些方法的调用,需要,每个 onResume 都对应一个 onPause ,每个 Start 都有一个 End 配对。这样才能保证每个页面统计的正确

3.在企业开发中我们APP的奔溃日记一般要求加密上传,防止别人抓包

您可以通过在程序入口处的  中调用如下代码来设置加密模式

/** 设置是否对日志信息进行加密, 默认false(不加密). */
AnalyticsConfig.enableEncrypt(boolean enable);//6.0.0版本以前
MobclickAgent.enableEncrypt(boolean enable);//6.0.0版本及以后
  • 如果enable为,SDK会对日志进行加密。加密模式可以有效防止网络攻击,提高数据安全性。
  • 如果enable为,SDK将按照非加密的方式来传输日志。
  • 如果您没有设置加密模式,SDK的加密模式为false(不加密)。

4.在企业开发中产品经理经常会装逼的跟你说我们能不能办到知道那个商品购买最多,这个时候你心里一个万马奔腾,你直接去后台查不就行了,你和我说个毛啊。产品经理又会和你说我们能不再在这个页面埋下一个点,听到这些话作为小白的是不是已经奔溃了,其实产品经理说的这些都可以用友盟来解决,解决办法就是友盟的自定义统计事件

(1)自定义字段说明

:自定义事件id

:自定义事件下的参数

:自定义事件参数下的参数值

(2)自定事件的依赖条件
  1. 使用自定义事件功能请先登陆友盟官网 , 在 “” (子账户由于权限限制可能无法看到“设置”选项,请联系主帐号开通权限。)页面中添加相应的事件id(事件id可用英文或数字,),然后服务器才会对相应的事件请求进行处理。
  2. 自定义事件的代码需要放在Activity里的,请在友盟初始化之后调用事件,不支持在service中统计。
(3)自定义事件的功能实现
1.统计发生次数

在您希望跟踪的代码部分,调用如下方法:

MobclickAgent.onEvent(Context context, String eventId);

指当前的Activity

为当前统计的事件ID。

示例:统计微博应用中"转发"事件发生的次数,那么在转发的函数里调用

MobclickAgent.onEvent(mContext,"Forward");
2.统计点击行为各种属性的触发

考虑事件在不同属性上的取值,可以调用如下方法:

MobclickAgent.onEvent(Context context, String eventId, HashMap map);

为当前事件的属性和取值(Key-Value键值对)。

示例:统计电商应用中“购买”事件发生的次数,以及购买的商品类型及数量,那么在购买的函数里调用:

HashMap<String,String> map = new HashMap<String,String>();
map.put(,);
map.put(,); 
MobclickAgent.onEvent(mContext, "purchase", map);

5.社交统计

针对社交行为的垂直统计,可以非常详尽地统计应用中发生的各种社交行为。 只需要调用一行代码,便可享用到丰富的社交行为和社交用户分析报表。

在发生社交行为,比如分享了到新浪微博之后,调用这样一行代码:

UMPlatformData platform = new UMPlatformData(UMedia.SINA_WEIBO,); 
platform.setGender(GENDER.MALE); //optional   
platform.setWeiboId();  //optional   
MobclickAgent.onSocialEvent(this, platform);

会把分享信息发送到友盟服务器, 我们会通过这些信息创建社交行为报表。

相关参数说明: UMPlatformData:
平台枚举类型(必填)
用户的id(必填)
 微博id 
用户姓名 
 用户性别



作者:LUFANZHENG 发表于2016/8/20 16:11:14 原文链接
阅读:17 评论:0 查看评论

自定义UICollectionView布局

$
0
0

一直以来想学习怎么样去自定义UICollectionViewLayout,但总是感觉太难,一直以来,都是看了一点点就放弃了。但其实任何事,只要去做了,就会发现,其实远没有想像的那么难。所以以后我遇事也要多动手。

废话说在前面

我之前尝试过去写这样一个关卡选择的功能,但是总是写不出来,后来同事用一个UIScrollView简单的写了一个,但是效果完全不给力,不但动画很生硬,而且没有复用机制。当关卡一多的时候,就会有很明显的卡顿,所以被我否决了。后来我想到了利用第三方库iCarousel来实现,但是并没有我所需要的效果,虽然可以自定义,但老实说我确实没有那种研究精神,而且我发现iCarousel与xib的结合使用似乎不太好。但出于简单省时的目的,我还是简单的修改了iCarousel的代码得到我需要的效果,但是同事却发现了其他的问题,于是最终还是决定自己实现一个,当然要实现这样的功能,当然是通过自定义UICollectionViewLayout来实现。本来我是在网上找资料,但是不知道是本性还是怎么,看一点就不想看下去了。我感觉没有一篇详细说明每个步骤的博文,所以决定把自己的实现过程纪录下来,供和我有同样需求的朋友参考。

效果展示

上面废话有点多,还是直接一点,上效果图吧。
效果展示
效果虽然很简单,但基本也能概括自定义UICollectionViewLayout的必要步骤吧。

CustomCarCollectionViewFlowLayout类的定义

CustomCarCollectionViewFlowLayout其实如果继承自UICollectionViewFlowLayout会很简单的实现该效果,但是我之所以让其继承自UICollectionViewLayout的原因主要是有两点:1、我自己想利用这次机会好好学习一下自定义。2、继承自UICollectionViewFlowLayout很多方面都不好控制,而继承自UICollectionViewLayout完全自由,定义如下:

@interface CustomCardCollectionViewFlowLayout : UICollectionViewLayout

@property(nonatomic, assign) CGFloat internalItemSpacing;
@property(nonatomic, assign) CGSize itemSize;
@property(nonatomic, assign) UIEdgeInsets sectionEdgeInsets;
@property(nonatomic, assign) CGFloat scale;
@property(nonatomic, assign) NSInteger currentItemIndex;
@property(nonatomic, assign) id<CustomCardCollectionViewFlowLayoutDelegate> delegate;

@end

类说明

属性说明

internalItemSpacing

@property(nonatomic, assign) CGFloat internalItemSpacing;

这个属性其实是参考了UICollectionViewFlowLayout里面的minimumInterItemSpacing,该属性表示每个Cell之间的间隔,不过UICollectionViewFlowLayout里的是指最小的,可变的。而internalItemSpacing则不可变

itemSize

@property(nonatomic, assign) CGSize itemSize;

这个也是参考的UICollectionViewFlowLayout,该属性表示每个Cell的大小

sectionEdgeInsets

@property(nonatomic, assign) UIEdgeInsets sectionEdgeInsets;

还是参考的UICollectionViewFlowLayout,该属性表示每个section之间的间距

scale

@property(nonatomic, assign) CGFloat scale;

即表示左边或右边的Cell的缩放系数,当Cell走到最左边或最右边的时候将会被缩放成指定的大小。

currentItemIndex

@property(nonatomic, assign) NSInteger currentItemIndex;

表示当前在中央的Cell在UICollectionView中的索引,只有当Cell处于最中间的时候才会设置。

代理定义

CustomCardCollectionViewFlowLayoutDelegate的定义如下:

@class CustomCardCollectionViewFlowLayout;
@protocol CustomCardCollectionViewFlowLayoutDelegate <NSObject>

@optional
-(void)scrolledToTheCurrentItemAtIndex:(NSInteger)itemIndex;

@end

该代理中的方法,就是当UICollectionView滚动停止后,当前所在中间的Cell的索引,参考效果图上的关卡指示(1/16)。

代码说明

下面我将一步步按照自己的编写顺序来说明该功能的实现。

prepareLayout

prepareLayout是一个必须要实现的方法,该方法的功能是为布局提供一些必要的初始化参数,我的代码如下:

-(void)prepareLayout {
    [super prepareLayout];

    _itemsCount = [self.collectionView numberOfItemsInSection:0];

    if(_internalItemSpacing == 0)
        _internalItemSpacing = 5;

    if(_sectionEdgeInsets.top == 0 && _sectionEdgeInsets.bottom == 0 && _sectionEdgeInsets.left == 0 && _sectionEdgeInsets.right == 0)
        _sectionEdgeInsets = UIEdgeInsetsMake(0, ([UIScreen mainScreen].bounds.size.width - self.itemSize.width) / 2, 0, ([UIScreen mainScreen].bounds.size.width - self.itemSize.width) / 2);

//    UITapGestureRecognizer* tapGesture = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(collectionViewTapped:)];
//    [tapGesture setDelegate:self];
//    [self.collectionView addGestureRecognizer:tapGesture];

    return ;
}

首先是获取collectionView中共有多少个Cell,因为该功能一般只有一个section,所以我直接获取了section 0的数量。

其次是为该效果设置一些默认参数,如果用户没有提供值的话,将使用这些默认值。。

在最后有一个注释的UITapGestureRecognizer,这个手势本来是用来实现,点击两边的Cell能自动将点击的Cell滚动到中央。但是最后发现和UICollectionView的点击事件冲突了,导致滑动起来很吃力,到目前为止我还没想到更好的解决办法,于是只能暂时注释,慢慢想办法解决。也不妨将该手势的执行方法说一说。

手势处理

虽然手势不能使用,但还是可以拿来讲一讲,装装逼。其中有两段代码,一段是我处理手势冲突写的,但似乎效果不理想,最终还是没启用。

-(void)collectionViewTapped:(UIGestureRecognizer*)recognizer {
    CGPoint location = [recognizer locationInView:self.collectionView];
    NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:location];

    if(indexPath == nil)
        return ;

    if(_currentItemIndex == indexPath.item) {
        if([self.collectionView.delegate respondsToSelector:@selector(collectionView:didSelectItemAtIndexPath:)])
            [self.collectionView.delegate collectionView:self.collectionView didSelectItemAtIndexPath:indexPath];
    }
    else {
        _currentItemIndex = indexPath.item;
        [self.collectionView setContentOffset:CGPointMake(indexPath.item * (_internalItemSpacing + _itemSize.width), 0) animated:YES];

        if([self.delegate respondsToSelector:@selector(scrolledToTheCurrentItemAtIndex:)])
            [self.delegate scrolledToTheCurrentItemAtIndex:_currentItemIndex];
    }

    return ;
}

该方法是先获取到点击点在collectionView中的坐标,然后对应到点击的Cell的indexPath,当点击的Cell位于中间的话,则调用原来collectionView的didSelectItemAtIndexPath:方法,否则,调用setContentOffset方法,设置将点击的Cell设置到中间位置。

还有一个是处理手势冲突的,如下:

-(BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch {
    CGPoint location = [touch locationInView:self.collectionView];
    NSIndexPath* indexPath = [self.collectionView indexPathForItemAtPoint:location];

    if(indexPath == nil || indexPath.item == _currentItemIndex)
        return NO;
    return YES;
}

该方法是UIGestureRecognizerDelegate里的代理方法,同样的先获取到点击的indexPath,如果没有点击在Cell上或者点击了中间项,就不响应点击手势。但是效果并不理想,目前项目紧张也暂时不去考虑这么多了。

collectionViewContentSize

顾名思义,该方法也是一个必写的方法,该方法返回了collectionView的contentSize,我的代码如下:

-(CGSize)collectionViewContentSize {
    CGFloat contentWidth = _sectionEdgeInsets.left + _sectionEdgeInsets.right + _itemsCount * _itemSize.width + (_itemsCount - 1) * _internalItemSpacing;
    CGFloat contentHeight = _sectionEdgeInsets.top + _sectionEdgeInsets.bottom + self.collectionView.frame.size.height;
    return CGSizeMake(contentWidth, contentHeight);
}

那么collectionView的contentSize应该是多少呢?根据代码,我们知道height可以设置为0,因为需要纵向滚动,横向呢?
contentSize
从图中我们可以很明显的看出来,宽度应该是 『左边间距 + Cell数 * Cell宽度 + (Cell数 - 1) * Cell间距 + 右边间距』。

layoutAttributesForItemAtIndexPath:方法

该方法也是一个必须要实现的方法,该方法是为每个Cell返回一个对应的Attributes,我们需要在该Attributes中设置对应的属性,如Frame等,代码如下:

-(UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath {
    UICollectionViewLayoutAttributes* attr = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attr.size = _itemSize;
    attr.frame = CGRectMake((int)indexPath.row * (_itemSize.width + _internalItemSpacing) + _sectionEdgeInsets.left, (self.collectionView.bounds.size.height - _itemSize.height) / 2 + _sectionEdgeInsets.top, attr.size.width, attr.size.height);

    return attr;
}

首先调用”layoutAttributesForCellWithIndexPath:类方法创建一个Attributes,然后设置对应cell的frame,最后再返回该Attributes。

layoutAttributesForElementsInRect:

该方法是为在一个rect中的Cell返回Attributes,我们必须在该方法中做相应的处理,才能实现相应的效果。代码如下:

-(NSArray<UICollectionViewLayoutAttributes *> *)layoutAttributesForElementsInRect:(CGRect)rect {
    NSMutableArray* attributes = [NSMutableArray array];

    CGRect visiableRect = CGRectMake(self.collectionView.contentOffset.x, 0, self.collectionView.bounds.size.width, self.collectionView.bounds.size.height);
    CGFloat centerX = self.collectionView.contentOffset.x + [UIScreen mainScreen].bounds.size.width / 2;

    for (NSInteger i=0 ; i < _itemsCount; i++) {
        NSIndexPath* indexPath = [NSIndexPath indexPathForItem:i inSection:0];
        UICollectionViewLayoutAttributes* attr = [self layoutAttributesForItemAtIndexPath:indexPath];
        [attributes addObject:attr];

        if(CGRectIntersectsRect(attr.frame, visiableRect) == false)
            continue ;
        CGFloat xOffset = fabs(attr.center.x - centerX);

        CGFloat scale = 1 - (xOffset * (1 - _scale)) / (([UIScreen mainScreen].bounds.size.width + self.itemSize.width) / 2 - self.internalItemSpacing);
        attr.transform = CGAffineTransformMakeScale(scale, scale);
    }

    return attributes;
}

首先,调用我们实现的layoutAttributesForItemAtIndexPath:方法,为每个Cell设置一个Attributes,然后遍历Attributes集,如果Cell没有和当前返回的rect相交,那么我们不用去处理,因为反正我们也看不到。否则设置scale,至于scale的计算,数学能力强的很容易写出来,我搞了好久,因为从小到大,数学就TM菜的一逼。最后设置transform进行缩放。

shouldInvalidateLayoutForBoundsChange:

该方法中,需要返回YES,当滚动的时候,重新生成对应属性

-(BOOL)shouldInvalidateLayoutForBoundsChange:(CGRect)newBounds {
    return YES;
}

targetContentOffsetForProposedContentOffset:withScrollingVelocity:

该方法的作用是当UICollectionView停止滚动时,用户希望停止在哪个位置上,对于该方法,我的代码如下所示:

-(CGPoint)targetContentOffsetForProposedContentOffset:(CGPoint)proposedContentOffset withScrollingVelocity:(CGPoint)velocity {

    NSInteger itemIndex = (NSInteger)(self.collectionView.contentOffset.x / (_itemSize.width + _internalItemSpacing));
    CGFloat xOffset = itemIndex * (_internalItemSpacing + _itemSize.width);
    CGFloat xOffset_1 = (itemIndex + 1) * (_internalItemSpacing + _itemSize.width);

    if(fabs(proposedContentOffset.x - xOffset) > fabs(xOffset_1 - proposedContentOffset.x)) {
        _currentItemIndex = itemIndex + 1;
        if([self.delegate respondsToSelector:@selector(scrolledToTheCurrentItemAtIndex:)])
            [self.delegate scrolledToTheCurrentItemAtIndex:_currentItemIndex];
        return CGPointMake(xOffset_1, 0);
    }

    _currentItemIndex = itemIndex;
    if([self.delegate respondsToSelector:@selector(scrolledToTheCurrentItemAtIndex:)])
        [self.delegate scrolledToTheCurrentItemAtIndex:_currentItemIndex];
    return CGPointMake(xOffset, 0);
}

首先,我根据偏移量计算出对应的当前Cell的index,然后分别获取到当前Cell和下一个Cell的偏移量,然后判断屏幕中央隔哪边比较近,就将哪一个调整到中间。最后修改中央Cell的index,调用结束之后的代理方法。

使用方法

至此,自定义UICollectionViewLayout就已经全部结束,其使用方法也和UICollectionViewFlowLayout差不多,我的使用代码如下:

((CustomCardCollectionViewFlowLayout*)self.m_pCollectionView.collectionViewLayout).itemSize = CGSizeMake(UI_IOS_WINDOW_WIDTH - 80, UI_IOS_WINDOW_HEIGHT - 64 - 40 - 105);

((CustomCardCollectionViewFlowLayout*)self.m_pCollectionView.collectionViewLayout).scale = 0.85f;

((CustomCardCollectionViewFlowLayout*)self.m_pCollectionView.collectionViewLayout).delegate = self;

关于UICollectionView的部分是在xib中设置的,没有相关代码,因为很多默认值都是根据我的项目需要来设置的,所以我这里只设置了itemSize和scale缩放系数这两个参数。效果就如上图所示。。

结束语

其实自定义一个布局真的不算太难,最难的点在于数学模型的建立,但是只要有决心,相信自己,用心钻研,也一定能搞定难题。虽然我不是这种人,但是原本我觉得很难,但这次无可奈何的情况下,只能硬着头皮去做,结果发现是我自己把事情想复杂了。所以最后再来一句:不要想,就是干。

代码链接

github代码链接

作者:cairo123 发表于2016/8/20 16:18:16 原文链接
阅读:23 评论:0 查看评论

Media Data之多媒体扫描过程分析(二)

$
0
0

此分析代码基于Android 6.0,转载请注明来源地址http://blog.csdn.net/lemon_blue/article/details/52262023

Media Data之多媒体扫描过程分析(一)
Media Data之多媒体扫描过程分析(三)

2.1.5 android_media_MediaScanner.cpp

对于android_media_MediaScanner.cpp来说,主要分析三个函数native_init,native_setup和processDirectory。

static void
android_media_MediaScanner_native_init(JNIEnv *env)
{
    ALOGV("native_init");
    jclass clazz = env->FindClass(kClassMediaScanner);
    if (clazz == NULL) {
        return;
    }
    //将之后创建的native对象的指针保存到MediaScanner.java的mNativeContext字段中
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
}

android_media_MediaScanner_native_init的功能主要是动态注册。

static void
android_media_MediaScanner_native_setup(JNIEnv *env, jobject thiz)
{
    //获取Stagefright的MediaScanner对象
    MediaScanner *mp = new StagefrightMediaScanner;
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "Out of memory");
        return;
    }
    //将对象保存到mNativeContext中
    env->SetLongField(thiz, fields.context, (jlong)mp);
}

android_media_MediaScanner_native_setup方法的作用是创建native的MediaScanner对象,并且用的是StagefrightMediaScanner,等会分析。

static void
android_media_MediaScanner_processDirectory(
        JNIEnv *env, jobject thiz, jstring path, jobject client)
{
    //传入的参数path是需要扫描的路径,client是MediaScannerClient.java对象
    //获取之前保存到mNativeContext的StagefrightMediaScanner对象
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    if (mp == NULL) {
        jniThrowException(env, kRunTimeException, "No scanner available");
        return;
    }
    if (path == NULL) {
        jniThrowException(env, kIllegalArgumentException, NULL);
        return;
    }
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }
    //构造native层的MyMediaScannerClient对象,参数是java层的MyMediaScannerClient     
    //对象
    MyMediaScannerClient myClient(env, client);
    //调用native层processDirectory方法,参数是扫描路径和native的MyMediaScannerClient 
    //对象
    MediaScanResult result = mp->processDirectory(pathStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning directory '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
}

android_media_MediaScanner_processDirectory方法的作用是启动native层processDirectory扫描方法,在配置过程稍显复杂,其一是java的MediaScanner的上下文环境传递给native额MediaScanner对象中,其二是native的MyMediaScannerClient对象与java的MyMediaScannerClient对象建立联系,方便将结果回调到java层。

2.1.6 MediaScanner.cpp

下面分析的是native层的相关处理,StagefrightMediaScanner.cpp继承自MediaScanner.cpp,在JNI调用的方法processDirectory也是由父类实现的。
先分析MediaScanner.cpp父类的方法。

MediaScanResult MediaScanner::processDirectory(
        const char *path, MediaScannerClient &client) {
    //前期的一些准备工作
    int pathLength = strlen(path);
    if (pathLength >= PATH_MAX) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    char* pathBuffer = (char *)malloc(PATH_MAX + 1);
    if (!pathBuffer) {
        return MEDIA_SCAN_RESULT_ERROR;
    }
    int pathRemaining = PATH_MAX - pathLength;
    strcpy(pathBuffer, path);
    if (pathLength > 0 && pathBuffer[pathLength - 1] != '/') {
        pathBuffer[pathLength] = '/';
        pathBuffer[pathLength + 1] = 0;
        --pathRemaining;
    }
    //设置native的MyMediaScannerClient对象的local信息
    client.setLocale(locale());
    //执行doProcessDirectory方法
    MediaScanResult result = doProcessDirectory(pathBuffer, pathRemaining, client, false);
    //释放资源
    free(pathBuffer);
    return result;
}

MediaScanResult MediaScanner::doProcessDirectory(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia) {
    // place to copy file or directory name
    char* fileSpot = path + strlen(path);
    struct dirent* entry;
    if (shouldSkipDirectory(path)) {
        ALOGD("Skipping: %s", path);
        return MEDIA_SCAN_RESULT_OK;
    }
    // Treat all files as non-media in directories that contain a  ".nomedia" file
    if (pathRemaining >= 8 /* strlen(".nomedia") */ ) {
        strcpy(fileSpot, ".nomedia");
        if (access(path, F_OK) == 0) {
            ALOGV("found .nomedia, setting noMedia flag");
            noMedia = true;
        }
        // restore path
        fileSpot[0] = 0;
    }
    //打开对应的文件夹路径
    DIR* dir = opendir(path);
    if (!dir) {
        ALOGW("Error opening directory '%s', skipping: %s.", path, strerror(errno));
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    MediaScanResult result = MEDIA_SCAN_RESULT_OK;
    //循环遍历所有文件
    while ((entry = readdir(dir))) {
        //调用doProcessDirectoryEntry方法
        if (doProcessDirectoryEntry(path, pathRemaining, client, noMedia, entry, fileSpot)
                == MEDIA_SCAN_RESULT_ERROR) {
            result = MEDIA_SCAN_RESULT_ERROR;
            break;
        }
    }
    //关闭文件夹
    closedir(dir);
    return result;
}

MediaScanResult MediaScanner::doProcessDirectoryEntry(
        char *path, int pathRemaining, MediaScannerClient &client, bool noMedia,
        struct dirent* entry, char* fileSpot) {
    struct stat statbuf;
    //枚举目录中的文件和子文件夹信息
    const char* name = entry->d_name;
    // ignore "." and ".."
    if (name[0] == '.' && (name[1] == 0 || (name[1] == '.' && name[2] == 0))) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    int nameLength = strlen(name);
    if (nameLength + 1 > pathRemaining) {
        // path too long!
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    strcpy(fileSpot, name);

    int type = entry->d_type;
    if (type == DT_UNKNOWN) {
        // If the type is unknown, stat() the file instead.
        // This is sometimes necessary when accessing NFS mounted filesystems, but
        // could be needed in other cases well.
        //执行stat方法,获取文件的所有属性,成功返回0失败返回-1
        if (stat(path, &statbuf) == 0) {
            if (S_ISREG(statbuf.st_mode)) {
                type = DT_REG;
            } else if (S_ISDIR(statbuf.st_mode)) {
                type = DT_DIR;
            }
        } else {
            ALOGD("stat() failed for %s: %s", path, strerror(errno) );
        }
    }
    if (type == DT_DIR) {
        bool childNoMedia = noMedia;
        // set noMedia flag on directories with a name that starts with '.'
        // for example, the Mac ".Trashes" directory
        if (name[0] == '.')
            childNoMedia = true;
        // report the directory to the client
        if (stat(path, &statbuf) == 0) {
            //调用MyMediaScannerClient的scanFile函数
            status_t status = client.scanFile(path, statbuf.st_mtime, 0,
                    true /*isDirectory*/, childNoMedia);
            if (status) {
                //返回值是checkAndClearExceptionFromCallback,如果是true就出错
                return MEDIA_SCAN_RESULT_ERROR;
            }
        }
        // and now process its contents
        strcat(fileSpot, "/");
        MediaScanResult result = doProcessDirectory(path, pathRemaining - nameLength - 1,
                client, childNoMedia);
        if (result == MEDIA_SCAN_RESULT_ERROR) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    } else if (type == DT_REG) {
        stat(path, &statbuf);
        status_t status = client.scanFile(path, statbuf.st_mtime, statbuf.st_size,
                false /*isDirectory*/, noMedia);
        if (status) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

从上面的分析中看到调用到了MyMediaScannerClient的scanFile函数,下面分析这个函数

virtual status_t scanFile(const char* path, long long lastModified,
        long long fileSize, bool isDirectory, bool noMedia)
{
    jstring pathStr;
    if ((pathStr = mEnv->NewStringUTF(path)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
   //此处的mClient是java层的MyMediaScannerClient,调用的也是java层的scanFile方法
    mEnv->CallVoidMethod(mClient, mScanFileMethodID, pathStr, lastModified,
            fileSize, isDirectory, noMedia);
    mEnv->DeleteLocalRef(pathStr);
    return checkAndClearExceptionFromCallback(mEnv, "scanFile");
}

可以看出在native层的MyMediaScannerClient调用的是java层MyMediaScannerClient的scanFile函数,下面分析java层的逻辑。

public void scanFile(String path, long lastModified, long fileSize,
        boolean isDirectory, boolean noMedia) {
    // This is the callback funtion from native codes.
    //调用了doScanFile方法
    doScanFile(path, null, lastModified, fileSize, isDirectory, false, noMedia);
}
public Uri doScanFile(String path, String mimeType, long lastModified,
                long fileSize, boolean isDirectory, boolean scanAlways, boolean noMedia) {
            //参数scanAlways控制是否强制扫描
            Uri result = null;
            try {
                // beginFile方法的作用主要是1. 生成FileEntry,2.判断是否有修改文件
                FileEntry entry = beginFile(path, mimeType, lastModified,
                        fileSize, isDirectory, noMedia);
                // if this file was just inserted via mtp, set the rowid to zero
                // (even though it already exists in the database), to trigger
                // the correct code path for updating its entry
                if (mMtpObjectHandle != 0) {
                    entry.mRowId = 0;
                }
                // rescan for metadata if file was modified since last scan
                if (entry != null && (entry.mLastModifiedChanged || scanAlways)) {
                    if (noMedia) {
                        //不是media的情况
                        result = endFile(entry, false, false, false, false, false);
                    } else {
                        //重新扫描获取的信息
                        String lowpath = path.toLowerCase(Locale.ROOT);
                        boolean ringtones = (lowpath.indexOf(RINGTONES_DIR) > 0);
                        boolean notifications = (lowpath.indexOf(NOTIFICATIONS_DIR) > 0);
                        boolean alarms = (lowpath.indexOf(ALARMS_DIR) > 0);
                        boolean podcasts = (lowpath.indexOf(PODCAST_DIR) > 0);
                        boolean music = (lowpath.indexOf(MUSIC_DIR) > 0) ||
                            (!ringtones && !notifications && !alarms && !podcasts);
                        boolean isaudio = MediaFile.isAudioFileType(mFileType);
                        boolean isvideo = MediaFile.isVideoFileType(mFileType);
                        boolean isimage = MediaFile.isImageFileType(mFileType); 
                        if (isaudio || isvideo || isimage) {
                        //如过类型是音频、视频和图片的话,对路径进行处理
                        //If the given path exists on emulated external storage, 
                        //return the translated backing path hosted on internal storage.
                            path = Environment.maybeTranslateEmulatedPathToInternal
                                        (new File(path)).getAbsolutePath();
                        }
                        // we only extract metadata for audio and video files
                        if (isaudio || isvideo) {
                        //调用processFile方法,把MyMediaScannerClient作为参数传入
                        // processFile方法是native方法,稍后分析
                            processFile(path, mimeType, this);
                        }
                        if (isimage) {
                            //如果是图片,单独处理,调用processImageFile方法
                            //Decode a file path into a bitmap.
                            processImageFile(path);
                        }
                    // endFile方法是更新数据库
                    result = endFile(entry, ringtones, notifications, alarms, music, podcasts);
                    }
                }
            } catch (RemoteException e) {
                Log.e(TAG, "RemoteException in MediaScanner.scanFile()", e);
            }
            return result;
        }

从上面的分析可以看到,其实又调用到了processFile方法中,他也是一个native方法,需要再回到jni层继续分析此方法。

static void
android_media_MediaScanner_processFile(
        JNIEnv *env, jobject thiz, jstring path,
        jstring mimeType, jobject client)
{
    // Lock already hold by processDirectory
    //获取的还是native层的MediaScanner对象,实际类型是StagefrightMediaScanner对象
    MediaScanner *mp = getNativeScanner_l(env, thiz);
    const char *pathStr = env->GetStringUTFChars(path, NULL);
    if (pathStr == NULL) {  // Out of memory
        return;
    }
    //构造了新的native层的MyMediaScannerClient对象,传入的还是java层的MyMediaScannerClient对象
    MyMediaScannerClient myClient(env, client);
    //调用的是StagefrightMediaScanner对象的processFile方法,等会分析
    MediaScanResult result = mp->processFile(pathStr, mimeTypeStr, myClient);
    if (result == MEDIA_SCAN_RESULT_ERROR) {
        ALOGE("An error occurred while scanning file '%s'.", pathStr);
    }
    env->ReleaseStringUTFChars(path, pathStr);
    if (mimeType) {
        env->ReleaseStringUTFChars(mimeType, mimeTypeStr);
    }
}

从上面的分析可以看出,调用了StagefrightMediaScanner对象的processFile方法,下面分析此方法。

MediaScanResult StagefrightMediaScanner::processFile(
        const char *path, const char *mimeType,
        MediaScannerClient &client) {
    //调用native层的MyMediaScannerClient对象进行local信息,语言设置
    client.setLocale(locale());
    //beginFile方法是由MyMediaScannerClient的父类实现的,其实谷歌并没有实现此方法 
    client.beginFile();
    //具体的方法是调用processFileInternal实现的
    MediaScanResult result = processFileInternal(path, mimeType, client);
    //根据设置的区域信息来对字符串进行转换
    client.endFile();
    return result;
}

MediaScanResult StagefrightMediaScanner::processFileInternal(
        const char *path, const char * /* mimeType */,
        MediaScannerClient &client) {
    //获取扩展名信息
    const char *extension = strrchr(path, '.');
    if (!extension) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    //对扩展名不符合的跳过扫描
    if (!FileHasAcceptableExtension(extension)
        && !AVUtils::get()->isEnhancedExtension(extension)) {
        return MEDIA_SCAN_RESULT_SKIPPED;
    }
    // MediaMetadataRetriever将一个输入媒体文件中设置帧和元数据
    sp<MediaMetadataRetriever> mRetriever(new MediaMetadataRetriever);
    //打开资源
    int fd = open(path, O_RDONLY | O_LARGEFILE);
    status_t status;
    if (fd < 0) {
        // couldn't open it locally, maybe the media server can?
        //打开资源失败
        status = mRetriever->setDataSource(NULL /* httpService */, path);
    } else {
        //设置资源
        status = mRetriever->setDataSource(fd, 0, 0x7ffffffffffffffL);
        close(fd);
    }
    if (status) {
        return MEDIA_SCAN_RESULT_ERROR;
    }

    const char *value;
    if ((value = mRetriever->extractMetadata(
                    METADATA_KEY_MIMETYPE)) != NULL) {
        //设置类型
        status = client.setMimeType(value);
        if (status) {
            return MEDIA_SCAN_RESULT_ERROR;
        }
    }
    //构造元数据的tag
    struct KeyMap {
        const char *tag;
        int key;
    };
    static const KeyMap kKeyMap[] = {
        { "tracknumber", METADATA_KEY_CD_TRACK_NUMBER },
        { "discnumber", METADATA_KEY_DISC_NUMBER },
        { "album", METADATA_KEY_ALBUM },
        { "artist", METADATA_KEY_ARTIST },
        { "albumartist", METADATA_KEY_ALBUMARTIST },
        { "composer", METADATA_KEY_COMPOSER },
        { "genre", METADATA_KEY_GENRE },
        { "title", METADATA_KEY_TITLE },
        { "year", METADATA_KEY_YEAR },
        { "duration", METADATA_KEY_DURATION },
        { "writer", METADATA_KEY_WRITER },
        { "compilation", METADATA_KEY_COMPILATION },
        { "isdrm", METADATA_KEY_IS_DRM },
        { "width", METADATA_KEY_VIDEO_WIDTH },
        { "height", METADATA_KEY_VIDEO_HEIGHT },
    };
    static const size_t kNumEntries = sizeof(kKeyMap) / sizeof(kKeyMap[0]);
    //循环遍历
    for (size_t i = 0; i < kNumEntries; ++i) {
        const char *value;
        if ((value = mRetriever->extractMetadata(kKeyMap[i].key)) != NULL) {
            //设置tag和value到MyMediaScannerClient中,稍后分析
            status = client.addStringTag(kKeyMap[i].tag, value);
            if (status != OK) {
                return MEDIA_SCAN_RESULT_ERROR;
            }
        }
    }
    return MEDIA_SCAN_RESULT_OK;
}

从上面的分析中,设置tag和value是通过MyMediaScannerClient调用的,在MyMediaScannerClient的父类MediaScannerClient有addStringTag方法,在方法中又调用了子类MyMediaScannerClient的handleStringTag方法。

status_t MediaScannerClient::addStringTag(const char* name, const char* value)
{
    //调用子类的handleStringTag方法
    handleStringTag(name, value);
    return OK;
}
virtual status_t handleStringTag(const char* name, const char* value)
{
    jstring nameStr, valueStr;
    //获取字符串的值
    if ((nameStr = mEnv->NewStringUTF(name)) == NULL) {
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
    char *cleaned = NULL;
    //如果value的值不是utf-8编码,则需要特殊处理
    if (!isValidUtf8(value)) {
        cleaned = strdup(value);
        char *chp = cleaned;
        char ch;
        while ((ch = *chp)) {
            if (ch & 0x80) {
                *chp = '?';
            }
            chp++;
        }
        value = cleaned;
    }
    //将处理完成的值赋值到新的字符串valueStr中
    valueStr = mEnv->NewStringUTF(value);
    //释放资源
    free(cleaned);
    if (valueStr == NULL) {
        mEnv->DeleteLocalRef(nameStr);
        mEnv->ExceptionClear();
        return NO_MEMORY;
    }
    //调用java层MyMediaScanner的handleStringTag方法
    mEnv->CallVoidMethod(
        mClient, mHandleStringTagMethodID, nameStr, valueStr);
    mEnv->DeleteLocalRef(nameStr);
    mEnv->DeleteLocalRef(valueStr);
    return checkAndClearExceptionFromCallback(mEnv, "handleStringTag");
}

此时在native层中又去调用java层的方法了,此处调用的是handleStringTag方法。

public void handleStringTag(String name, String value) {
    if (name.equalsIgnoreCase("title") || name.startsWith("title;")) {
        // Don't trim() here, to preserve the special \001 character
        // used to force sorting. The media provider will trim() before
        // inserting the title in to the database.
        //将tag信息中的value值都赋值到了成员变量中
        mTitle = value;
    } else if (name.equalsIgnoreCase("artist") || name.startsWith("artist;")) {
        mArtist = value.trim();
    } else if (name.equalsIgnoreCase("albumartist") || name.startsWith("albumartist;")
            || name.equalsIgnoreCase("band") || name.startsWith("band;")) {
        mAlbumArtist = value.trim();
    ... ...
}

到此文件的读取过程分析完成了,这些成员变量装填完成之后就会调用到endFile方法中,进行更新数据库了。

2.2 IPC方式

由于发广播的方式无法实时地获取连接的状态,所以Android又提供了一种查询方法,就是通过IPC,也就是进程间通信的方式去启动扫描,然后获取扫描的状态。

2.2.1流程图

这里写图片描述

2.2.2MediaScannerConnection.java

/**
 * MediaScannerConnection provides a way for applications to pass a
 * newly created or downloaded media file to the media scanner service.
 * The media scanner service will read metadata from the file and add
 * the file to the media content provider.
 * The MediaScannerConnectionClient provides an interface for the
 * media scanner service to return the Uri for a newly scanned file
 * to the client of the MediaScannerConnection class.
 */

通过注释可以看出,MediaScannerConnection可以提供另一种非发广播的方式去主动扫描文件,他的调用过程是跨进程的,扫描的结果会通过回调函数获得。
在MediaScannerConnection内部提供了两种方式去供客户端使用,一种是实现接口和回调方法,另一种是使用代理模式所提供的静态方法。

(1)实现接口
首先通过构造方法新建实例,并且设置相关的成员变量。然后在客户端处调用connect方法,去绑定service,并且调用requestScanFile方法去跨进程调用MediaScannerService中的方法。当连接到MediaScannerService后回调客户端onMediaScannerConnected方法,当MediaScannerService扫描完成后,回调客户端onScanCompleted方法,整个过程完成。

//监听扫描完成的接口
public interface OnScanCompletedListener {
    public void onScanCompleted(String path, Uri uri);
}
//客户端需要实现的接口,同时也是在服务端所获取的客户端的实例
public interface MediaScannerConnectionClient extends OnScanCompletedListener {
    public void onMediaScannerConnected();
    public void onScanCompleted(String path, Uri uri);
}
//构造方法,传入的参数是客户端的上下文环境和客户端的实例
public MediaScannerConnection(Context context, MediaScannerConnectionClient client) {
    mContext = context;
    mClient = client;
}
// ServiceConnection的回调方法,当service连接时回调
public void onServiceConnected(ComponentName className, IBinder service) {
    synchronized (this) {
        //获取IMediaScannerService的实例mService
        mService = IMediaScannerService.Stub.asInterface(service);
        if (mService != null && mClient != null) {
            //当service连接上时,回调到客户端的onMediaScannerConnected方法
            mClient.onMediaScannerConnected();
        }
    }
}
// IMediaScannerListener是AIDL文件,只有一个方法scanCompleted
//这里获取了服务端IMediaScannerListener的实例
private final IMediaScannerListener.Stub mListener = new IMediaScannerListener.Stub() {
    public void scanCompleted(String path, Uri uri) {
        MediaScannerConnectionClient client = mClient;
        if (client != null) {
            //当回调到scanCompleted时,调用客户端的onScanCompleted方法
            client.onScanCompleted(path, uri);
        }
    }
};
//此方法是在客户端处调用,传入需要扫描的路径和文件类型
public void scanFile(String path, String mimeType) {
    synchronized (this) {
        if (mService == null || !mConnected) {
            throw new IllegalStateException("not connected to MediaScannerService");
        }
        try {
            //调用IMediaScannerService的方法
            mService.requestScanFile(path, mimeType, mListener);
        } catch (RemoteException e) {
        }
    }
}
//在客户端调用方法,bindService到MediaScannerService
public void connect() {
    synchronized (this) {
        if (!mConnected) {
            Intent intent = new Intent(IMediaScannerService.class.getName());
            intent.setComponent(
                    new ComponentName("com.android.providers.media",
                            "com.android.providers.media.MediaScannerService"));
            mContext.bindService(intent, this, Context.BIND_AUTO_CREATE);
            mConnected = true;
        }
    }
}

MediaScannerConnection部分分析完成,可以看出在connect方法中去绑定了远程的MediaScannerService,接下来分析在MediaScannerService完成的操作。

@Override
public IBinder onBind(Intent intent){
    return mBinder;
}

//在绑定之后获取到了服务端的实例,实现requestScanFile的具体方法
private final IMediaScannerService.Stub mBinder = 
        new IMediaScannerService.Stub() {
    //此处是requestScanFile实现的具体方法
    public void requestScanFile(String path, String mimeType, IMediaScannerListener listener){
        Bundle args = new Bundle();
        //将相关的参数都放入到了bundle中
        args.putString("filepath", path);
        args.putString("mimetype", mimeType);
        if (listener != null) {
            args.putIBinder("listener", listener.asBinder());
        }
        // 用startService的启动方式去启动,传入bundle
        startService(new Intent(MediaScannerService.this,
                MediaScannerService.class).putExtras(args));
    }
    //此处是scanFile实现的具体方法
    public void scanFile(String path, String mimeType) {
        requestScanFile(path, mimeType, null);
    }
};

//在onStartCommand方法中将intent的值发送到了ServiceHandler处理
private final class ServiceHandler extends Handler {
    @Override
    public void handleMessage(Message msg)
    {
        Bundle arguments = (Bundle) msg.obj;
        String filePath = arguments.getString("filepath");

        try {
            if (filePath != null) {
                //从intent中获取IBinder对象
                IBinder binder = arguments.getIBinder("listener");
                //获取IMediaScannerListener的实例
                IMediaScannerListener listener = (binder == null ? null :
                       IMediaScannerListener.Stub.asInterface(binder));
                Uri uri = null;
                try {
                    uri = scanFile(filePath, arguments.getString("mimetype"));
                } catch (Exception e) {
                    Log.e(TAG, "Exception scanning file", e);
                }
                if (listener != null) {
                    //查询完成后,回调到IMediaScannerListener,客户端处也随之回调
                    listener.scanCompleted(filePath, uri);
                }
            ... ...

在MediaScannerService的主要作用就是接受intent,调用scanFile方法扫描,扫描完成之后调用回调方法,给客户端回调。

(2) 静态方法实现
在MediaScannerConnection也可以通过提供的静态方法去实现扫描。其原理就是实现代理模式,远程代理客户端的实例进行相关操作,客户端只需要传入相应的参数即可,不需要手动连接service等操作,比较方便实用。

public static void scanFile(Context context, String[] paths, String[] mimeTypes,
        OnScanCompletedListener callback) {
    //实例化ClientProxy,并给构造函数传参
    ClientProxy client = new ClientProxy(paths, mimeTypes, callback);
    //实例化MediaScannerConnection,并给构造函数传参
    MediaScannerConnection connection = new MediaScannerConnection(context, client);
    client.mConnection = connection;
    //调用connect函数
    connection.connect();
}

//客户端的远程代理类
static class ClientProxy implements MediaScannerConnectionClient {
    final String[] mPaths;
    final String[] mMimeTypes;
    final OnScanCompletedListener mClient;
    MediaScannerConnection mConnection;
    int mNextPath;
    //构造函数,配置参数
    ClientProxy(String[] paths, String[] mimeTypes, OnScanCompletedListener client) {
        mPaths = paths;
        mMimeTypes = mimeTypes;
        mClient = client;
    }
    //实现回调方法
    public void onMediaScannerConnected() {
        scanNextPath();
    }
    public void onScanCompleted(String path, Uri uri) {
        if (mClient != null) {
            mClient.onScanCompleted(path, uri);
        }
        scanNextPath();
    }
    //因为传入的路径是数组,进行循环扫描
    void scanNextPath() {
        if (mNextPath >= mPaths.length) {
            mConnection.disconnect();
            return;
        }
        String mimeType = mMimeTypes != null ? mMimeTypes[mNextPath] : null;
        mConnection.scanFile(mPaths[mNextPath], mimeType);
        mNextPath++;
    }
}

所以对于客户端来说,实现此静态方法去扫描,只需要传入上下文,查询的路径(可以是多个路径,用数组表示),文件类型和监听器即可,不需要考虑其他,比较方便使用。

作者:lemon_blue 发表于2016/8/20 16:18:54 原文链接
阅读:49 评论:0 查看评论

Android 最全颜色对应的颜色表

$
0
0


 Android 最全颜色对应的颜色表 :









android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 

android 颜色对应的颜色表 - 李智勇 - 李智勇的博客 


Java代码  收藏代码
  1. <?xml version="1.0" encoding="utf-8" ?>  
  2. <resources>  
  3. <color name="white">#FFFFFF</color><!--白色 -->  
  4. <color name="ivory">#FFFFF0</color><!--象牙色 -->  
  5. <color name="lightyellow">#FFFFE0</color><!--亮黄色 -->  
  6. <color name="yellow">#FFFF00</color><!--黄色 -->  
  7. <color name="snow">#FFFAFA</color><!--雪白色 -->  
  8. <color name="floralwhite">#FFFAF0</color><!--花白色 -->  
  9. <color name="lemonchiffon">#FFFACD</color><!--柠檬绸色 -->  
  10. <color name="cornsilk">#FFF8DC</color><!--米绸色 -->  
  11. <color name="seashell">#FFF5EE</color><!--海贝色 -->  
  12. <color name="lavenderblush">#FFF0F5</color><!--淡紫红 -->  
  13. <color name="papayawhip">#FFEFD5</color><!--番木色 -->  
  14. <color name="blanchedalmond">#FFEBCD</color><!--白杏色 -->  
  15. <color name="mistyrose">#FFE4E1</color><!--浅玫瑰色 -->  
  16. <color name="bisque">#FFE4C4</color><!--桔黄色 -->  
  17. <color name="moccasin">#FFE4B5</color><!--鹿皮色 -->  
  18. <color name="navajowhite">#FFDEAD</color><!--纳瓦白 -->  
  19. <color name="peachpuff">#FFDAB9</color><!--桃色 -->  
  20. <color name="gold">#FFD700</color><!--金色 -->  
  21. <color name="pink">#FFC0CB</color><!--粉红色 -->  
  22. <color name="lightpink">#FFB6C1</color><!--亮粉红色 -->  
  23. <color name="orange">#FFA500</color><!--橙色 -->  
  24. <color name="lightsalmon">#FFA07A</color><!--亮肉色 -->  
  25. <color name="darkorange">#FF8C00</color><!--暗桔黄色 -->  
  26. <color name="coral">#FF7F50</color><!--珊瑚色 -->  
  27. <color name="hotpink">#FF69B4</color><!--热粉红色 -->  
  28. <color name="tomato">#FF6347</color><!--西红柿色 -->  
  29. <color name="orangered">#FF4500</color><!--红橙色 -->  
  30. <color name="deeppink">#FF1493</color><!--深粉红色 -->  
  31. <color name="fuchsia">#FF00FF</color><!--紫红色 -->  
  32. <color name="magenta">#FF00FF</color><!--红紫色 -->  
  33. <color name="red">#FF0000</color><!--红色 -->  
  34. <color name="oldlace">#FDF5E6</color><!--老花色 -->  
  35. <color name="lightgoldenrodyellow">#FAFAD2</color><!--亮金黄色 -->  
  36. <color name="linen">#FAF0E6</color><!--亚麻色 -->  
  37. <color name="antiquewhite">#FAEBD7</color><!--古董白 -->  
  38. <color name="salmon">#FA8072</color><!--鲜肉色 -->  
  39. <color name="ghostwhite">#F8F8FF</color><!--幽灵白 -->  
  40. <color name="mintcream">#F5FFFA</color><!--薄荷色 -->  
  41. <color name="whitesmoke">#F5F5F5</color><!--烟白色 -->  
  42. <color name="beige">#F5F5DC</color><!--米色 -->  
  43. <color name="wheat">#F5DEB3</color><!--浅黄色 -->  
  44. <color name="sandybrown">#F4A460</color><!--沙褐色 -->  
  45. <color name="azure">#F0FFFF</color><!--天蓝色 -->  
  46. <color name="honeydew">#F0FFF0</color><!--蜜色 -->  
  47. <color name="aliceblue">#F0F8FF</color><!--艾利斯兰 -->  
  48. <color name="khaki">#F0E68C</color><!--黄褐色 -->  
  49. <color name="lightcoral">#F08080</color><!--亮珊瑚色 -->  
  50. <color name="palegoldenrod">#EEE8AA</color><!--苍麒麟色 -->  
  51. <color name="violet">#EE82EE</color><!--紫罗兰色 -->  
  52. <color name="darksalmon">#E9967A</color><!--暗肉色 -->  
  53. <color name="lavender">#E6E6FA</color><!--淡紫色 -->  
  54. <color name="lightcyan">#E0FFFF</color><!--亮青色 -->  
  55. <color name="burlywood">#DEB887</color><!--实木色 -->  
  56. <color name="plum">#DDA0DD</color><!--洋李色 -->  
  57. <color name="gainsboro">#DCDCDC</color><!--淡灰色 -->  
  58. <color name="crimson">#DC143C</color><!--暗深红色 -->  
  59. <color name="palevioletred">#DB7093</color><!--苍紫罗兰色 -->  
  60. <color name="goldenrod">#DAA520</color><!--金麒麟色 -->  
  61. <color name="orchid">#DA70D6</color><!--淡紫色 -->  
  62. <color name="thistle">#D8BFD8</color><!--蓟色 -->  
  63. <color name="lightgray">#D3D3D3</color><!--亮灰色 -->  
  64. <color name="lightgrey">#D3D3D3</color><!--亮灰色 -->  
  65. <color name="tan">#D2B48C</color><!--茶色 -->  
  66. <color name="chocolate">#D2691E</color><!--巧可力色 -->  
  67. <color name="peru">#CD853F</color><!--秘鲁色 -->  
  68. <color name="indianred">#CD5C5C</color><!--印第安红 -->  
  69. <color name="mediumvioletred">#C71585</color><!--中紫罗兰色 -->  
  70. <color name="silver">#C0C0C0</color><!--银色 -->  
  71. <color name="darkkhaki">#BDB76B</color><!--暗黄褐色 -->  
  72. <color name="rosybrown">#BC8F8F</color><!--褐玫瑰红 -->  
  73. <color name="mediumorchid">#BA55D3</color><!--中粉紫色 -->  
  74. <color name="darkgoldenrod">#B8860B</color><!--暗金黄色 -->  
  75. <color name="firebrick">#B22222</color><!--火砖色 -->  
  76. <color name="powderblue">#B0E0E6</color><!--粉蓝色 -->  
  77. <color name="lightsteelblue">#B0C4DE</color><!--亮钢兰色 -->  
  78. <color name="paleturquoise">#AFEEEE</color><!--苍宝石绿 -->  
  79. <color name="greenyellow">#ADFF2F</color><!--黄绿色 -->  
  80. <color name="lightblue">#ADD8E6</color><!--亮蓝色 -->  
  81. <color name="darkgray">#A9A9A9</color><!--暗灰色 -->  
  82. <color name="darkgrey">#A9A9A9</color><!--暗灰色 -->  
  83. <color name="brown">#A52A2A</color><!--褐色 -->  
  84. <color name="sienna">#A0522D</color><!--赭色 -->  
  85. <color name="darkorchid">#9932CC</color><!--暗紫色 -->  
  86. <color name="palegreen">#98FB98</color><!--苍绿色 -->  
  87. <color name="darkviolet">#9400D3</color><!--暗紫罗兰色 -->  
  88. <color name="mediumpurple">#9370DB</color><!--中紫色 -->  
  89. <color name="lightgreen">#90EE90</color><!--亮绿色 -->  
  90. <color name="darkseagreen">#8FBC8F</color><!--暗海兰色 -->  
  91. <color name="saddlebrown">#8B4513</color><!--重褐色 -->  
  92. <color name="darkmagenta">#8B008B</color><!--暗洋红 -->  
  93. <color name="darkred">#8B0000</color><!--暗红色 -->  
  94. <color name="blueviolet">#8A2BE2</color><!--紫罗兰蓝色 -->  
  95. <color name="lightskyblue">#87CEFA</color><!--亮天蓝色 -->  
  96. <color name="skyblue">#87CEEB</color><!--天蓝色 -->  
  97. <color name="gray">#808080</color><!--灰色 -->  
  98. <color name="grey">#808080</color><!--灰色 -->  
  99. <color name="olive">#808000</color><!--橄榄色 -->  
  100. <color name="purple">#800080</color><!--紫色 -->  
  101. <color name="maroon">#800000</color><!--粟色 -->  
  102. <color name="aquamarine">#7FFFD4</color><!--碧绿色 -->  
  103. <color name="chartreuse">#7FFF00</color><!--黄绿色 -->  
  104. <color name="lawngreen">#7CFC00</color><!--草绿色 -->  
  105. <color name="mediumslateblue">#7B68EE</color><!--中暗蓝色 -->  
  106. <color name="lightslategray">#778899</color><!--亮蓝灰 -->  
  107. <color name="lightslategrey">#778899</color><!--亮蓝灰 -->  
  108. <color name="slategray">#708090</color><!--灰石色 -->  
  109. <color name="slategrey">#708090</color><!--灰石色 -->  
  110. <color name="olivedrab">#6B8E23</color><!--深绿褐色 -->  
  111. <color name="slateblue">#6A5ACD</color><!--石蓝色 -->  
  112. <color name="dimgray">#696969</color><!--暗灰色 -->  
  113. <color name="dimgrey">#696969</color><!--暗灰色 -->  
  114. <color name="mediumaquamarine">#66CDAA</color><!--中绿色 -->  
  115. <color name="cornflowerblue">#6495ED</color><!--菊兰色 -->  
  116. <color name="cadetblue">#5F9EA0</color><!--军兰色 -->  
  117. <color name="darkolivegreen">#556B2F</color><!--暗橄榄绿 -->  
  118. <color name="indigo">#4B0082</color><!--靛青色 -->  
  119. <color name="mediumturquoise">#48D1CC</color><!--中绿宝石 -->  
  120. <color name="darkslateblue">#483D8B</color><!--暗灰蓝色 -->  
  121. <color name="steelblue">#4682B4</color><!--钢兰色 -->  
  122. <color name="royalblue">#4169E1</color><!--皇家蓝 -->  
  123. <color name="turquoise">#40E0D0</color><!--青绿色 -->  
  124. <color name="mediumseagreen">#3CB371</color><!--中海蓝 -->  
  125. <color name="limegreen">#32CD32</color><!--橙绿色 -->  
  126. <color name="darkslategray">#2F4F4F</color><!--暗瓦灰色 -->  
  127. <color name="darkslategrey">#2F4F4F</color><!--暗瓦灰色 -->  
  128. <color name="seagreen">#2E8B57</color><!--海绿色 -->  
  129. <color name="forestgreen">#228B22</color><!--森林绿 -->  
  130. <color name="lightseagreen">#20B2AA</color><!--亮海蓝色 -->  
  131. <color name="dodgerblue">#1E90FF</color><!--闪兰色 -->  
  132. <color name="midnightblue">#191970</color><!--中灰兰色 -->  
  133. <color name="aqua">#00FFFF</color><!--浅绿色 -->  
  134. <color name="cyan">#00FFFF</color><!--青色 -->  
  135. <color name="springgreen">#00FF7F</color><!--春绿色 -->  
  136. <color name="lime">#00FF00</color><!--酸橙色 -->  
  137. <color name="mediumspringgreen">#00FA9A</color><!--中春绿色 -->  
  138. <color name="darkturquoise">#00CED1</color><!--暗宝石绿 -->  
  139. <color name="deepskyblue">#00BFFF</color><!--深天蓝色 -->  
  140. <color name="darkcyan">#008B8B</color><!--暗青色 -->  
  141. <color name="teal">#008080</color><!--水鸭色 -->  
  142. <color name="green">#008000</color><!--绿色 -->  
  143. <color name="darkgreen">#006400</color><!--暗绿色 -->  
  144. <color name="blue">#0000FF</color><!--蓝色 -->  
  145. <color name="mediumblue">#0000CD</color><!--中兰色 -->  
  146. <color name="darkblue">#00008B</color><!--暗蓝色 -->  
  147. <color name="navy">#000080</color><!--海军色 -->  
  148. <color name="black">#000000</color><!--黑色 -->  
  149. </resources>  

作者:shenggaofei 发表于2016/8/20 16:50:47 原文链接
阅读:41 评论:0 查看评论

SystemServer进程

$
0
0

背景
SystemServer进程是zygote进程启动后,主动“分裂”的第一个进程。
它负责启动大量的Android系统核心服务,其重要性不言而喻。一旦该进程崩溃,整个Android系统将重新启动。

版本
Android 6.0

一、启动SystemServer进程
在分析zygote进程时,我们知道当zygote进程进入到java世界后,在ZygoteInit.java中,将调用startSystemServer函数启动SystemServer进程,其关键代码是:

pid = Zygote.forkSystemServer(
    parsedArgs.uid, parsedArgs.gid,
    parsedArgs.gids,
    parsedArgs.debugFlags,
    null,
    parsedArgs.permittedCapabilities,
    parsedArgs.effectiveCapabilities);

其中,函数forkSystemServer函数定义于Zygote.java中。

public static int forkSystemServer(int uid, int gid, int[] gids, int debugFlags, int[][] rlimits, long permittedCapabilities, long effectiveCapabilities) {
    ..................
    int pid = nativeForkSystemServer(uid, gid, gids, debugFlags, rlimits, permittedCapabilities, effectiveCapabilities);
    ..................
}

容易看出,该函数通过调用native方法,完成实际的创建操作。
该Native方法定义于frameworks/base/core/jni/com_android_internal_os_Zygote.cpp中。
我们来看看对应的native函数。

static jint com_android_internal_os_Zygote_nativeForkSystemServer(
        JNIEnv* env, jclass, uid_t uid, gid_t gid, jintArray gids,
        jint debug_flags, jobjectArray rlimits, jlong permittedCapabilities,
        jlong effectiveCapabilities) {

  //进行实际的“分裂”工作
  pid_t pid = ForkAndSpecializeCommon(env, uid, gid, gids,
                                      debug_flags, rlimits,
                                      permittedCapabilities, effectiveCapabilities,
                                      MOUNT_EXTERNAL_DEFAULT, NULL, NULL, true, NULL,
                                      NULL, NULL);

  if (pid > 0) {
      //这里SystemServer进程已经创建出来,pid > 0 说明在父进程中
      //将子进程SystemServer的pid存在zygote进程的全局变量中
      gSystemServerPid = pid;

      int status;
      //小概率,SystemServer进程刚创建,就crash;此时需要重启zygote
      if (waitpid(pid, &status, WNOHANG) == pid) {
          ALOGE("System server process %d has died. Restarting Zygote!", pid);
          RuntimeAbort(env);
      }
  }
  return pid;
}

上述代码中,实际的“分裂”工作,由函数ForAndSpecializeCommon完成。

static pid_t ForkAndSpecializeCommon(......) {
  //注册信号监听器
  SetSigChldHandler();
  ..........
  pid_t pid = fork();
  if (pid == 0) {
      //根据传入参数进行对应的处理,例如设置进程名,设置各种id(用户id,组id)等
      ........
      //反注册掉信号监听器
      UnsetSigChldHandler();
      ......
  } else if () {
      .......
  }

  return pid;

从上面的代码可以看出,ForkAndSpecializeCommon最终是通过fork的方式,分裂出子进程。
这里需要关注一下的是,在zygote进程fork之前,调用SetSigChldHandler函数注册了一个子进程信号监听器。由于子进程共享父进程中的堆及栈信息,因此在子进程中也会有相应的信号处理器。
为了避免该信号监听器对子进程的影响,可以看到在子进程中进行了UnsetSigChldHandler的操作。

接下来,我们看看SetSigChldHandler进行了哪些操作。

static void SetSigChldHandler() {
  struct sigaction sa;
  memset(&sa, 0, sizeof(sa));
  sa.sa_handler = SigChldHandler;
  //该信号监听器关注子进程结束,对应的处理函数为SigChldHandler
  int err = sigaction(SIGCHLD, &sa, NULL);
  if (err < 0) {
    ALOGW("Error setting SIGCHLD handler: %s", strerror(errno));
  }
}

从上面的代码可以看出,SetSigChldHandler函数将注册一个信号处理器,来监听子进程的死亡。当子进程死亡后,利用SigChldHandler进行操作。需要注意的是,zygote的信号监听器,关注的是zygote所有的子进程,而不只是SystemServer进程(每次创建一个新的进程时,zygote都会注册对应的监听器)。

SigChldHandler中的重要代码如下所示:

static void SigChldHandler(int /*signal_number*/) {
    .......
    //监听的pid为-1,表示监听任何子进程结束
    while ((pid = waitpid(-1, &status, WNOHANG)) > 0) {
        //通过status判断子进程结束的原因,并打印相应的log
        ........
        //上文已经介绍过,gSystemServerPid中记录了SystemServer的pid
        if (pid == gSystemServerPid) {
            ALOGE("Exit zygote because system server (%d) has terminated", pid);
            //如果结束的子进程为SystemServer, zygote也将结束自己
            kill(getpid(), SIGKILL);
        }
    }
    .........
}

这里的问题是,所有zygote的子进程中,zygote只关心了SystemServer的死活。当其它子进程crash时,zygote只打印了log信息。

最后看看UnsetSigChldHandler函数:

// Sets the SIGCHLD handler back to default behavior in zygote children.
static void UnsetSigChldHandler() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));
    sa.sa_handler = SIG_DFL;

    int err = sigaction(SIGCHLD, &sa, NULL);
    if (err < 0) {
        ALOGW("Error unsetting SIGCHLD handler: %s", strerror(errno));
    }
}

zygote子进程的子进程crash后,应该还是zygote来处理,当然只是打印log信息。

二、SystemServer的主要工作
在分析zygote进程时,我们知道当ZygoteInit.java的startSystemServer函数,通过fork创建出SystemServer进程后,SystemServer进程调用handleSystemServerProcess函数,开始执行自己的工作。

........
if (pid == 0) {
    ........
    handleSystemServerProcess(parsedArgs);
}
........

接下来,我们来看看handleSystemServerProcess函数的主要内容。

private static void handleSystemServerProcess(ZygoteConnection.Arguments parsedArgs) 
throws ZygoteInit.MethodAndArgsCaller {
    //关闭从zygote进程那里继承下来server socket
    closeServerSocket();
    //设置SystemServer进程的一些属性
    ........
    //加载SystemServer对应的文件
    final String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH");
    if (systemServerClasspath != null) {
        performSystemServerDexOpt(systemServerClasspath);
    }

    if (parsedArgs.invokeWith != null) {
        ........
    } else {
        //利用systemServerClass对应的路径构建对应的ClassLoader
        ClassLoader cl = null;
        if (systemServerClasspath != null) {
            cl = new PathClassLoader(systemServerClasspath, ClassLoader.getSystemClassLoader());
            Thread.currentThread().setContextClassLoader(cl);
        }

        //将剩余参数及classLoader递交给RuntimeInit的zygoteInit函数
        RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, cl);
    }
}

从上面的代码可以看出,接下来的流程进入到RuntimeInit中的zygoteInit函数。zygoteInit函数将根据classLoader和参数,完成不同进程所需要的初始化工作(SystemServer进程与zygote的其它子进程均将使用zygoteInit函数)。

public static final void zygoteInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
    ..........
    commonInit();
    nativeZygoteInit();
    applicationInit(targetSdkVersion, argv, classLoader);
}

2.1 commonInit
commonInit主要进行一些常规初始化。由于自己是做通信的,所以比较关注的是创建UA(user agent):

private static final void commonInit() {
    .......
    /* Sets the default HTTP User-Agent used by HttpURLConnection.*/
    String userAgent = getDefaultUserAgent();
    System.setProperty("http.agent", userAgent);
    .........
}

User-Agent是Http协议中的一部分,属于头域的组成部分,是一种向访问网站提供你所使用的浏览器类型、操作系统、浏览器内核等信息的标识。通过这个标识,用户所访问的网站可以显示不同的排版,从而为用户提供更好的体验或者进行信息统计。

2.2 nativeZygoteInit
函数nativeZyoteInit实现在frameworks/base/core/jni/AndroidRuntime.cpp中,主要用于为Binder通信打下基础。

static void com_android_internal_os_RuntimeInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
    gCurRuntime->onZygoteInit();
}

这里需要关注的是,SystemServer进程中的gCurRuntime指的是什么呢?

实际上在zygote进程启动时,在app_main.cpp的main函数中,创建出了AppRuntime:

int main(int argc, char* const argv[])
{
    ........
    AppRuntime runtime(argv[0], computeArgBlockSize(argc, argv));
    ........

AppRuntime定义如下:

class AppRuntime : public AndroidRuntime
{
public:
    AppRuntime(char* argBlockStart, const size_t argBlockLength)
        : AndroidRuntime(argBlockStart, argBlockLength)
        , mClass(NULL)
    {
    }
    ...........
}

看看AppRuntime的父类AndroidRuntime:

AndroidRuntime::AndroidRuntime(char* argBlockStart, const size_t argBlockLength) :
        mExitWithoutCleanup(false),
        mArgBlockStart(argBlockStart),
        mArgBlockLength(argBlockLength)
{
    .................
    assert(gCurRuntime == NULL);        // one per process
    gCurRuntime = this;
}

从代码可以看出,AndroidRuntime初始化时定义了gCurRuntime。gCurRuntime指向对象自身,也就是说gCurRuntime指向的是AppRuntime对象。

由于SystemServer进程由zygote进程fork出来,于是system server进程中也存在gCurRuntime对象,类型为AppRuntime。至此我们知道,Native函数中gCurRuntime->onZygoteInit将调用AppRuntime中的onZygoteInit。

virtual void onZygoteInit()
{
    sp<ProcessState> proc = ProcessState::self();
    ALOGV("App process: starting thread pool.\n");
    proc->startThreadPool();
}

onZygoteInit的用途是启动一个线程,用于binder通信。这由将是一个沉重的话题,我们今后再分析。

2.3 applicationInit

private static void applicationInit(int targetSdkVersion, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
    //设置一些进程退出的处理策略,可用堆栈上限等
    .............
    invokeStaticMain(args.startClass, args.startArgs, classLoader);
}

我们来进一步看看invokeStaticMain函数的内容。

private static void invokeStaticMain(String className, String[] argv, ClassLoader classLoader) throws ZygoteInit.MethodAndArgsCaller {
    //className为进行初始化工作的进程类名
    //在SystemServer初始化时,为com.android.server.SystemServer
    Class<?> cl;

    //下面就是通过反射得到对应类的main方法
    try {
        cl = Class.forName(className, true, classLoader);
    } catch (ClassNotFoundException ex) {
        .......
    }

    Method m;
    try {
        m = cl.getMethod("main", new Class[] { String[].class });
    } catch (NoSuchMethodException ex) {
        .....
    } catch (SecurityException ex) {
        .......
    }

    int modifiers = m.getModifiers();
    if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
        ......
    }

    /*
     * This throw gets caught in ZygoteInit.main(), which responds
     * by invoking the exception's run() method. This arrangement
     * clears up all the stack frames that were required in setting
     * up the process.
     */
    throw new ZygoteInit.MethodAndArgsCaller(m, argv);
}

上述代码的最后抛出了一个异常。那么这个异常是在哪里捕获的呢?
实际上注释中已经给出了提示,在ZygoteInit.java的main函数中:

public static void main(String argv[]) {
    try {
        ........
        if (startSystemServer) {
            startSystemServer(abiList, socketName);
        }

        Log.i(TAG, "Accepting command socket connections");
        runSelectLoop(abiList);

        closeServerSocket();
    } catch (MethodAndArgsCaller caller) {
        //不论是startSystemServer拉起SystemServer进程
        //还是runSelectLoop收到请求,建立起进程
        //都会抛出MethodAndArgsCaller
        caller.run();
    } catch (RuntimeException ex) {
        Log.e(TAG, "Zygote died with exception", ex);
        closeServerSocket();
        throw ex;
    }
}

从上述代码,可以看出捕获MethodAndArgsCaller异常后,调用了MethodAndArgsCaller的run方法:

public void run() {
    try {
        mMethod.invoke(null, new Object[] { mArgs });
    } catch (IllegalAccessException ex) {
        throw new RuntimeException(ex);
    } catch (InvocationTargetException ex) {
        ......
    }
}

从上面的代码可以看到,run方法单纯地利用反射调用对应类的main方法(此处是SystemServer.java的main方法)。

这里的问题是,为什么不在RuntimeInit.java的invokeStaticMain中,直接利用反射调用每个类的main方法?

参考invokeStaticMain中抛出异常的注释,我们可以推测出,这与linux的exec函数族的意图相似。
注意到,我们此时运行在SystemServer进程中。由于zygote进程fork的原因,SystemServer调用到invokeStaticMain时,整个堆栈实际上包含了大量zygote进程复制过来的调用信息。此时,我们通过抛异常捕获的方式,让位于栈底的ZygoteInit.main函数来进行处理,可起到刷新整个调用栈的作用(旧的无用调用出栈)。

exec 函数族就提供了一个在进程中启动另一个程序执行的方法。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段,在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。

2.4 SystemServer.java main

接下来就进入了SystemServer.java的main函数:

public static void main(String[] args) {
    //创建并运行,简单粗暴!
    new SystemServer().run();
}

我们来看看run方法中,进行了哪些重要操作。

private void run() {
    //设置一些属性
    ........
    //确保主线程的优先级,并初始化SystemServer的looper。
    android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_FOREGROUND);
    android.os.Process.setCanSelfBackground(false);
    Looper.prepareMainLooper();

    //加载native层的库文件
    System.loadLibrary("android_servers");
    .........
    //创建出SystemServiceManager, 将用于创建和管理系统服务
    mSystemServiceManager = new SystemServiceManager(mSystemContext);
    LocalServices.addService(SystemServiceManager.class, mSystemServiceManager);

    try {
        // 分种类启动不同的system service
        startBootstrapServices();
        startCoreServices();
        startOtherServices();
    } catch (Throwable ex) {
        ........
    }
    ..........
    //启动looper,以处理到来的消息
    Looper.loop();
    throw new RuntimeException("Main thread loop unexpectedly exited");
}

三、其它进程的启动
最后这一部分,我们举例分析一下,其它的进程如何被zygote进程启动,与SystemServer进程做一个对比。

我们已经知道,zygote进程分裂出SystemServer进程后,就调用runSelectLoop函数等待并处理来自客户端的消息。
为了进一步理解这个过程,我们以启动Activity对应进程的过程举例。
在ActivityManagerService.java中,函数startProcessLocked中的关键代码如下所示:

..........
Process.ProcessStartResult startResult = Process.start(entryPoint,
                    app.processName, uid, uid, gids, debugFlags, mountExternal,
                    app.info.targetSdkVersion, app.info.seinfo, requiredAbi, instructionSet,
                    app.info.dataDir, entryPointArgs);
.........

接下来我们看看Process.java中的start函数:

public static final ProcessStartResult start(final String processClass,
                              final String niceName,
                              int uid, int gid, int[] gids,
                              int debugFlags, int mountExternal,
                              int targetSdkVersion,
                              String seInfo,
                              String abi,
                              String instructionSet,
                              String appDataDir,
                              String[] zygoteArgs) {
    try {
        //processClass的值是“android.app.ActivityThread”
        //函数名很清楚,via zygote
        return startViaZygote(processClass, niceName, uid, gid, gids,
                    debugFlags, mountExternal, targetSdkVersion, seInfo,
                    abi, instructionSet, appDataDir, zygoteArgs);
    } catch (ZygoteStartFailedEx ex) {
        .........
    }
 }

startViaZygote的主要工作如下面代码所示。

private static ProcessStartResult startViaZygote(....) {
    //准备参数,写入到argsForZygote
    ........
    return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi), argsForZygote);
}

其中,openZygoteSocketIfNeeded的代码如下:

private static ZygoteState openZygoteSocketIfNeeded(String abi) throws ZygoteStartFailedEx {
    if (primaryZygoteState == null || primaryZygoteState.isClosed()) {
        try {
            //当前还没有可用的与zygote通信的socket,则创建一个
            primaryZygoteState = ZygoteState.connect(ZYGOTE_SOCKET);
        } catch (IOException ioe) {
            throw new ZygoteStartFailedEx("Error connecting to primary zygote", ioe);
        }
    }

    if (primaryZygoteState.matches(abi)) {
        //如果已经有可用的socket,就使用该socket
        return primaryZygoteState;
    }
    .................
}

我们稍微来看一下ZygoteState的connect函数定义,这个socket通信写的非常标准:

 public static ZygoteState connect(String socketAddress) throws IOException {
    DataInputStream zygoteInputStream = null;
    BufferedWriter zygoteWriter = null;
    //创建本地进程的socket
    final LocalSocket zygoteSocket = new LocalSocket();

    try {
        //连接zygote进程中的server socket
        zygoteSocket.connect(new LocalSocketAddress(socketAddress, LocalSocketAddress.Namespace.RESERVED));

        zygoteInputStream = new DataInputStream(zygoteSocket.getInputStream());

        //利用zygoteWriter包装socket的outputStream
        zygoteWriter = new BufferedWriter(new OutputStreamWriter( zygoteSocket.getOutputStream()), 256);
    } catch (IOException ex) {
        try {
            zygoteSocket.close();
        } catch (IOException ignore) {
        }

        throw ex;
    }
    .........

    return new ZygoteState(zygoteSocket, zygoteInputStream, zygoteWriter, Arrays.asList(abiListString.split(",")));
}

socket连接建立成功后,我们回头再看看函数zygoteSendArgsAndGetResult:

private static ProcessStartResult zygoteSendArgsAndGetResult(ZygoteState zygoteState, ArrayList<String> args) throws ZygoteStartFailedEx {
    try {
        //其实就是获取的socket的outputStream和inputStream
        final BufferedWriter writer = zygoteState.writer;
        final DataInputStream inputStream = zygoteState.inputStream;

        writer.write(Integer.toString(args.size()));
        writer.newLine();

        int sz = args.size();
        for (int i = 0; i < sz; i++) {
            String arg = args.get(i);
            if (arg.indexOf('\n') >= 0) {
                 throw new ZygoteStartFailedEx("embedded newlines not allowed");
            }
            writer.write(arg);
            writer.newLine();
        }

        //利用socket将消息发往zygote
        writer.flush();

        ProcessStartResult result = new ProcessStartResult();
        //阻塞直到收到结果,pid大于0则说明进程启动成功
        result.pid = inputStream.readInt();
        if (result.pid < 0) {
            throw new ZygoteStartFailedEx("fork() failed");
        }
        result.usingWrapper = inputStream.readBoolean();
        return result;
    } catch (IOException ex) {
        .......
    } 
}

ActivityManagerService将请求发送给zygote进程后, 就轮到zygote进程处理消息了。通过分析zygote进程的启动流程,我们已经知道zygote进程收到请求后,将执行ZygoteConnection的runOnce函数。

private static void runSelectLoop(String abiList) throws MethodAndArgsCaller {
    ArrayList<FileDescriptor> fds = new ArrayList<FileDescriptor>();
    ArrayList<ZygoteConnection> peers = new ArrayList<ZygoteConnection>();
    ......
    for (int i = pollFds.length - 1; i >= 0; --i) {
        .........
        if (i == 0) {
            ZygoteConnection newPeer = acceptCommandPeer(abiList);
            peers.add(newPeer);
            fds.add(newPeer.getFileDesciptor());
        } else {
            //处理请求
            boolean done = peers.get(i).runOnce();
            if (done) {
                peers.remove(i);
                fds.remove(i);
            }
        }
    }
    ............
}

接下来,我们看看函数runOnce的实际操作:

boolean runOnce() throws ZygoteInit.MethodAndArgsCaller {
    //解析传入的参数
    ........
    try {
        .......
        //与启动SystemServer进程一样,最终也会通过native函数fork,并配置进程的参数
        pid = Zygote.forkAndSpecialize(parsedArgs.uid, parsedArgs.gid, parsedArgs.gids, parsedArgs.debugFlags, rlimits, parsedArgs.mountExternal, parsedArgs.seInfo, parsedArgs.niceName, fdsToClose, parsedArgs.instructionSet, parsedArgs.appDataDir);
    }  catch (ErrnoException ex) {
        .......   
    } catch (IllegalArgumentException ex) {
        ....... 
    } catch (ZygoteSecurityException ex) {
        ....... 
    }

    try {
        if (pid == 0) {
            // in child
            IoUtils.closeQuietly(serverPipeFd);
            serverPipeFd = null;
            //子进程根据参数利用handleChildProc作进一步处理
            handleChildProc(parsedArgs, descriptors, childPipeFd, newStderr);

            // should never get here, the child is expected to either
            // throw ZygoteInit.MethodAndArgsCaller or exec().
            return true;
        } else {
            // in parent...pid of < 0 means failure
            IoUtils.closeQuietly(childPipeFd);
            childPipeFd = null;
            //父进程进行一些后续操作,例如清理工作等
            return handleParentProc(pid, descriptors, serverPipeFd, parsedArgs);
        }
    } finally {
        IoUtils.closeQuietly(childPipeFd);
        IoUtils.closeQuietly(serverPipeFd);
    }
}

最后,我们看看handlehandleChildProc:

private void handleChildProc(Arguments parsedArgs, FileDescriptor[] descriptors, FileDescriptor pipeFd, PrintStream newStderr) throws ZygoteInit.MethodAndArgsCaller {
    //关闭fork过来的zygote server socket
    closeSocket();
    ZygoteInit.closeServerSocket();
    //处理参数
    ........
    if (parsedArgs.invokeWith != null) {
        .......
    } else {
        //完成进程的初始化,然后抛异常
        RuntimeInit.zygoteInit(parsedArgs.targetSdkVersion, parsedArgs.remainingArgs, null /* classLoader */);
    }
}

从上面的代码可以看出,函数handleChildProc最终还是会调用Runtime的zygoteInit。
如同SystemServer进程一样,普通进程也会进行一些初始化,建立binder通信后,抛出异常,最终由ZygoteInit.java捕获异常,然后反射启动对应类的main函数(实际上是android.app.ActivityThread类的main函数)。

结束语
以上就是对SystemServer进程的初步分析,通过对比普通进程,我们可以找到Android中加载进程的普遍流程。

作者:Gaugamela 发表于2016/8/20 17:20:54 原文链接
阅读:61 评论:0 查看评论

Android常用实例——实现修改用户头像功能

$
0
0

首先祝大家周末愉快!前几天发表了几个项目中常用的实例,读者反映不错,可以看出大家还是希望技术能够在项目得到实际的应用,那么这篇博客就来聊聊实现用户修改头像的功能。

现在的APP,无论是大型的APP还是小型的项目,都或多或少的跟修改头像相关联,这个功能可以说在哪儿都用的说,所以有必要掌握这门技术。

先说说这个功能所需要掌握的知识点:

1.对startActivityForResult()方法要有一定的掌握;

  1. 文件存储的掌握;

3.PopupWindow的使用。

如果对这些知识点都比较了解的话,那看这篇博客就毫无压力了。

我这里为了方便,单独写了一个PopupWindow,用于封装我们想要的功能和实现PopupWindow的显示。先看看代码:

public class MPoPuWindow extends PopupWindow implements OnClickListener {

    public Context mContext;

    private Type type;

    public Activity mActivity;

    private File file;
    private Uri ImgUri;

    private TextView mTakePhoto, mAlbumPhoto, mCancel;

    public MPoPuWindow(Context context, Activity mActivity) {
        initView(context);
        this.mActivity = mActivity;
    }

    private void initView(Context mContext) {
        this.mContext = mContext;
        View v = LayoutInflater.from(mContext).inflate(R.layout.activity_popu,
                null);
        setContentView(v);

        mTakePhoto = (TextView) v.findViewById(R.id.photo_take);
        mAlbumPhoto = (TextView) v.findViewById(R.id.photo_album);
        mCancel = (TextView) v.findViewById(R.id.photo_cancel);

        mTakePhoto.setOnClickListener(this);
        mAlbumPhoto.setOnClickListener(this);
        mCancel.setOnClickListener(this);

        // 设置SelectPicPopupWindow弹出窗体的宽
        this.setWidth(ViewGroup.LayoutParams.MATCH_PARENT);
        // 设置SelectPicPopupWindow弹出窗体的高
        this.setHeight(ScreenUtils.getScreenHeight(mContext));

        // 设置SelectPicPopupWindow弹出窗体可点�?
        this.setTouchable(true);
        this.setFocusable(true);
        this.setOutsideTouchable(true);

        // 刷新状�?
        this.update();
        // 设置SelectPicPopupWindow弹出窗体动画效果
        this.setAnimationStyle(R.style.popuwindow_from_bottom);
        // 实例化一个ColorDrawable颜色为半透明
        ColorDrawable dw = new ColorDrawable(0x50000000);
        // 设置SelectPicPopupWindow弹出窗体的背景
        this.setBackgroundDrawable(dw);
    }

    public void showPopupWindow(View parent) {

        if (!this.isShowing()) {
            this.showAtLocation(parent, Gravity.BOTTOM, 0, 0);
        } else {
            this.dismiss();

        }
    }

    @Override
    public void onClick(View arg0) {
        switch (arg0.getId()) {
        case R.id.photo_take:
            Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
            file = new File(Environment.getExternalStorageDirectory(),
                    System.currentTimeMillis() + ".jpg");
            ImgUri = Uri.fromFile(file);
            intent.putExtra(MediaStore.EXTRA_OUTPUT, ImgUri);
            mActivity.startActivityForResult(intent, 1);
            type = Type.CAMERA;
            if (listener != null) {
                listener.getType(type);
                listener.getImgUri(ImgUri, file);
            }

            this.dismiss();
            break;
        case R.id.photo_album:
            Intent intent2 = new Intent("android.intent.action.PICK");
            intent2.setType("image/*");
            mActivity.startActivityForResult(intent2, 2);
            type = Type.PHONE;
            if (listener != null) {
                listener.getType(type);
            }
            this.dismiss();
            break;
        case R.id.photo_cancel:
            this.dismiss();
            break;
        default:
            break;
        }
    }

    public void onPhoto(Uri uri, int outputX, int outputY) {
        Intent intent = null;

        intent = new Intent("com.android.camera.action.CROP");
        intent.setDataAndType(uri, "image/*");

        intent.putExtra("crop", "true");
        intent.putExtra("aspectX", 1);
        intent.putExtra("aspectY", 1);
        intent.putExtra("outputX", outputX);
        intent.putExtra("outputY", outputY);
        intent.putExtra("scale", true);
        intent.putExtra(MediaStore.EXTRA_OUTPUT, uri);
        intent.putExtra("return-data", true);
        intent.putExtra("circleCrop", true);
        intent.putExtra("outputFormat", Bitmap.CompressFormat.JPEG.toString());
        intent.putExtra("noFaceDetection", true); // no face detection
        mActivity.startActivityForResult(intent, 3);
    }

    public interface onGetTypeClckListener {
        void getType(Type type);

        void getImgUri(Uri ImgUri, File file);
    }

    private onGetTypeClckListener listener;

    public void setOnGetTypeClckListener(onGetTypeClckListener listener) {
        this.listener = listener;
    }

}

重点的内容我都加上了注释,所以就不再赘述,值得注意的是我这里为了能跟外面交互,我定义了一个接口,然后对外暴露出两个方法,传递我们选择图片的时候产生的file和类型。另外我这里还用到了样式:

<style name="popuwindow_from_bottom">
        <item name="android:windowEnterAnimation">@anim/popu_show_from_bottom</item>
        <item name="android:windowExitAnimation">@anim/popu_hiden_from_top</item>

    </style>

anim文件下的popu_hiden_from_top.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:toXDelta="0"
        android:fromXDelta="0"
        android:duration="300"
        android:fromYDelta="0"
        android:toYDelta="100%"

        />


</set>

还有一个popu_show_from_bottom.xml

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">

    <translate
        android:toXDelta="0"
        android:fromXDelta="0"
        android:duration="300"
        android:fromYDelta="100%"
        android:toYDelta="0"
        />


</set>

这个样式主要是给popuwindow添加一个弹出的样式,使它更好看一些。接下来就看看Activity怎么写的:

public class MainActivity extends Activity {
    private ImageView mIvThumb;

    private File file;

    private Uri ImgUri;

    private Type type;

    private MPoPuWindow puWindow;

    public enum Type {
        PHONE, CAMERA
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mIvThumb=(ImageView) findViewById(R.id.btn_change);

        mIvThumb.setOnClickListener(new OnClickListener() {

            @Override
            public void onClick(View v) {
                puWindow = new MPoPuWindow(MainActivity.this, MainActivity.this);
                puWindow.showPopupWindow(findViewById(R.id.set_act_parent));
                puWindow.setOnGetTypeClckListener(new onGetTypeClckListener() {

                    @Override
                    public void getType(Type type) {
                        MainActivity.this.type = type;
                    }

                    @Override
                    public void getImgUri(Uri ImgUri, File file) {
                        MainActivity.this.ImgUri = ImgUri;
                        MainActivity.this.file = file;
                    }

                });

            }
        });

    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        // TODO Auto-generated method stub
        super.onActivityResult(requestCode, resultCode, data);
        Log.e("requestCode", type + "");
        if (requestCode == 1) {
            if (ImgUri != null) {
                puWindow.onPhoto(ImgUri, 300, 300);
            }
        } else if (requestCode == 2) {
            if (data != null) {
                Uri uri = data.getData();
                puWindow.onPhoto(uri, 300, 300);
            }
        } else if (requestCode == 3) {
            if (type == Type.PHONE) {
                if (data != null) {
                    Bundle extras = data.getExtras();
                    Bitmap bitmap = (Bitmap) extras.get("data");
                    if (bitmap != null) {
                        mIvThumb.setImageBitmap(bitmap);
                    }
                }
            } else if (type == Type.CAMERA) {
                mIvThumb.setImageBitmap(BitmapFactory.decodeFile(file.getPath()));
            }
        }
    }

}

其他都好理解,重点看看onActivityResult()方法里面的内容,这里分别使用了三个判断语句,也就是说返回的时候有三种情况,从相机里面返回的、从相册里面返回的、从切图界面返回的,对应不同的返回进行不一样的操作。代码再popuwindow里面都封装好了。
最后看到用到的两个工具类,这两个工具类都是项目中常用到的,可以保存下来以后用

第一个:DensityUtils

import android.content.Context;
import android.util.TypedValue;

/**
 * 常用单位转换的辅助类
 * 
 * 
 * 
 */
public class DensityUtils {
    private DensityUtils() {
        /* cannot be instantiated */
        throw new UnsupportedOperationException("cannot be instantiated");
    }

    /**
     * dp转px
     * 
     * @param context
     * @param val
     * @return
     */
    public static int dp2px(Context context, float dpVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                dpVal, context.getResources().getDisplayMetrics());
    }

    /**
     * sp转px
     * 
     * @param context
     * @param val
     * @return
     */
    public static int sp2px(Context context, float spVal) {
        return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP,
                spVal, context.getResources().getDisplayMetrics());
    }

    /**
     * px转dp
     * 
     * @param context
     * @param pxVal
     * @return
     */
    public static float px2dp(Context context, float pxVal) {
        final float scale = context.getResources().getDisplayMetrics().density;
        return (pxVal / scale);
    }

    /**
     * px转sp
     * 
     * @param fontScale
     * @param pxVal
     * @return
     */
    public static float px2sp(Context context, float pxVal) {
        return (pxVal / context.getResources().getDisplayMetrics().scaledDensity);
    }

}

第二个:ScreenUtils

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.view.View;
import android.view.WindowManager;

/**
 * 获得屏幕相关的辅助类
 *
 *
 *
 */
public class ScreenUtils
{
    private ScreenUtils()
    {
        /* cannot be instantiated */
        throw new UnsupportedOperationException("cannot be instantiated");
    }

    /**
     * 获得屏幕宽度
     *
     * @param context
     * @return
     */
    public static int getScreenWidth(Context context)
    {
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return outMetrics.widthPixels;
    }

    /**
     * 获得屏幕高度
     *
     * @param context
     * @return
     */
    public static int getScreenHeight(Context context)
    {
        WindowManager wm = (WindowManager) context
                .getSystemService(Context.WINDOW_SERVICE);
        DisplayMetrics outMetrics = new DisplayMetrics();
        wm.getDefaultDisplay().getMetrics(outMetrics);
        return outMetrics.heightPixels;
    }

    /**
     * 获得状态栏的高度
     *
     * @param context
     * @return
     */
    public static int getStatusHeight(Context context)
    {

        int statusHeight = -1;
        try
        {
            Class<?> clazz = Class.forName("com.android.internal.R$dimen");
            Object object = clazz.newInstance();
            int height = Integer.parseInt(clazz.getField("status_bar_height")
                    .get(object).toString());
            statusHeight = context.getResources().getDimensionPixelSize(height);
        } catch (Exception e)
        {
            e.printStackTrace();
        }
        return statusHeight;
    }

    /**
     * 获取当前屏幕截图,包含状态栏
     *
     * @param activity
     * @return
     */
    public static Bitmap snapShotWithStatusBar(Activity activity)
    {
        View view = activity.getWindow().getDecorView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bmp = view.getDrawingCache();
        int width = getScreenWidth(activity);
        int height = getScreenHeight(activity);
        Bitmap bp = null;
        bp = Bitmap.createBitmap(bmp, 0, 0, width, height);
        view.destroyDrawingCache();
        return bp;

    }

    /**
     * 获取当前屏幕截图,不包含状态栏
     *
     * @param activity
     * @return
     */
    public static Bitmap snapShotWithoutStatusBar(Activity activity)
    {
        View view = activity.getWindow().getDecorView();
        view.setDrawingCacheEnabled(true);
        view.buildDrawingCache();
        Bitmap bmp = view.getDrawingCache();
        Rect frame = new Rect();
        activity.getWindow().getDecorView().getWindowVisibleDisplayFrame(frame);
        int statusBarHeight = frame.top;

        int width = getScreenWidth(activity);
        int height = getScreenHeight(activity);
        Bitmap bp = null;
        bp = Bitmap.createBitmap(bmp, 0, statusBarHeight, width, height
                - statusBarHeight);
        view.destroyDrawingCache();
        return bp;

    }

}

这些都写好了。这个功能也就可基本实现了。

作者:qq_25193681 发表于2016/8/20 18:17:15 原文链接
阅读:43 评论:0 查看评论

android_aidl_的使用

$
0
0

Android Aidl 的使用

Aidl 是android 跨进程通信的中一种,是一种RPC。底层基于binder 框架。通常用在C/S架构中。

Aidl 跨进程通信支持有限的数据类型

Aidl 可以进行跨进程通信,但是不是所有的数据类型都支持,支持的类型主要是:

  1. Java 的基本类型
  2. String 和CharSequence
  3. List 和 Map, 并且List和Map 对象的元素必须是AIDL支持的数据类型;以上三种类型都不需要导入(import)
  4. AIDL 自动生成的接口 需要导入(import)
  5. 实现android.os.Parcelable 接口的类. 需要导入(import)。

创建Aidl 文件

在android studio 中直接new 一个Aidl 文件 IHelloWorldInterface.aidl。注意文件命名规则,IXXX.adil。 只定义了一个printHelloWorld 接口。

interface IHelloWorldInterface {
    /**
     * Demonstrates some basic types that you can use as parameters
     * and return values in AIDL.
     */
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

    String printHelloWorld();
}

编译后生成 IHelloWorldInterface.java 文件。由于文件是aidl 工具生成的,格式比较乱,代码用工具格式化后IHelloWorldInterface 是一个接口,里面主要是两个类静态虚类public Stub类和private Stub.Proxy 类,Stub为存根的意思,那就是服务端使用,Stub.Proxy 为代理类,客户端使用。

public interface IHelloWorldInterface extends android.os.IInterface {
    public static abstract class Stub extends android.os.Binder implements com.example.louiewh.aidlapplication.IHelloWorldInterface {
        private static final java.lang.String DESCRIPTOR = "com.example.louiewh.aidlapplication.IHelloWorldInterface";

        private static class Proxy implements com.example.louiewh.aidlapplication.IHelloWorldInterface {
            private android.os.IBinder mRemote;

            @Override
            public java.lang.String printHelloWorld() throws android.os.RemoteException {

            }
        }
    }
}

服务端的实现

服务端 HelloWorldService 继承 IHelloWorldInterface.Stub 接口

public class HelloWorldService  extends IHelloWorldInterface.Stub {
    @Override
    public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

    }

    @Override
    public String printHelloWorld() throws android.os.RemoteException {
        if(mListerner != null) {
            final int begin = mListerner.beginBroadcast();
            for (int i = 0; i < begin; i++) {
                mListerner.getBroadcastItem(i).onAidlListerner(
                        new StringBuilder().
                        append("Pid:").append(android.os.Process.myPid()).
                        append(" Threadtime:").append(SystemClock.uptimeMillis()).
                        toString()
                );
            }
            mListerner.finishBroadcast();
        }
        return "Hello AIDL!";
    }
}

创建Service

创建一个AidlService extends Service。AndroidManifext 中注册Service。 在Service 的
onBind 中返回HelloWorldService。

public class AidlService extends Service {
    public final static String TAG = "AidlService";

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy");
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new HelloWorldService();
    }
}

代理端调用

service 启动

android Service 启动有两种方式,一种startService,一种binderService. 由于我们要获取Service 的代理端,使用binderService。在 MainActivity onCreate 中 binderService。

 Intent intent = new Intent(context, AidlService.class);
 context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);

获取Service Proxy

然后 New 一个ServiceConnection, 在ServiceConnection中的onServiceConnected回调函数中会通过IHelloWorldInterface.Stub.asInterface返回Stub.proxy 对象。这样我们就拿到了服务的代理端。在MainActivity中设置text.setOnClickListener,当点击时显示printHelloWorld 的结果,就是进程PID和uptimeMillis时间。

ServiceConnection helloWorldConn = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
            remoteService = IHelloWorldInterface.Stub.asInterface(iBinder);

         }

        @Override
        public void onServiceDisconnected(ComponentName componentName) {

        }
    };

text.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                try {
                    text.setText(remoteService.printHelloWorld());
                } catch (RemoteException e) {
                    e.printStackTrace();
                }
            }
 });

service 退出

在Activity退出时,需要unbinder

protected void onDestroy() {
     helloWorldProxy.unbindService();
     super.onDestroy();
}

一个Aidl 通信的架构就基本完完成了.

需要注意的是ServiceConnection是异步通信

设置listener

现在客户端可以调用服务端了,如果服务端需要通知客户端呢,就需要listener 出场了,listener 同样基于Aidl。定义一个onAidlListerner 回调。

1. 定义IAidlListernerInterface

interface IAidlListernerInterface {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

    void onAidlListerner(String str);
}

2. 定义一个抽象类 AidlListerner 继承IAidlListernerInterface.Stub。


public abstract class AidlListerner extends IAidlListernerInterface.Stub {
}

3. IHelloWorldInterface.aidl 中增加两个函数,注册listener 和 反注册listener。

 void registerListerner(IAidlListernerInterface listener);

 void unregisterListerner(IAidlListernerInterface listener);

4. HelloWorldService 中同样实现这两个函数。

同时有一个变量 ArrayList\

 public String printHelloWorld() throws RemoteException {
        if(mListerner != null) {
            for (int i = 0; i < mListerner.size(); i++) {
                mListerner.get(i).onAidlListerner(new StringBuilder().append("current time:").append(SystemClock.currentThreadTimeMillis()).toString());;
            }
        }
        return "Hello AIDL!";
    }

5. MainActivity 中 注册

这样在点击text 的时候 listenertext 会显示printHelloWorld 的结果。

remoteService.registerListener(new AidlListener() {
            @Override
            public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

            }

            @Override
            public void onAidlListerner(String str) throws RemoteException {
                listenerText.setText(str);
            }
        });        

onDestory 中 unregisterListener。

6. 多进程模式下RemoteCallbackList

现在Service 和Activity 运行在一个进程中,Service 和 Client 通常运行在不同的进程中,现在我们配置Service 运行在另外一个进程。 这时候需要注意的是在HelloWorldService 中保存listener 的变量mListerner 类型为ArrayList 修改为: RemoteCallbackList\ mListerner = new RemoteCallbackList\();
原因是因为垮进程的listener 的指针地址改变了。listener 的遍历方式也发生了变化。

 <service
     android:name=".AidlService"
     android:process=":AidlService">
 </service>  
if(mListerner != null) {
     final int begin = mListerner.beginBroadcast();
     for (int i = 0; i < begin; i++) {
         mListerner.getBroadcastItem(i).onAidlListerner(new StringBuilder().
                     append("Pid:").append(android.os.Process.myPid()).                      append("Current time:").append(SystemClock.currentThreadTimeMillis()).
                        toString()
        );
    }
    mListerner.finishBroadcast();
}

代理服务

在一个应用里可能有很多这样的Aidl 文件,通常的做法是每个Aidl 文件都都建一个Service 用来bind,这样一个APK 进程中有很多Service 存在。是不是我们用一个Service 就可以了,因为都是在后台运行,这样就大大减小了对内存的消耗。在讲到Aidl 传输数据类型的时候Aidl 本身也支持Aidl自动生成的Interface 类型的传输,而我们定义的业务Service,比如HelloWorldService 本身就是继承Aidl 自动生成的Interface 类型。所以可以有一个专业的Service 来传递业务Service。 通常在获取系统服务的时候是 getApplicationContext().getSystemService(“XXX”), 同样也定义一个这样的API,来为应用内的调用提供APK 级别的Service 服务。

定义 IAidlBinderService.aidl

这个Aidl 文件之定义一个接口getService.

interface IAidlBinderService {
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,
            double aDouble, String aString);

    IBinder getService(String service);
}

所有的业务Service 都通过getService 获取。

AidlBinderService 的服务端

onBind 的时候返回AidlBinderService 的Binder。
通过getService 传递进来的参数返回不同的Service 服务。这里一共两种业务Service,HelloWorldService HelloAidlService。这两种Service 单独一个java 文件,和Service 代码分离,当加入一个新的Service的时候,通常定义Aidl 文件,Service 代码继承 Stub 接口。然后定义getService 的字符串就可以加入一个新的Service。

public class AidlService extends Service {

    public final static String HELLOWORLDSERVICE = "HelloWorldService";
    public final static String HELLOAIDLSERVICE = "HelloAidlService";
    public final static String TAG = "AidlService";

    @Override
    public void onCreate() {
        super.onCreate();
    }

    @Override
    public void onDestroy() {
        Log.d(TAG, "onDestroy");
        super.onDestroy();
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return new AidlBinderService();
    }

    class AidlBinderService extends IAidlBinderService.Stub{

        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {

        }

        @Override
        public IBinder getService(String service) throws RemoteException {
            switch(service) {
                case HELLOWORLDSERVICE:
                    return new HelloWorldService();
                case HELLOAIDLSERVICE:
                    return new HelloAidlService();
                default:
                    return null;
            }
        }
    }
}

AidlBinderService 的代理。

在处理AidlBinderService 的代理的时候,通常是在 ServiceConnection 回调中得到 iBinder。 这是一个异步的通信。我们希望代码能够耦合度更低,和业务能够分开,把AidlBinderService 单独的实现子啊一个java 文件中。一个做法是使用异步转同步的方法,直到ServiceConnection 的 onConnected 返回。但是这样毕竟有系统时间的消耗,而Service 的binder通常在APK 启动时,影响启动速度。这里继续沿用异步回到,两种方法,设置回调函数,使用Handler。AidlBinderServiceProxy 为单例模式,构造函数中去bindService,在onServiceConnected 使用传递进来的Handler 发送binderService 成功的消息。

获取AidlBinderService 的代理

public class AidlBinderServiceProxy {
    static final String TAG = "AidlBinderServiceProxy";
    static final int AidlBinderService = 1;
    private Context mContext;
    private ServiceConnection mConnection;
    private IAidlBinderService mRemoteService;
    private Handler mHandler;
    private static AidlBinderServiceProxy instance;

    private AidlBinderServiceProxy(Context context, Handler handler) {
        mContext = context;
        mHandler = handler;
        initServiceConnection();
        Intent intent = new Intent(context, AidlService.class);
        context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }

    public static AidlBinderServiceProxy instance(Context context, Handler handler){

        synchronized (AidlBinderServiceProxy.class) {
            if(instance == null) {
                instance = new AidlBinderServiceProxy(context, handler);
            }
        }

        return instance;
    }

    public void unbindService() {

        Log.d(TAG, "unbindService");
        mContext.unbindService(mConnection);
        instance = null;
    }

    private void initServiceConnection() {
        mConnection = new ServiceConnection() {
            @Override
            public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
                mRemoteService = IAidlBinderService.Stub.asInterface(iBinder);
                Message message = Message.obtain(mHandler, AidlBinderService);
                mHandler.sendMessage(message);
                Log.d(TAG, "onServiceConnected");
            }

            @Override
            public void onServiceDisconnected(ComponentName componentName) {

            }
        };
    }

    public IBinder getService(String service) {
        if(mRemoteService == null) {
        }

        try {
            return mRemoteService.getService(service);
        } catch (RemoteException e) {
            Log.e(TAG, "getService " + "service");
            e.printStackTrace();
        }

        return null;
    }
}

实现业务Service 的Proxy

实现两个业务Service 的Proxy。HelloAidlProxy 如下,在构造函数中通过getService 获取到IBinder 对象,通过I HelloAidlInterface.Stub.asInterface 函数转为代理对象。

public class HelloAidlProxy {
    static final String TAG = "HelloAidlProxy";
    public AidlBinderServiceProxy  mAidlBinderService;
    private IHelloAidlInterface mRemoteService;


    public HelloAidlProxy(AidlBinderServiceProxy proxy) {

        mAidlBinderService = proxy;
        IBinder binder = mAidlBinderService.getService(AidlService.HELLOAIDLSERVICE);

        mRemoteService = IHelloAidlInterface.Stub.asInterface(binder);
    }

    public PidInfo getPidInfo() {
        if(mRemoteService != null){
            try {
                return mRemoteService.getPidInfo();
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        return null;
    }
}

调用

修改MainActivity 的实现, 实现一个内部Handler 类:

class AidlHandler extends Handler{
    public void handleMessage(Message msg) {

        switch(msg.what) {
            case AidlBinderServiceProxy.AidlBinderService:
                Log.d("louie", "AidlBinderService");
                helloWorldProxy = new HelloWorldProxy(mAidlBinderService);
            default:
                break;
            }
        });            

使用代理服务前后App 的结构变化:

Aidl Service
https://www.processon.com/view/link/57a5991be4b02c28bf471316

Parcelable

上面已经实现了Aidl 生成的类型的数据跨进程传递,Aidl 生成的数据类型主要用于C/S 这样的架构。对于普通的对象如何处理呢,android 给我们准备了Parcelable 这种数据类型。和java 的Serializable 比较类似,Serializable 序列化基于文本,Parcelable 基于binder,效率更高。Parcelable 基于android提供的Parcel类型,将数据写入Parcel打包,需要的时候再从Parcel 读出完成序列化。

定义类继承 Parcelable

类 PidInfo implements Parcelable

  • 重写writeToParcel方法,将对象序列化为一个Parcel对象,即:将类的数据写入外部提供的Parcel中
  • 重写describeContents方法,内容接口描述,默认返回0就可以
  • 实例化静态内部对象CREATOR实现接口Parcelable.Creator,在 createFromParcel new 了一个参数为 Parcel 构造函数,需要注意的是:
    这个构造函数实现的时候的读写顺序要和writeToParcel 方法一致
public class PidInfo implements Parcelable {
    private int mPid;

    PidInfo(int pid ){
        mPid = pid;
    }

    PidInfo(Parcel in ) {
        mPid = in.readInt();
    }

    public int getPid(){
        return mPid;
    }

    public void setPid(int pid ){
        mPid = pid;
    }

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
         dest.writeInt(mPid);
    }

    public static final Creator<PidInfo> CREATOR = new Creator<PidInfo>(){

        @Override
        public PidInfo createFromParcel(Parcel source) {
            return new PidInfo(source);
        }

        @Override
        public PidInfo[] newArray(int size) {
            return new PidInfo[0];
        }
    };
}

AIDL 声明

PidInfo 定义完后还不能直接使用,需要在Aidl 中声明:

  • 新建一个和类同名的Aidl 文件 PidInfo.aidl
  • 在PidInfo.aidl 中 声明PidInfo 为parcelable 类型
parcelable PidInfo;

code

Aild 的使用大概就是这些,code 地址:github

作者:chituhuan 发表于2016/8/21 15:10:23 原文链接
阅读:49 评论:0 查看评论

Android进阶系列4—从LayoutInflater到setContentView的LayoutInflater

$
0
0

N天前博主面试的时候,面试官问我TextView是如何加载到界面上的。博主说LayoutInflator,interviewer问怎么操作的呢?我说:记不得了。。。翻看郭神的博客,发现早在14年底,我就在他的Android LayoutInflater原理分析,带你一步步深入了解View(一)里面有留言,可能是当时too young,对郭神的讲解不知所云,时隔一年多再看,觉得博主很会讲课,通俗易懂。拍郭神个马屁(他很可能看不到),他是我在CSDN上看到的,最会讲解知识的人之一。
不管是动态加载布局,还是在xml实现布局,控件是被显示的过程都是相似的,离不开LayoutInflater的支持。本文将讲述LayoutInflater如何将布局加载,以及setContentView如何将参数传递到LayoutInflater中。
本文要大力感谢 Android应用setContentView与LayoutInflater加载解析机制源码分析,博主是看了他的文章之后才有些明白的,本文不只总结,会讲细节!!!啊哈哈。

1. LayouInflater

LayoutInflater的使用流程一般是这样的:

//第一步实例化LayoutInflater
LayoutInflater layoutInflater = LayoutInflater.from(context);  
//第二步inflate布局id并返回view
        View view = layoutInflater.inflate(R.layout.***id, null);  

实例化LayoutInflater有两种写法:

//第一种
  LayoutInflater lif = LayoutInflater.from(Context context);
//第二种
   LayoutInflater lif = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);

第一种写法其实是对第二种写法的封装

    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);//调用getSystemService获取LayoutInflater实例
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

实例化之后,就是layoutInflater.inflate()方法的调用,我们就从这儿说开去。

1. inflate()

    public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);//没有设置attachToRoot的情况下,如果root!=null,默认为true
    }

继续看inflate(int resource, ViewGroup root, boolean attachToRoot)方法:

    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        if (DEBUG) {
            Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("
                    + Integer.toHexString(resource) + ")");
        }
        final XmlResourceParser parser = res.getLayout(resource);//Android提供的pull解析方式解析布局文件
        try {
            return inflate(parser, root, attachToRoot);//关键的方法
        } finally {
            parser.close();
        }
    }

实际上,不管使用的哪个inflate()方法的重载,最终都会辗转调用到LayoutInflater的inflate(parser, root, attachToRoot)方法

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
        final AttributeSet attrs = Xml.asAttributeSet(parser);
        Context lastContext = (Context)mConstructorArgs[0];
        mConstructorArgs[0] = mContext;//定义返回值,初始化为传入的形参root
        View result = root;
        try {
             // 寻找根节点
             int type;
             while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
              }
              //如果一开始就是END_DOCUMENT,抛出异常
              if (type != XmlPullParser.START_TAG) {
                  throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
               }
                //通过了上面的while和if条件,说明这里type一定是START_TAG,也就是xml文件里的根节点
                final String name = parser.getName();
                if (DEBUG) {
System.out.println("**************************");
System.out.println("Creating root view: "+ name);
System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                //处理merge tag的情况(merge,APP的xml布局优化)
                    //root必须非空且attachToRoot为true,否则抛异常结束(使用merge时要注意的地方:merge的xml并不代表某个具体的view,只是将它包起来的其他xml的内容,加到某个上层ViewGroup中)
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    //递归rinflate方法调运,循环遍历布局下的子元素
                    rInflate(parser, root, attrs, false, false);
                } else {
                    // xml文件中的root view,根据tag节点创建view对象
                    final View temp = createViewFromTag(root, name, attrs, false);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                        if (DEBUG) {
                           System.out.println("Creating params from root: " +root);
                        }
//根据root生成LayoutParams
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
// 设置temp的LayoutParams,如果attachToRoot为true,则在后面会调用root.addView()。
                  temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    //递归调用rInflate绘制剩下的children
                    rInflate(parser, temp, attrs, true, true);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }
                    if (root != null && attachToRoot) {
                        //root非空且attachToRoot=true则将xml文件的temp root加到形参提供的root里
                        root.addView(temp, params);
                    }

                    // 如果root为空,或者为绑定到root
                    if (root == null || !attachToRoot) {
                        //返回xml里解析的root view
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            //返回参数root或xml文件里的temp root
            return result;
        }
    }

代码的详细说明在注释里面都体现了,inflate()方法的流程概括而言就是:

  1. 找到根节点
  2. 处理merge
  3. 创建根View
  4. 按照root参数,attachToRoot参数确定LayoutParams以及返回
  5. 递归绘制子View。

inflate的返回结果和参数之间的关系总结成三类情况:

  1. inflate(xmlId, null)/inflate(xmlId,null,true/false):返回xml的root view。
  2. inflate(xmlId,parent,false):利用parent的layoutParam绘制xml中的root view,将xml的root view返回。
  3. inflate(xmlId,parent,true)/inflate(xmlId,parent):返回parent,xml中内容以parent.addView()的方式加入到parent中。

注意到inflate()中调用了addView()方法,并提到了LayoutParam,我们看下这两个是干嘛的。

2. addView

//添加一个子View
public void addView(View child) {
        addView(child, -1);
    }
public void addView(View child, int index) {
        if (child == null) {
            throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
        }
        LayoutParams params = child.getLayoutParams();
        if (params == null) {
        //如果没有设置它的布局参数的话,会提供它的父布局的默认参数给子view 
            params = generateDefaultLayoutParams();
            if (params == null) {
                throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
            }
        }
        addView(child, index, params);//按指定的布局添加子View
    }

addView(child, index, params)的具体实现,我们暂且不关心,由上述流程可以看出,addView()的作用即可。

3. LayoutParams

再关心下LayoutParams布局参数有时干啥的?官方文档说:
LayoutParams are used by views to tell their parents how they want to be laid out——用于视图告诉父布局他们该如何摆放。不同的布局一般在ViewGroup的基础上,扩充了LayoutParams,在构建LayoutParams对象时,可调用父布局自身的构造函数创建。比如向LinearLayout添加子视图时,子视图可以

LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT);

不同的需求情况,查看谷歌手册就好。

4. rInflate()

在inflate()函数中,只是创建了一个布局文件中的根部局(按照条件决定是否要将其添加到参数root),对于根部局下的子布局会循环调用rInflate()函数遍历解析。看下rInflate()的实现代码,和inflate()有相似之处

    void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
            boolean finishInflate, boolean inheritContext) throws XmlPullParserException,
            IOException {

        final int depth = parser.getDepth();
        int type;
        //XmlPullParser解析器的标准解析模式
        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
            //找到START_TAG节点程序才继续执行这个判断语句之后的逻辑
            if (type != XmlPullParser.START_TAG) {
                continue;
            }
            //获取Name标记
            final String name = parser.getName();
            //处理REQUEST_FOCUS的标记
            if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_TAG.equals(name)) {
                //处理tag标记
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                //处理include标记
                if (parser.getDepth() == 0) {
                    //include节点如果是根节点就抛异常
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs, inheritContext);
            } else if (TAG_MERGE.equals(name)) {
                //merge节点必须是xml文件里的根节点(这里不该再出现merge节点)
                throw new InflateException("<merge /> must be the root element");
            } else {
                //创建View
                final View view = createViewFromTag(parent, name, attrs, inheritContext);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                //继续递归解析子View
                rInflate(parser, view, attrs, true, true);
                viewGroup.addView(view, params);
            }
        }
        //parent的所有子节点都inflate完毕的时候回onFinishInflate方法
        if (finishInflate) parent.onFinishInflate();
    }

rInflate的流程大概包括:

  1. 找到开始节点
  2. 处理requestFoucs,处理tag,处理include,处理merge
  3. 处理其他情况,并递归处理子节点,addView
  4. 全都绘制完后返回onFinish方法

可以看到不管在rInflate还是在inflate中,都是利用createViewFromTag创建View的。createViewFromTag方法内部又会去调用createView()方法,然后使用反射的方式创建出View的实例,createView方法不做深入分析。

总结

好啦,LayoutInflater就差不多说完了。从创建对象->inflate()->rInflate()完成从布局文件到View的建立,再到LayoutParams构建参数->addView,添加到父布局中。下篇再说setContentView到LayoutInflater中间又是如何衔接的。
很惭愧,做了一点微小的贡献!

作者:u011026779 发表于2016/8/21 15:18:03 原文链接
阅读:43 评论:0 查看评论

Android之PopupWindow-底部弹出,以及中间弹出有变暗效果

$
0
0

Android之PopupWindow-底部弹出,以及中间弹出有变暗效果

  • This class represents a popup window that can be used to display an arbitrary view. The popup window is a floating container that appears on top of the current activity.官网原文

  • 大概意思:这个类代表一个弹出窗口,可以用来显示任意的视图.弹出的窗口是当前活动的一个浮动容器.

  • 在这里写出我们项目中常用的两种弹窗方式,底部弹窗以及中间弹窗,并且可以设置PopupWindow以外部分有变暗的效果.只要大家理解了一种,其它的都可以很快的写出来的.

  • 先来张效果图:

    这里写图片描述

底部弹出PopupWindow(点击PopupWindow以外部分,PopupWindow 会消失)

  • 这里需要注意的是:当我们点击返回键或点击PopupWindow以外的地方时,需要PopupWindow消失时,下面的两个方法是很重要的.
    popupWindow.setFocusable(true); //设置PopupWindow的焦点
    popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000)); //为PopupWindow设置背景,这里为了不影响效果,我也设置了透明背景.具体解释看代码中,我写了很详细的注释.
/**
* 底部弹出PopupWindow
*
* 点击PopupWindow以外部分或点击返回键时,PopupWindow 会 消失
* @param view  parent view
*/
public void showBottomPopupWindow(View view) {
        //自定义PopupWindow的布局
        View contentView = LayoutInflater.from(this).inflate(R.layout.popupwindow_layout, null);
        //初始化PopupWindow,并为其设置布局文件
        final PopupWindow popupWindow = new PopupWindow(contentView);
        //确定按钮点击事件
        contentView.findViewById(R.id.tv_confirm).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(PopupWindowTest.this, "点击了确定", Toast.LENGTH_SHORT).show();
                popupWindow.dismiss();
            }
        });
        //取消按钮点击事件
        contentView.findViewById(R.id.tv_cancel).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                popupWindow.dismiss();
            }
        });
        //设置PopupWindow的宽和高,必须设置,否则不显示内容(也可用PopupWindow的构造方法设置宽高)
        popupWindow.setWidth(LinearLayout.LayoutParams.MATCH_PARENT);
        popupWindow.setHeight(LinearLayout.LayoutParams.WRAP_CONTENT);
        //当需要点击返回键,或者点击空白时,需要设置下面两句代码.
        //如果有背景,则会在contentView外面包一层PopupViewContainer之后作为mPopupView,如果没有背景,则直接用contentView作为mPopupView。
        //而这个PopupViewContainer是一个内部私有类,它继承了FrameLayout,在其中重写了Key和Touch事件的分发处理
        popupWindow.setFocusable(true);
        popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));   //为PopupWindow设置透明背景.
        popupWindow.setOutsideTouchable(false);
        //设置PopupWindow进入和退出动画
        popupWindow.setAnimationStyle(R.style.anim_popup_bottombar);
        //设置PopupWindow显示的位置
        popupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0);
    }

底部弹出PopupWindow(点击PopupWindow以外部分,PopupWindow 不会消失 )

  • 这里设置的是,点击PopupWindow以外部分,不让PopupWindow消失,只有点击返回键,或用户主动取消时,PopupWindow才会消失.已满足有时我们变态的需求….直接上代码.
/**
* 底部弹出PopupWindow
*
* 点击PopupWindow以外部分或点击返回键时,PopupWindow 不会 消失
* @param view  parent view
*/
    public void showBottomPopupWindow2(View view) {
        //自定义PopupWindow的布局
        View contentView = LayoutInflater.from(this).inflate(R.layout.popupwindow_layout, null);
        contentView.setFocusable(true);
        contentView.setFocusableInTouchMode(true);
        //初始化PopupWindow对象,并为其设置宽高以及布局文件
        final PopupWindow popupWindow = new PopupWindow(contentView, LinearLayout.LayoutParams.MATCH_PARENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        popupWindow.setFocusable(true);
        popupWindow.setOutsideTouchable(false);

        contentView.setOnKeyListener(new View.OnKeyListener() {
            @Override
            public boolean onKey(View v, int keyCode, KeyEvent event) {
                if (keyCode == KeyEvent.KEYCODE_BACK) {
                    popupWindow.dismiss();

                    return true;
                }
                return false;
            }
        });
        //确定按钮点击事件
        contentView.findViewById(R.id.tv_confirm).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(PopupWindowTest.this, "点击了确定", Toast.LENGTH_SHORT).show();
                popupWindow.dismiss();
            }
        });
        //取消按钮点击事件
        contentView.findViewById(R.id.tv_cancel).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                popupWindow.dismiss();
            }
        });

        //设置PopupWindow进入和退出动画
        popupWindow.setAnimationStyle(R.style.anim_popup_bottombar);
        //设置PopupWindow显示在底部
        popupWindow.showAtLocation(view, Gravity.BOTTOM, 0, 0);
    }

中间弹出PopupWindow,有一种变暗的效果

  • 其实设置变暗效果也挺简单的,我们知道,微信上就有这种效果.下面给出代码:(需要注意的是:需要监听PopupWindow的状态,当PopupWindow消失时,恢复设置的背景颜色)
// 设置PopupWindow以外部分的背景颜色  有一种变暗的效果
final WindowManager.LayoutParams wlBackground = getWindow().getAttributes();
wlBackground.alpha = 0.5f;      // 0.0 完全不透明,1.0完全透明
getWindow().setAttributes(wlBackground);
  • 完整代码:
/**
* 中间弹出PopupWindow
*
*  设置PopupWindow以外部分有一中变暗的效果
* @param view  parent view
*/
public void showCenterPopupWindow(View view) {
        View contentView = LayoutInflater.from(this).inflate(R.layout.popupwindow_layout, null);
        final PopupWindow popupWindow = new PopupWindow(contentView, LinearLayout.LayoutParams.WRAP_CONTENT, LinearLayout.LayoutParams.WRAP_CONTENT);
        TextView tvTitle = (TextView)contentView.findViewById(R.id.tv_title);
        TextView tvConfirm = (TextView)contentView.findViewById(R.id.tv_confirm);
        TextView tvCancel = (TextView)contentView.findViewById(R.id.tv_cancel);
        tvTitle.setText("标为已读");
        tvConfirm.setText("置顶公众号");
        tvCancel.setText("取消关注");

        tvTitle.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(PopupWindowTest.this, "标为已读", Toast.LENGTH_SHORT).show();
                popupWindow.dismiss();
            }
        });

        tvConfirm.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(PopupWindowTest.this, "置顶公众号", Toast.LENGTH_SHORT).show();
                popupWindow.dismiss();
            }
        });

        tvCancel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(PopupWindowTest.this, "取消关注", Toast.LENGTH_SHORT).show();
                popupWindow.dismiss();
            }
        });
        popupWindow.setFocusable(true);
        popupWindow.setBackgroundDrawable(new ColorDrawable(0x00000000));
        // 设置PopupWindow以外部分的背景颜色  有一种变暗的效果
        final WindowManager.LayoutParams wlBackground = getWindow().getAttributes();
        wlBackground.alpha = 0.5f;      // 0.0 完全不透明,1.0完全透明
        getWindow().setAttributes(wlBackground);
        // 当PopupWindow消失时,恢复其为原来的颜色
        popupWindow.setOnDismissListener(new PopupWindow.OnDismissListener() {
            @Override
            public void onDismiss() {
                wlBackground.alpha = 1.0f;
                getWindow().setAttributes(wlBackground);
            }
        });
        //设置PopupWindow进入和退出动画
        popupWindow.setAnimationStyle(R.style.anim_popup_centerbar);
        // 设置PopupWindow显示在中间
        popupWindow.showAtLocation(view,Gravity.CENTER,0,0);

    }
作者:listeners_Gao 发表于2016/8/21 16:14:19 原文链接
阅读:52 评论:0 查看评论

实现音乐播放器歌词显示效果

$
0
0

这两天有个任务,说是要写一个QQ音乐播放器歌词的那种效果,毕竟刚学自定义View,没有什么思路,然后就Google.写了一个歌词效果,效果图在后面,下面是我整理的代码。

首先实现这种效果有两种方式

    1.自定义View里重载onDraw方法,自己绘制歌词

    2.用ScrollView实现

   第一种方式比较精确,但要支持滑动之后跳转播放的话难度很大,所以我选择第二种,自定义ScrollView

我也不多说,直接上代码,代码中有注释

 一.自定义LycicView extends ScrollView

   里面包括一个空白布局,高度是LycicView的一半,再是一个布局存放歌词的,最后是一个空白布局高度是LycicView的一半

  这里动态的向第二个布局里面添加了显示歌词的TextView,并利用ViewTreeObserver得到每个textview的高度,方便知道每个textview歌词所要滑动到的高度

public class LycicView extends ScrollView {
    LinearLayout rootView;//父布局
    LinearLayout lycicList;//垂直布局
    ArrayList<TextView> lyricItems = new ArrayList<TextView>();//每项的歌词集合

    ArrayList<String> lyricTextList = new ArrayList<String>();//每行歌词文本集合,建议先去看看手机音乐里的歌词格式和内容
    ArrayList<Long> lyricTimeList = new ArrayList<Long>();//每行歌词所对应的时间集合
    ArrayList<Integer> lyricItemHeights;//每行歌词TextView所要显示的高度

    int height;//控件高度
    int width;//控件宽度
    int prevSelected = 0;//前一个选择的歌词所在的item


    public LycicView(Context context) {
        super(context);
        init();
    }

    public LycicView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public LycicView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }
    private void init(){
        rootView = new LinearLayout(getContext());
        rootView.setOrientation(LinearLayout.VERTICAL);
        //创建视图树,会在onLayout执行后立即得到正确的高度等参数
        ViewTreeObserver vto = rootView.getViewTreeObserver();
        vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
            @Override
            public void onGlobalLayout() {
                height = LycicView.this.getHeight();
                width = LycicView.this.getWidth();

                refreshRootView();

            }
        });
        addView(rootView);//把布局加进去
    }

    /**
     *
     */
    void refreshRootView(){
        rootView.removeAllViews();//刷新,先把之前包含的所有的view清除
        //创建两个空白view
        LinearLayout blank1 = new LinearLayout(getContext());
        LinearLayout blank2 = new LinearLayout(getContext());
        //高度平分
        LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(width,height/2);
        rootView.addView(blank1,params);
        if(lycicList !=null){
            rootView.addView(lycicList);//加入一个歌词显示布局
            rootView.addView(blank2,params);
        }

    }

    /**
     *设置歌词,
     */
    void refreshLyicList(){
        if(lycicList == null){
            lycicList = new LinearLayout(getContext());
            lycicList.setOrientation(LinearLayout.VERTICAL);
            //刷新,重新添加
            lycicList.removeAllViews();
            lyricItems.clear();
            lyricItemHeights = new ArrayList<Integer>();
            prevSelected = 0;
            //为每行歌词创建一个TextView
            for(int i = 0;i<lyricTextList.size();i++){
                final TextView textView = new TextView(getContext());

                textView.setText(lyricTextList.get(i));
                //居中显示
                LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
                params.gravity = Gravity.CENTER_HORIZONTAL;
                textView.setLayoutParams(params);
                //对高度进行测量
                ViewTreeObserver vto = textView.getViewTreeObserver();
                final int index = i;
                vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() {
                    @Override
                    public void onGlobalLayout() {
                                textView.getViewTreeObserver().removeOnGlobalLayoutListener(this);//api 要在16以上 >=16
                                lyricItemHeights.add(index,textView.getHeight());//将高度添加到对应的item位置
                    }
                });
                lycicList.addView(textView);
                lyricItems.add(index,textView);
            }
        }
    }
    /**
     *     滚动到index位置
     */
    public void scrollToIndex(int index){
        if(index < 0){
            scrollTo(0,0);
        }
        //计算index对应的textview的高度
        if(index < lyricTextList.size()){
            int sum = 0;
            for(int i = 0;i<=index-1;i++){
                sum+=lyricItemHeights.get(i);
            }
            //加上index这行高度的一半
            sum+=lyricItemHeights.get(index)/2;
            scrollTo(0,sum);
        }
    }

    /**
     * 歌词一直滑动,小于歌词总长度
     * @param length
     * @return
     */

    int getIndex(int length){
        int index = 0;
        int sum = 0;
        while(sum <= length){
            sum+=lyricItemHeights.get(index);
            index++;
        }
        //从1开始,所以得到的是总item,脚标就得减一
        return index - 1;
    }

    /**
     * 设置选择的index,选中的颜色
     * @param index
     */
    void setSelected(int index){
        //如果和之前选的一样就不变
        if(index == prevSelected){
            return;
        }
        for(int i = 0;i<lyricItems.size();i++){
            //设置选中和没选中的的颜色
            if(i == index){
                lyricItems.get(i).setTextColor(Color.BLUE);
            }else{
                lyricItems.get(i).setTextColor(Color.WHITE);
            }
            prevSelected = index;
        }
    }

    /**
     * 设置歌词,并调用之前写的refreshLyicList()方法设置view
     * @param textList
     * @param timeList
     */
    public void setLyricText(ArrayList<String> textList,ArrayList<Long> timeList){
        //因为你从歌词lrc里面可以看出,每行歌词前面都对应有时间,所以两者必须相等
        if(textList.size() != timeList.size()){
             throw  new IllegalArgumentException();
        }
        this.lyricTextList = textList;
        this.lyricTimeList = timeList;

        refreshLyicList();
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);
        //滑动时,不往回弹,滑到哪就定位到哪
        setSelected(getIndex(t));
        if(listener != null){
            listener.onLyricScrollChange(getIndex(t),getIndex(oldt));
        }
    }
    OnLyricScrollChangeListener listener;
    public void setOnLyricScrollChangeListener(OnLyricScrollChangeListener l){
        this.listener = l;
    }

    /**
     * 向外部提供接口
     */
    public interface  OnLyricScrollChangeListener{
        void onLyricScrollChange(int index,int oldindex);
    }
}

 二..MainActivity中的布局

 

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:background="@mipmap/img01"
    tools:context=".MainActivity">

    <EditText
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:inputType="number"
        android:ems="10"
        android:id="@+id/editText"
        android:layout_alignParentBottom="true"
        android:layout_alignParentLeft="true"
        android:layout_alignParentStart="true" />

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="scroll to"
        android:id="@+id/button"
        android:layout_alignTop="@+id/editText"
        android:layout_alignParentRight="true"
        android:layout_alignParentEnd="true" />

    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_alignParentTop="true"
        android:layout_centerHorizontal="true"
        android:layout_above="@+id/editText">

        <custom.LycicView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:id="@+id/view"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />

        <View
            android:layout_width="match_parent"
            android:layout_height="2dp"
            android:background="@null"
            android:id="@+id/imageView"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
        <View
            android:layout_below="@id/imageView"
            android:layout_width="match_parent"
            android:layout_height="1dp"
            android:layout_marginTop="6dp"
            android:background="#999999"
            android:id="@+id/imageView2"
            android:layout_centerVertical="true"
            android:layout_centerHorizontal="true" />
    </RelativeLayout>
</RelativeLayout>

   具体实现代码如下:

public class MainActivity extends AppCompatActivity {

    LycicView view;
    EditText editText;
    Button btn;
    Handler handler = new Handler(new Handler.Callback() {
        @Override
        public boolean handleMessage(Message msg) {
            if(msg.what == 1){

                if(lrc_index == list.size()){
                    handler.removeMessages(1);
                }
                lrc_index++;

                System.out.println("******"+lrc_index+"*******");
                view.scrollToIndex(lrc_index);
                handler.sendEmptyMessageDelayed(1,4000);
            }
            return false;
        }
    });
    private ArrayList<LrcMusic> lrcs;
    private ArrayList<String> list;
    private ArrayList<Long> list1;
    private int lrc_index;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initViews();

        initEvents();
    }
    private void initViews(){
        view = (LycicView) findViewById(R.id.view);
        editText = (EditText) findViewById(R.id.editText);
        btn = (Button) findViewById(R.id.button);
    }
    private void initEvents(){
        InputStream is = getResources().openRawResource(R.raw.eason_tenyears);

       // BufferedReader br = new BufferedReader(new InputStreamReader(is));
        list = new ArrayList<String>();
        list1 = new ArrayList<>();
        lrcs = Utils.redLrc(is);
        for(int i = 0; i< lrcs.size(); i++){
             list.add(lrcs.get(i).getLrc());
            System.out.println(lrcs.get(i).getLrc()+"=====");
            list1.add(0l);//lrcs.get(i).getTime()
        }
        view.setLyricText(list, list1);
        view.postDelayed(new Runnable() {
            @Override
            public void run() {
                view.scrollToIndex(0);
            }
        },1000);

        btn.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String text = editText.getText().toString();
                int index = 0;
                index = Integer.parseInt(text);
                view.scrollToIndex(index);
            }
        });
        view.setOnLyricScrollChangeListener(new LycicView.OnLyricScrollChangeListener() {
            @Override
            public void onLyricScrollChange(final int index, int oldindex) {
                editText.setText(""+index);
                lrc_index = index;
                System.out.println("===="+index+"======");
                //滚动handle不能放在这,因为,这是滚动监听事件,滚动到下一次,handle又会发送一次消息,出现意想不到的效果
            }
        });
        handler.sendEmptyMessageDelayed(1,4000);
        view.setOnTouchListener(new View.OnTouchListener() {
            @Override
            public boolean onTouch(View v, MotionEvent event) {
                switch (event.getAction()){
                    case MotionEvent.ACTION_DOWN:
                        handler.removeCallbacksAndMessages(null);

                        System.out.println("取消了");
                        break;
                    case MotionEvent.ACTION_UP:
                        System.out.println("开始了");
                        handler.sendEmptyMessageDelayed(1,2000);
                        break;
                    case MotionEvent.ACTION_CANCEL://时间别消耗了
                        break;
                }
                return false;
            }
        });

        getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE|WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN);
    }

}

 

其中utils类和LycicMusic是一个工具类和存放Music信息实体类
   Utils类
public class Utils {
    public static ArrayList<LrcMusic> redLrc(InputStream in) {
        ArrayList<LrcMusic> alist = new ArrayList<LrcMusic>();
        //File f = new File(path.replace(".mp3", ".lrc"));
        try {
            //FileInputStream fs = new FileInputStream(f);
            InputStreamReader input = new InputStreamReader(in, "utf-8");
            BufferedReader br = new BufferedReader(input);
            String s = "";

            while ((s = br.readLine()) != null) {
                if (!TextUtils.isEmpty(s)) {
                    String lyLrc = s.replace("[", "");
                    String[] data_ly = lyLrc.split("]");
                    if (data_ly.length > 1) {
                        String time = data_ly[0];
                        String lrc = data_ly[1];
                        LrcMusic lrcMusic = new LrcMusic(lrcData(time), lrc);
                        alist.add(lrcMusic);
                    }
                }
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return alist;
    }
    public static int lrcData(String time) {
        time = time.replace(":", "#");
        time = time.replace(".", "#");

        String[] mTime = time.split("#");

        //[03:31.42]
        int mtime = Integer.parseInt(mTime[0]);
        int stime = Integer.parseInt(mTime[1]);
        int mitime = Integer.parseInt(mTime[2]);

        int ctime = (mtime*60+stime)*1000+mitime*10;

        return ctime;
    }
}

  LrcMusic实体类

  

public class LrcMusic {
    private int time;
    private String lrc;

    public LrcMusic() {
    }

    public LrcMusic(int time, String lrc) {
        this.time = time;
        this.lrc = lrc;
    }

    public int getTime() {
        return time;
    }

    public void setTime(int time) {
        this.time = time;
    }

    public String getLrc() {
        return lrc;
    }

    public void setLrc(String lrc) {
        this.lrc = lrc;
    }
}

效果图:


大体就这样,如有无情纠正,附上源码地址:点击打开链接


作者:Tenderness4 发表于2016/8/21 16:33:25 原文链接
阅读:38 评论:0 查看评论

Android面试之Activity

$
0
0

对于面试,有人恐惧有人享受其过程,但对于不善言语的猿猴,想必面试一定很痛苦,不过不要紧,面试一般会分为两个环节:技术面试和人事面试。

想把自己面试过程遇到的问题总结一下,分享给更多朋友。


本文重点讲技术环节。


考虑良久,我认为还是要从Activity开始说起。引用《Android开发艺术探索》的开篇语的一句话就是:Activity时候如此重要,以至于开篇不得不讲到它。

在我看来,Activity确实是非常重要的。下面进入正题:

Activity作为Android开发中最基础的部分,面试中经常会遇到。由于Android系统是由四大组件所构成,且难度不不大,所以   如果在面试时没有将Activity相关问题回答好,我想基本上就会被Pass掉。

1.Activity的启动模式

2.Activity的生命周期

3.Activity的状态保存和恢复。


在介绍Activity的启动模式前,有必要先了解一下Activity的任务栈。所谓任务栈中的任务,可以单纯的理解为用户的操作行为。比如我们用QQ聊天,必须先打开QQ,找到好友,发送消息,这些都可以看做是任务。而这些任务,都运行在Activity中,在Activity的角度来看,任务就是用户将Activity放入任务栈中执行的过程。其中,一个进程可以有多个任务栈,而任务栈中的Activity会按照启动顺序依次被push到当前进程的任务栈中,而我们的跳转和返回事件,就是进栈和出栈的过程,以此来完成任务。那么任务栈和运行中的APP是一一对应的关系么?答案是否定的,一个APP的Activity可以属于不同的任务栈,同样,一个任务栈的Activity可以来自于不同的APP,这种情况通常是需要多个APP配合完成任务,例如有些APP中使用了XX地图等。


启动方式1.standard

默认的启动模式,每次使用该模式下的Activity都会创建一个新的Activity实例并push到任务栈中,这样就可能会出现一个任务栈中存在同一Activity的多个实例的情况。考虑一下,这样做什么好处和坏处呢?好处是使用简单,不需要额外配置,如果Activity比较轻量级那么用这种模式一般不会出现什么问题;坏处是,如果创建实例太过频繁或者Activity的开销较大,那么这种启动模式就很有可能带来性能问题,还有就是,在使用某些APP时,偶尔会出现使用BACK键返回,结果一直在几个页面来回切的情况,这有可能就是启动模式的选择有问题。

b.singleTop

栈顶复用模式,也就是说,当你想启动Activity A 时,如果目标任务栈中已经存在Activity A 且位于栈顶,那么系统就不会再次实例化它,而是直接使用栈顶的Activity A。这样有个好处,不会出现由于栈顶都是相同Activity而导致用户出现BACK键失灵的错觉。那么在什么情况下使用singleTop模式呢?比如某个APP的多个推送消息显示在通知栏,你想把他们都给点了,每次点击实际就是启动某个Activity,这时候如果使用的是singleTop模式,就可以有效避免重复创建同一Activity实例的工作。

c.singleTask

官方解释为:标志为singleTask的Activity,最多仅有一个实例存在,并且位于以它为根的Task中,所有对该Activity的请求,都会跳到该Activity的Task中展开进行。如果我们使用时,该Activity不在栈顶,那么位于它上方的Activity就会全部出栈,从而使其回到栈顶。什么时候该模式呢?我们一般在创建开销较大且经常被其他APP调用的Activity中使用,这种Activity一般作为程序的入口,比如浏览器的主页使用的就是singleTask。

d.singleInstance启动模式

singleInstance模式和singleTask很相似,他们的不同点仅在于,该模式下的Activity是它所属任务栈中的唯一Activity。可以看出,这样的Activity封闭性极强,能够很好地保持唯一性,也能大大减少不必要的开销。一个应用的例子就是XX地图,如果APP中使用了XX地图,那么当我们切到桌面,并进入到XX地图时,看到的页面跟之前是一样的。


生命周期

由于生命周期相对太简单太简单,而且到处都是有关生命周期的博客,这里提供@小孟6601的精彩回答,原文地址http://ask.csdn.net/questions/262577

由于回答太精彩了,本人学不来,所以就不发表自己的见解了


activity三种状态
(1)active:当Activity运行在屏幕前台(处于当前任务活动栈的最上面),此时它获取了焦点能响应用户的操作,属于活动状态,同一个时刻只会有一个Activity处于活动(Active)。
(2)paused:当Activity失去焦点但仍对用户可见(如在它之上有另一个透明的Activity或Toast、AlertDialog等弹出窗口时)它处于暂停状态。暂停的Activity仍然是存活状态(它保留着所有的状态和成员信息并保持和窗口管理器的连接),但是当系统内存极小时可以被系统杀掉。
(3)stoped:完全被另一个Activity遮挡时处于停止状态,它仍然在内存中保留着所有的状态和成员信息。只是对用户不可见,当其他地方需要内存时它往往被系统杀掉。
3、activity七个方法
onCreate():当Activity第一次被实例化的时候系统会调用,整个生命周期只调用1次这个方法。通常用于初始化设置,为Activity设置所要使用的布局文件,为按钮绑定监听器等静态的设置操作。
onStart():当Activity可见未获得用户焦点不能交互时系统会调用。
onRestart():当Activity已经停止然后重新被启动时系统会调用。
onResume():当Activity可见且获得用户焦点能交互时系统会调用。
onPause():用来存储持久数据。到这一步是可见但不可交互的,系统会停止动画等消耗CPU的事情。从上文的描述已经知道,应该在这里保存你的一些数据,因为这个时候你的程序的优先级降低,有可能被系统收回。
onStop():当Activity被新的Activity完全覆盖不可见时被系统调用。
onDestroy():当Activity(用户调用finish()或系统由于内存不足)被系统销毁杀掉时系统调用,(整个生命周期只调用1次)用来释放onCreate()方法中创建的资源,如结束线程等。
4、android三个嵌套循环
(1)Activity完整的生命周期:从第一次调用onCreate()开始直到调用onDestroy()结束。
(2)Activity的可视生命周期:从调用onStart()到相应的调用onStop()。在这两个方法之间,可以保持显示Activity所需要的资源。如在onStart()中注册一个广播接收者监听影响你的UI的改变,在onStop()中注销。
(3)Activity的前台生命周期:从调用onResume()到相应的调用onPause()。
5、BroadcastReceiver广播接收器生命周期
生命周期只有十秒左右,如果在onReceive()内做超过十秒内的事情,就会报ANR(Application No Response)程序无响应的错误信息。它的生命周期为从回调onReceive()方法开始到该方法返回结果后结束。
6、Service服务生命周期
Service完整的生命周期从调用onCreate()开始直到调用onDestroy()结束。
Service有两种使用方法:
(1)以调用Context.startService()启动,而以调用Context.stopService()结束。
(2)以调用Context.bindService()方法建立,以调用Context.unbindService()关闭。

7、一个activity的启动过程
(1)第一个Activity的启动顺序:onCreate()——>onStart()——>onResume()
(2)当另一个Activity启动时:第一个Activity onPause()——>第二个Activity onCreate()——>onStart()——>onResume()——>第一个Activity onStop()
(3)当返回到第一个Activity时:第二个Activity onPause()——> 第一个Activity onRestart()——>onStart()——>onResume()——>第二个Activity onStop()——>onDestroy()
每一个Activity都处于某一个状态,对于开发者来说,是无法控制其应用程序处于某一个状态的,这些均由系统来完成。


Activity的状态保存和恢复


正常情况下,我们会在Activity的OnCreate时初始化一些工作。

在onDestroy的时候将无用的资源回收

基本上到这里就没什么好说的了。下面总结一下

(1) onStart和onResume的区别是onStart可见,还没有出现在前台,无法和用户进行交互。onResume获取到焦点可以和用户交互。


(2) 新Activity是透明主题时,旧Activity不会走onStop;


(3)Activity切换时,旧Activity的onPause会先执行,然后才会启动新的Activity;


(4)Activity在异常情况下被回收时,onSaveInstanceState方法会被回调,回调时机是在onStop之前,当Activity被重新创建的时候,onRestoreInstanceState方法会被回调,时序在onStart之后;


(5)Activity的LaunchMode


a.
 standard 系统默认。每次启动会重新创建新的实例,谁启动了这个Activity,这个Activity就在谁的栈里。


b.
 singleTop 栈顶复用模式。该Activity的onNewIntent方法会被回调,onCreate和onStart并不会被调用。


c.
 singleTask 栈内复用模式。只要该Activity在一个栈中存在,都不会重新创建,onNewIntent会被回调。如果不存在,系统会先寻找是否存在需要的栈,如果不存在该栈,就创建一个任务栈,然后把这个Activity放进去;如果存在,就会创建到已经存在的这个栈中。


d.
 singleInstance。具有此种模式的Activity只能单独存在于一个任务栈。


(5) 标识Activity任务栈名称的属性:TaskAffinity,默认为应用包名。


如果有什么不明白的或者需要探讨,欢迎留言或者加我们的QQ群一起讨论。我们的QQ群是140767523,人少,但是我们都致力于解决你的 问题。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------转载请注明出处。

作者:u012552275 发表于2016/8/21 16:34:19 原文链接
阅读:33 评论:0 查看评论

Android责任链模式

$
0
0

责任链模式,使得每个对象都有机会去处理请求,从而避免请求的发送者和处理者之间的耦合,对于请求的发送者不需要关心具体的处理者是谁,这样就可以动态的去组织和分配的具体的处理者。

责任链的概述

责任链的描述

阎宏博士的《JAVA与模式》关于责任链的描述
责任链模式是一种对象的行为模式。在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,直到链上的某一个对象决定处理此请求。发出这个请求的客户端并不知道链上的哪一个对象最终处理这个请求,这使得系统可以在不影响客户端的情况下动态地重新组织和分配责任。

责任链的uml

这里写图片描述
- Client:客户端,请求的发送者
- Handler:请求的处理者,其中的successor引用的是上层Handler对象,从而形成一个链状。
- ConcreteHandler:具体的请求的处理者

标准的责任链

当客户端Client发送请求的时候,接收端接受请求的时候,从链的第一个处理者Handler开始尝试去处理,如果第一个Handler处理成功,则该请求的处理完成,如果第一个请求没有处理成功,则交给这个Handler的successor引用的上层的Handler去处理,直到某个Handler处理成功。
在android,View measure,layout ,draw,dispathEvent,saveInsances,都是采用责任链模式去实现,从而将某个请求从发送,然后在每个处理者都相应对这个请求进行相应的处理。

责任链模式的时序图

这里写图片描述

责任链使用的场景

多个处理者

请求的发出后,每个处理者都会请求进行处理。并根据请求的不同从而对请求的处理不一样。

单个处理者

多个处理者都有机会去处理请求,但是最终是交给其中一种的处理者,客户端不必知道具体的请求处理者。只需要请求的返回值即可。

总之,在存在时序或者优先级的时候处理过程,都可能使用责任链模式。

例子

关于责任链模式的例子有很多,比如常见的申请费用,对联网请求request的处理。下面我说下,在android中的使用的一些场景。

场景一:图片的加载

需求分析:图片的加载标准是先从MemoryCache加载,如果没有加载本地文件。如果还是没有则加载Remote的图片。

uml图

这里写图片描述

代码分析

ImageHandler:定义抽象的ImageHandler,

public abstract class ImageHandler{
    protect ImageHandler superImageHandler;

    public abstract Bitmap getBitmap(String uri);

    public void setSuperImageHandler(ImageHandler handler){
        superImageHandler = handler;
    }

    public ImageHandler getSuperImageHandler(){
        return superImageHandler;
    }
}

MemoryImageHandler:从内存加载图片

public class MemoryImageHandler extends ImageHandler{
    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    public Bitmap getBitmap(String uri){
        //从MemoryCache 取Bitmap
        Bitmap bitmap = getMemoryCahe(uri);

        if(bitmap == null && superImageHandler != null){
            bitmap = superImageHandler.getBitmap(uri);
            //保存到MemoryCache
            saveMemoryCache(uri,bitmap);
        }

        return bitmap;
    }
}

FileImageHandler:从文件加载图片

public class FileImageHandler extends ImageHandler{

    @Override
    public Bitmap getBitmap(String uri){
        //从File 中取Bitmap 
        Bitmap bitmap = getFileBitmap(uri);

        if(bitmap == null && superImageHandler != null){
            bitmap = superImageHandler.getBitmap(uri);
            //保存到File
            saveFile(uri);
        }
        return bitmap;
    }
}

RemoteImageHandler :从远程服务器加载图片

public class RemoteImageHandler extends ImageHandler{
    @Override
    public Bitmap getBitmap(String uri){
        //remote net 中取Bitmap 
        Bitmap bitmap = getRemoteBitmap(uri);
        return bitmap;
    }
}

ImageHandlerManager:动态组织和分配具体的ImageHandler

public class ImageHandlerManager{
    private ImageHandler firstHandler;
    private ImageHandler secondHandler;
    private ImageHandler thirdHandler;

    public ImageHandlerMangaer(){
        firstHandler = new MemoryImageHandler();
        secondHandler = new FileImageHandler();
        thirdHandler = new RemoteImageHandler();

        firstImageHandler.setSuperImageHandler(secondHandler);
        secondHandler.setSuperImageHandler(thirdHandler);
    }

    public Bitmap getBitmap(String uri){
        firstImageHandler.getBitmap(uri);
    }
}

MainActivity:Client

ImageHandlerManager imageHandlerManager = new ImageHandlerManager();
Bitmap imageBitmap = imageHandlerManager.getBitmap(uri);

从上面可以看出,加载的图片的三种发式,通过动态去处理加载图片的请求,并对这个请求的进行相应的处理(保存的操作),使用责任链模式,使得三种ImageHander均有机会去处理到这个请求,同时对这个请求做相应的处理,满足使用情境1.

场景二 adapter的复用

需求分析:对于一些比较容易的复用的adapter,我们应该尽量抽取出来,并共用着,但是通常的一个列表视图是存在的多类型的情况下,所以我们就需要一个WarpperAdapter去包装下我们的Adapter.但是这个时候,又存在一个问题就是我们想在里面的adapter去notify的时候,外面包装的WarpperAdapter是没有去的notify的。解决这一问题,通常有两种方案,一种就是利用责任链模式来实现,还有一种就是监听的方式实现,理论上推荐第一种。

代码分析

我们需要实现下面这样子的多type的视图结构
这里写图片描述

首先基本的BaseAdapter为

public abstract class BaseAdapter<T> extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    protected BaseAdapter wrapperAdapter;
    protected Context context;
    protected List<T> dataList;

    {
        dataList = new ArrayList<>();
    }

    public BaseAdapter(Context context) {
        this.context = context;
    }

    public void setDataList(@NonNull List<T> dataList) {
        this.dataList = dataList;
    }

    /**
     * 刷新数据,尽量使用这个方法,而不是notifyDataSetChanged();
     */
    public void requestNotify() {
        if (wrapperAdapter != null) {
            wrapperAdapter.notifyDataSetChanged();
        } else {
            notifyDataSetChanged();
        }
    }

    public void setWrapperAdapter(BaseAdapter wrapperAdapter){
        this.wrapperAdapter = wrapperAdapter;
    }

    public void addDataList(@NonNull List<T> dataList) {
        this.dataList.addAll(dataList);
    }

    @Override
    public int getItemCount() {
        return dataList.size();
    }
}

注意上面的requestNotify的方法,先检测有没有包装的Adapter,从而将请求分配给上一层或者自己。注意这个方法,这个是将Adapter 和mWarpperAdapter行成责任链模式。

接下来看下里面带图片的Adapter:用来展示图片的item的

public class CategoryListAdapter extends BaseRecyclerAdapter<PlayListModel> {

    public CategoryListAdapter(Activity activity) {
        super(activity);
    }

    @Override
    public CategoryListAdapter onCreateViewHolder(ViewGroup parent, int viewType) {
        return new CategoryViewHolder(mActivity, parent);
    }

    @Override
    public void onBindViewHolder(CategoryListAdapter viewHolder, int position) {
        onBindViewHolder(viewHolder, position, false);
    }

    public void onBindViewHolder(CategoryListAdapter holder, int position, boolean listEnd) {

        PlayListModel playListModel = mDataList.get(position);
        final String playlist_name = playListModel.getName();
        final String playlist_image = playListModel.getImage();
        final int playlist_counts = playListModel.getCount();
        final int playlist_id = playListModel.getId();

        holder.itemView.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                VideoPlayListActivity.start(mActivity, playlist_id, playlist_name, playlist_image, playlist_counts);

                if(mOnClickListener!=null){
                    mOnClickListener.onClick(holder.itemView,position);
                }
            }
        });
    }
}

紧接着是标题的item的adapter

public class CategoryNameAdapter extends BaseRecyclerAdapter<StringCategoryNameViewHolder> {

    public CategoryNameAdapter(Activity activity) {
        super(activity);
    }

    @Override
    public CategoryNameViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new CategoryNameViewHolder(mActivity, parent);
    }

    @Override
    public void onBindViewHolder(CategoryNameViewHolder viewHolder, int position) {
        holder.mCategoryName.setText(mDataList.get(position));
    }

    @Override
    public int getItemCount() {
        return mDataList.size();
    }
}

最后就是重点将两个Adapter包装在一起的WarpperAdapter;

public class VideoCategoryListAdapter extends BaseAdapter {
    private Activity mActivity;

    private static final int CATEGORYLIST_ADAPTER_TYPE = 2;
    private static final int CATEGORY_LIST_NAME_TYPE = 1;

    private CategoryListAdapter mNormalAdapter;
    private CategoryNameAdapter mNameAdapter;

    private List<PlayListModel> mPlayListDatas = new ArrayList<>();
    private Map<Integer, Integer> mCategoryListCountsMap = new HashMap<>();
    private List<String> mNames = new ArrayList<>();


    public VideoCategoryListAdapter(Activity activity) {
        mActivity = activity;
        mNormalAdapter = new CategoryListAdapter(activity);
        mNameAdapter = new CategoryNameAdapter(activity);

        mNormalAdapter.setWrapperAdapter(this);
        mNameAdapter.setWrapperAdapter(this);
    }

    @Override
    public int getItemViewType(int position) {
        int count = 0;
        for (int i = 0; i < mCategoryListCountsMap.size(); i++) {
            if (position == count + i) {
                return CATEGORY_LIST_NAME_TYPE;
            }
            count += mCategoryListCountsMap.get(i);
        }
        return CATEGORYLIST_ADAPTER_TYPE;
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        switch (viewType) {
            case CATEGORY_LIST_NAME_TYPE:
                return new CategoryNameViewHolder(mActivity, parent);

            default:
            case CATEGORYLIST_ADAPTER_TYPE:
                return new CategoryViewHolder(mActivity, parent);
        }
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        int type = getItemViewType(position);
        int offset = 0;
        int count = 0;
        boolean isSongListEnd = false;

        for (int i = 0; i < mCategoryListCountsMap.size(); i++) {
            if (position >= count + i) {//0+0  //5+1  //18+2
                count += mCategoryListCountsMap.get(i);
                offset++;
            }
            if (position == count + i - 1) {
                isSongListEnd = true;
            }
        }

        switch (type) {
            case CATEGORY_LIST_NAME_TYPE:
                mNameAdapter.onBindViewHolder(holder, offset - 1);
                break;
            case CATEGORYLIST_ADAPTER_TYPE:
                mNormalAdapter.onBindViewHolder(holder, position - offset, isSongListEnd);
                break;
        }

    }

    @Override
    public int getItemCount() {
        return mNameAdapter.getItemCount() + mNormalAdapter.getItemCount();
    }

}    

关于逻辑大家可以跳过,只需要注意在构造器的时候,通过setWrapperAdapter()的方式,将VideoCategoryListAdapter和CategoryListAdapter以及CategoryNameAdapter进行绑定,从而无论你在任何时候想刷新的时候,只要的调用requestNotify既可以,无论是VideoCategoryListAdapter的对象,还是CategoryListAdapter的对象,或者CategoryNameAdapter的对象。

总结

关于的责任链模式,其实日常使用的情况的还是较多,理论上满足时序,链状,优先级等特点,都可以尝试着使用责任链模式来重构一次代码。毕竟责任链能够有效的请求和处理者进行耦合。

作者:zhi184816 发表于2016/8/21 16:40:49 原文链接
阅读:18 评论:0 查看评论

深入理解CoordinatorLayout.Behavior

$
0
0

Behavior

要研究的几个问题

一、Behavior是什么?为什么要用Behavior?
二、怎么使用Behavior?
三、从源码角度看为什么要这么使用Behavior?

一、Behavior是什么?为什么要用Behavior?

CoordinatorLayout是android support design推出的新布局,主要用于作为视图根布局以及协调子控件的行为,而Behavior就是用于直接子控件来协调自身CoordinatorLayout以及和其他子控件的关系,使用Behavior的控件必须是直接从属于CoordinatorLayout。

在传统的事件分发流程中,在子控件处理事件过程中,父控件是可以进行拦截的,但一旦父控件进行拦截,那么这次事件只能由父控件处理,而不能再由子控件处理了。

在android5.0之后新的嵌套滑动机制中,引入了:NestScrollChildNestedScrollingParent两个接口,用于协调子父控件滑动状态,而CoordinatorLayout实现了NestedScrollingParent接口,在实现了NestScrollChild这个接口的子控件在滑动时会调用NestedScrollingParent接口的相关方法,将事件发给父控件,由父控件决定是否消费当前事件,在CoordinatorLayout实现的NestedScrollingParent相关方法中会调用Behavior内部的方法。

我们实现Behavior的方法,就可以嵌入整个CoordinatorLayout所构造的嵌套滑动机制中,可以获取到两个方面的内容:

1、某个view监听另一个view的状态变化,例如大小、位置、显示状态等
需要重写layoutDependsOnonDependentViewChanged方法

2、某个view监听CoordinatorLayout内NestedScrollingChild的接口实现类的滑动状态
重写onStartNestedScrollonNestedPreScroll方法。注意:是监听实现了NestedScrollingChild的接口实现类的滑动状态,这就可以解释为什么不能用ScrollView而用NestScrollView来滑动了。

二、怎么使用Behavior?

我们先看下Behavior最常见的几个方法,Behavior还有其他比如onMeasureChild、onLayoutChild等一些方法,列举的这几个方法平时还是比较常见的,知道常见方法的使用后,在研究下其他方法,思路还是相通的。

public static abstract class Behavior<V extends View> {
//指定Behavior关注的滑动方向
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout,
                V child, View directTargetChild, View target, int nestedScrollAxes) {
            return false;
        }
//用来监听滑动状态,对象消费滚动距离前回调
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, V child, View target,
                int dx, int dy, int[] consumed) {
            // TODO
        }
//确定子视图与同级视图的依赖
    @Override 
     public boolean layoutDependsOn(CoordinatorLayout parent, View 
child, View dependency) {
        return Build.VERSION.SDK_INT >= 11 && dependency instanceof Snackbar.SnackbarLayout;
}
 //依赖布局变化时调用
//If the Behavior changes the child view's size or position, 
//it should return true. The default implementation returns false
    public boolean onDependentViewChanged(CoordinatorLayout parent, V child, View dependency) {
            return false;
        }
    @Override
      public boolean onNestedFling(CoordinatorLayout coordinatorLayout, View child, View target, float velocityX, float
      velocityY, boolean consumed) {
    //快速滑动
       return super.onNestedFling(coordinatorLayout, child,target,velocityX, velocityY, consumed);
}
//所有Behavior能在子View之前收到CoordinatorLayout的所有触摸事件
    @Override
    public boolean onInterceptTouchEvent(CoordinatorLayout parent,View child, MotionEvent ev) { 
      return super.onInterceptTouchEvent(parent, child, ev);
    }
  @Override
  public boolean onTouchEvent(CoordinatorLayout parent, View child, MotionEvent ev) { 
      return super.onTouchEvent(parent, child, ev);
    }
}

1、某个view监听另一个view的状态变化

这样的效果最常见的如之后导航栏那样:

底部跟随顶部导航栏显示隐藏

前面已经说了,如果要监听另一个view的状态变化,需要重写layoutDependsOnonDependentViewChanged方法,看下具体实现:
layout:

<?xml version="1.0" encoding="utf-8"?>
<android.support.design.widget.CoordinatorLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
                                                 xmlns:app="http://schemas.android.com/apk/res-auto"
                                                 android:id="@+id/behavior_demo_coordinatorLayout"
                                                 android:layout_width="match_parent"
                                                 android:layout_height="match_parent">

    <android.support.design.widget.AppBarLayout
        android:id="@+id/appbar"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:theme="@style/AppTheme.AppBarOverlay">

        <android.support.v7.widget.Toolbar
            android:id="@+id/toolbar"
            android:layout_width="match_parent"
            android:layout_height="?attr/actionBarSize"
            app:layout_scrollFlags="scroll|enterAlways|snap"
            android:background="?attr/colorPrimary" />
    </android.support.design.widget.AppBarLayout>

        <android.support.v4.widget.NestedScrollView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            >
            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:orientation="vertical">

                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                android:layout_width="match_parent"
                android:layout_height="400dp"
                android:text="哈哈哈"
                android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>
                <TextView
                    android:layout_width="match_parent"
                    android:layout_height="400dp"
                    android:text="哈哈哈"
                    android:gravity="center"/>

            </LinearLayout>
        </android.support.v4.widget.NestedScrollView>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="?attr/actionBarSize"
        android:layout_gravity="bottom"
        android:background="@color/colorPrimary"
        android:gravity="center"
        app:aucher_id="@id/appbar"
        app:layout_behavior="com.mrzk.newstudy.behavior.MyCustomBehavior">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:textColor="#ffffff"
            android:text="底部导航栏"/>
    </LinearLayout>

</android.support.design.widget.CoordinatorLayout>

attrs:

   <declare-styleable name="MyCustomStyle">
        <attr name="anchor_id" format="integer|reference"/>
    </declare-styleable>

MyCustomBehavior.java:

public class MyCustomBehavior extends CoordinatorLayout.Behavior<View>{

    private int id;
    public MyCustomBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
        TypedArray typedArray = context.getResources().obtainAttributes(attrs, R.styleable.MyCustomStyle);
        id = typedArray.getResourceId(R.styleable.MyCustomStyle_anchor_id, -1);
        typedArray.recycle();
    }

    @Override
    public boolean layoutDependsOn(CoordinatorLayout parent, View child, View dependency) {

//        return dependency instanceof AppBarLayout;
        return dependency.getId() == id;
    }

    @Override
    public boolean onDependentViewChanged(CoordinatorLayout parent, View child, View dependency) {

        child.setTranslationY(-dependency.getTop());
        return true;
    }
}

重点关注几点:
首先,我们必须重写两个参数的构造方法,因为通过反射实例化的时候就是用的这个构造方法,在这个构造方法中我们也可以获取一些东西,比如我们的依赖控件ID。
之后layoutDependsOn方法我们来决定要依赖哪个view,如果我们知道要依赖的控件,可以直接写:

return dependency instanceof AppBarLayout

而如果我们不知道,也可以由外部传入,在构造方法中获取资源ID来进行判断,这样具有更高的灵活性:

return dependency.getId() == id

我们看下在CoordinatorLayout中两个方法的调用过程:

class OnPreDrawListener implements ViewTreeObserver.OnPreDrawListener {
        @Override
        public boolean onPreDraw() {
            dispatchOnDependentViewChanged(false);
            return true;
        }
    }

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        ...
            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
              //如果Behavior不为null,layoutDependsOn方法返回true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        // If this is not from a nested scroll and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
                  //调用onDependentViewChanged方法
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);
                 ...
            }
        }
    }

从调用上来看,在CoordinatorLayout内部的任何子view均可产生依赖关系。

2、某个view监听CoordinatorLayout内NestedScrollingChild的接口实现类的滑动状态

如前所说,重写onStartNestedScrollonNestedPreScroll方法。它可以监听实现了NestedScrollingChild的接口实现类的滑动状态

如果用WebView来滚动的,结果预期要隐藏和显示的appbar没有反应,在外层加上NestScrollView就解决了问题,这是因为WebView没有实现NestedScrollingChild接口造成的,因为滑动控件的滑动状态是通过NestedScrollingChild接口方法处理中来调用NestedScrollingParent接口方法来实现。
实现上面的效果我们还可以用重写onStartNestedScrollonNestedPreScroll来实现。来看看吧:

public class MyCustomBehavior extends CoordinatorLayout.Behavior<View>{

    private boolean isAnimate;
    public MyCustomBehavior(Context context, AttributeSet attrs) {
        super(context,attrs);
    }

    @Override
    public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes) {
        return (nestedScrollAxes & ViewCompat.SCROLL_AXIS_VERTICAL)!=-1;//判断是否为垂直滚动
    }

    @Override
    public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed) {
        //super.onNestedPreScroll(coordinatorLayout, child, target, dx, dy, consumed);

        if (dy>0 &&!isAnimate && child.getTranslationY()<child.getHeight()){
            child.setTranslationY(child.getTranslationY() + dy);
        }else if (dy<0 &&!isAnimate && child.getTranslationY()>0){
            child.setVisibility(View.VISIBLE);
            if (child.getTranslationY()+dy<0){
                child.setTranslationY(0);
            }else {
                child.setTranslationY(child.getTranslationY()+dy);
            }
        }
    }


    @Override
    public void onStopNestedScroll(CoordinatorLayout coordinatorLayout, View child, View target) {
        //super.onStopNestedScroll(coordinatorLayout, child, target);
            if (child.getTranslationY()<child.getHeight()/2){
                changeState(child,0);
            }else{
                changeState(child,child.getHeight());
            }
    }

    private void changeState(final View view, final int scrollY) {
        ViewPropertyAnimator animator = view.animate().translationY(scrollY).setInterpolator(new FastOutSlowInInterpolator()).setDuration(200*scrollY/view.getHeight());
        animator.setListener(new Animator.AnimatorListener() {
            @Override
            public void onAnimationStart(Animator animator) {
                isAnimate=true;
            }
            @Override
            public void onAnimationEnd(Animator animator) {
                if (view.getTranslationY() == view.getHeight()){
                         view.setVisibility(View.GONE);
                }
                isAnimate=false;
            }

            @Override
            public void onAnimationCancel(Animator animator) {
                view.setTranslationY(scrollY);
            }

            @Override
            public void onAnimationRepeat(Animator animator) {
            }
        });
        animator.start();
    }
}

用这个来实现的话,需要注意的是滚动控件必须实现NestedScrollingChild接口,而没有实现该接口且不调用 dispatchNestedScroll相关接口的滚动控件如ScrollView、WebView、ListView是没有作用的。

三、从源码角度看为什么要这么使用Behavior

我们从Behavior获取实例化开始看,看CoordinatorLayout.LayoutParams源码:

 LayoutParams(Context context, AttributeSet attrs) {
            super(context, attrs);

            final TypedArray a = context.obtainStyledAttributes(attrs,
                    R.styleable.CoordinatorLayout_LayoutParams);

            this.gravity = a.getInteger(
                    R.styleable.CoordinatorLayout_LayoutParams_android_layout_gravity,
                    Gravity.NO_GRAVITY);
            mAnchorId = a.getResourceId(R.styleable.CoordinatorLayout_LayoutParams_layout_anchor,
                    View.NO_ID);
            this.anchorGravity = a.getInteger(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_anchorGravity,
                    Gravity.NO_GRAVITY);

            this.keyline = a.getInteger(R.styleable.CoordinatorLayout_LayoutParams_layout_keyline,
                    -1);

            mBehaviorResolved = a.hasValue(
                    R.styleable.CoordinatorLayout_LayoutParams_layout_behavior);
            if (mBehaviorResolved) {
              //在这里解析获取Behavior
                mBehavior = parseBehavior(context, attrs, a.getString(
                        R.styleable.CoordinatorLayout_LayoutParams_layout_behavior));
            }

            a.recycle();
        }

接着来看看具体是怎么获取到Behavior的:

 static final Class<?>[] CONSTRUCTOR_PARAMS = new Class<?>[] {
            Context.class,
            AttributeSet.class
    };

static Behavior parseBehavior(Context context, AttributeSet attrs, String name) {
        if (TextUtils.isEmpty(name)) {
            return null;
        }

        final String fullName;
        if (name.startsWith(".")) {
            // Relative to the app package. Prepend the app package name.
            fullName = context.getPackageName() + name;
        } else if (name.indexOf('.') >= 0) {
            // Fully qualified package name.
            fullName = name;
        } else {
            // Assume stock behavior in this package (if we have one)
            fullName = !TextUtils.isEmpty(WIDGET_PACKAGE_NAME)
                    ? (WIDGET_PACKAGE_NAME + '.' + name)
                    : name;
        }
        try {
            Map<String, Constructor<Behavior>> constructors = sConstructors.get();
            if (constructors == null) {
                constructors = new HashMap<>();
                sConstructors.set(constructors);
            }
            Constructor<Behavior> c = constructors.get(fullName);
            if (c == null) {
                //这里通过反射获取到Behavior
                final Class<Behavior> clazz = (Class<Behavior>) Class.forName(fullName, true,
                        context.getClassLoader());
                //获取两个参数的构造方法
                c = clazz.getConstructor(CONSTRUCTOR_PARAMS);
                c.setAccessible(true);
                constructors.put(fullName, c);
            }
          //在这里实例化
            return c.newInstance(context, attrs);
        } catch (Exception e) {
            throw new RuntimeException("Could not inflate Behavior subclass " + fullName, e);
        }
    }

这里就解释了为什么我们每次继承都要写两个参数的构造方法了,如果没有,则会报Caused by: java.lang.NoSuchMethodException: [class android.content.Context, interface android.util.AttributeSet]错误。
然后我们看看主要关注的onStartNestedScroll和onNestedPreScroll的调用时机,当实现了NestScrollChild接口的子控件滑动时,会回调CoordinatorLayout中的onStartNestedScroll方法:

 public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
        boolean handled = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //获取Behavior 
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
            //true if the Behavior wishes to accept this nested scroll
            //调用viewBehavior.onStartNestedScroll方法,如果返回true表示希望接受滚动事件
                final boolean accepted = viewBehavior.onStartNestedScroll(this, view, child, target,
                        nestedScrollAxes);
                handled |= accepted;
                lp.acceptNestedScroll(accepted);
            } else {
                lp.acceptNestedScroll(false);
            }
        }
        return handled;
    }

当实现了NestScrollChild接口的子控件滚动时,在消费滚动距离之前把总的滑动距离传给父布局,即CoordinatorLayout。然后回调onNestedPreScroll方法:

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed) {
        int xConsumed = 0;
        int yConsumed = 0;
        boolean accepted = false;

        final int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            final View view = getChildAt(i);
            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
            //遍历所有子控件 如果不希望接受处理事件  跳出本次循环
            if (!lp.isNestedScrollAccepted()) {
                continue;
            }
            //获得child view的Behavior
            final Behavior viewBehavior = lp.getBehavior();
            if (viewBehavior != null) {
                mTempIntPair[0] = mTempIntPair[1] = 0;
                //调用viewBehavior.onNestedPreScroll方法
                viewBehavior.onNestedPreScroll(this, view, target, dx, dy, mTempIntPair);
                //dy大于0是向上滚动 小于0是向下滚动
                xConsumed = dx > 0 ? Math.max(xConsumed, mTempIntPair[0])
                        : Math.min(xConsumed, mTempIntPair[0]);
                yConsumed = dy > 0 ? Math.max(yConsumed, mTempIntPair[1])
                        : Math.min(yConsumed, mTempIntPair[1]);

                accepted = true;
            }
        }
        //consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离
        consumed[0] = xConsumed;
        consumed[1] = yConsumed;

        if (accepted) {
            dispatchOnDependentViewChanged(true);
        }
    }

然后我们来研究layoutDependsOn和onDependentViewChanged的调用时机,看CoordinatorLayout的dispatchOnDependentViewChanged方法:

void dispatchOnDependentViewChanged(final boolean fromNestedScroll) {
        final int layoutDirection = ViewCompat.getLayoutDirection(this);
        final int childCount = mDependencySortedChildren.size();
        for (int i = 0; i < childCount; i++) {
            final View child = mDependencySortedChildren.get(i);
            final LayoutParams lp = (LayoutParams) child.getLayoutParams();

            // Check child views before for anchor
            for (int j = 0; j < i; j++) {
                final View checkChild = mDependencySortedChildren.get(j);

                if (lp.mAnchorDirectChild == checkChild) {
                    offsetChildToAnchor(child, layoutDirection);
                }
            }

            // Did it change? if not continue
            final Rect oldRect = mTempRect1;
            final Rect newRect = mTempRect2;
            getLastChildRect(child, oldRect);
            getChildRect(child, true, newRect);
            if (oldRect.equals(newRect)) {
                continue;
            }
            recordLastChildRect(child, newRect);

            // Update any behavior-dependent views for the change
            for (int j = i + 1; j < childCount; j++) {
                final View checkChild = mDependencySortedChildren.get(j);
                final LayoutParams checkLp = (LayoutParams) checkChild.getLayoutParams();
                final Behavior b = checkLp.getBehavior();
                //behavior不为null同时layoutDependsOn返回了true
                if (b != null && b.layoutDependsOn(this, checkChild, child)) {
                    if (!fromNestedScroll && checkLp.getChangedAfterNestedScroll()) {
                        // If this is not from a nested scroll and we have already been changed
                        // from a nested scroll, skip the dispatch and reset the flag
                        checkLp.resetChangedAfterNestedScroll();
                        continue;
                    }
                    //this:CoordinatorLayout
                    //checkChild:behavior所属的view
                    //child:依赖的view
                    //true if the Behavior changed the child view's size or position, false otherwise
                    final boolean handled = b.onDependentViewChanged(this, checkChild, child);

                    if (fromNestedScroll) {
                        // If this is from a nested scroll, set the flag so that we may skip
                        // any resulting onPreDraw dispatch (if needed)
                        checkLp.setChangedAfterNestedScroll(handled);
                    }
                }
            }
        }
    }

这段代码在onNestedScroll、onNestedPreScroll、onNestedFling和OnPreDrawListener.onPreDraw方法中都有调用,判断依赖控件大小或者位置变化时及时通知behavior,子控件作出相应调整。
这里把我们主要关心的控件的调用时机大体走读了一遍,对于为什么在behavior中调用相关方法可以依赖和监听其他控件的滑动事件应该有了一定认识,如果关注CoordinatorLayout的实现细节,务必要搞明白NestScrollChild和NestedScrollingParent机制的调用关系,建议查看NestScrollView源码,这里给出NestScrollChild和NestedScrollingParent的一些主要方法说明,对其具体了解还可以看Android 嵌套滑动机制(NestedScrolling)这篇文章。

NestScrollChild

public void setNestedScrollingEnabled(boolean enabled)
enabled:true表示view使用嵌套滚动,false表示禁用

public boolean startNestedScroll(int axes)
axes:表示滚动的方向如:ViewCompat.SCROLL_AXIS_VERTICAL(垂直方向滚动)和 ViewCompat.SCROLL_AXIS_HORIZONTAL(水平方向滚动)
return:true表示本次滚动支持嵌套滚动,false不支持
startNestedScroll表示view开始滚动了,一般是在ACTION_DOWN中调用,如果返回true则表示父布局支持嵌套滚动

public void stopNestedScroll()
在事件结束比如ACTION_UP或者ACTION_CANCLE中调用stopNestedScroll,告诉父布局滚动结束

public boolean dispatchNestedScroll(int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow)
dxConsumed: 表示view消费了x方向的距离长度
dyConsumed: 表示view消费了y方向的距离长度
dxUnconsumed: 表示滚动产生的x滚动距离还剩下多少没有消费>dyUnconsumed: 表示滚动产生的y滚动距离还剩下多少没有消费
offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
在view消费滚动距离之后,把剩下的滑动距离再次传给父布局

public boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow)
dx: 表示view本次x方向的滚动的总距离长度
dy: 表示view本次y方向的滚动的总距离长度
consumed: 表示父布局消费的距离,consumed[0]表示x方向,consumed[1]表示y方向
参数offsetInWindow: 表示剩下的距离dxUnconsumed和dyUnconsumed使得view在父布局中的位置偏移了多少
view消费滚动距离之前把总的滑动距离传给父布局

* public boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed)*
velocityX:X方向滚动的距离
velocityY:Y方向滚动的距离
consumed:父布局是否消费

public boolean dispatchNestedPreFling(float velocityX, float velocityY)
velocityX:X方向滚动的距离
velocityY:Y方向滚动的距离

NestedScrollingParent

public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes)
child:ViewParent包含触发嵌套滚动的view的对象
target:触发嵌套滚动的view (在这里如果不涉及多层嵌套的话,child和target)是相同的
nestedScrollAxes:就是嵌套滚动的滚动方向了.
当子view的调用NestedScrollingChild的方法startNestedScroll时,会调用该方法
该方法决定了当前控件是否能接收到其内部View(并非是直接子View)滑动时的参数

public void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
如果onStartNestedScroll方法返回true,之后就会调用该方法.它是让嵌套滚动在开始滚动之前,让布局容器(viewGroup)或者它的父类执行一些配置的初始化(React to the successful claiming of a nested scroll operation)

public void onStopNestedScroll(View target)
当子view调用stopNestedScroll时会调用该方法,停止滚动

public void onNestedScroll(View target, int dxConsumed, int dyConsumed, int dxUnconsumed, int dyUnconsumed)
target:同上
dxConsumed:表示target已经消费的x方向的距离
dyConsumed:表示target已经消费的x方向的距离
dxUnconsumed:表示x方向剩下的滑动距离
dyUnconsumed:表示y方向剩下的滑动距离
当子view调用dispatchNestedScroll方法时,会调用该方法

public void onNestedPreScroll(View target, int dx, int dy, int[] consumed)
target:同上
dx:表示target本次滚动产生的x方向的滚动总距离
dy:表示target本次滚动产生的y方向的滚动总距离
consumed:表示父布局要消费的滚动距离,consumed[0]和consumed[1]分别表示父布局在x和y方向上消费的距离.
当子view调用dispatchNestedPreScroll方法是,会调用该方法

调用时机:

子view 父view
startNestedScroll onStartNestedScroll、onNestedScrollAccepted
dispatchNestedPreScroll onNestedPreScroll
dispatchNestedScroll onNestedScroll
stopNestedScroll onStopNestedScroll
作者:zhangke3016 发表于2016/8/21 17:13:29 原文链接
阅读:79 评论:0 查看评论

《React-Native系列》20、 RN数据流之Flux概览

$
0
0

今天我们来看下ReactNative的数据流框架Flux。

Flux是Facebook用来构建用户端的web应用的应用程序体系架构。它通过利用数据的单向流动为React的可复用的视图组件提供了补充。相比于形式化的框架它更像是一个架构思想,不需要太多新的代码你就可以马上使用Flux构建你的应用。


React 标榜自己是 MVC 里面 V 的部分,那么 Flux 就相当于添加 M 和 C 的部分。


一个 Flux 应用主要包含四个部分:
dispatcher
处理动作分发,维护 Store 之间的依赖关系

stores
数据和逻辑部分

views
React 组件,这一层可以看作 controller-views,作为视图同时响应用户交互

actions
提供给 dispatcher 传递数据给 store


在Flux应用中数据是单向流动的:


单向的数据流是Flux应用的核心特性,上图应该成为Flux程序员的主要心智模型。Dispatcher,stores和views是拥有清晰的输入输出的独立节点。而actions是包含了新的数据和身份属性的简单对象。

更多时候 View 会通过用户交互触发 Action,所以一个简单完整的数据流类似这样:


整个流程如下:
首先要有 action,通过定义一些 action creator 方法根据需要创建 Action 提供给 dispatcher
View 层通过用户交互(比如 onClick)会触发 Action
Dispatcher 会分发触发的 Action 给所有注册的 Store 的回调函数
Store 回调函数根据接收的 Action 更新自身数据之后会触发一个 change 事件通知 View 数据更改了
View 会监听这个 change 事件,拿到对应的新数据并调用 setState 更新组件 UI


所有的状态都由 Store 来维护,通过 Action 传递数据,构成了如上所述的单向数据流循环,所以应用中的各部分分工就相当明确,高度解耦了。这种单向数据流使得整个系统都是透明可预测的。

下篇文章,我们结合Demo来分析下Flux。


参考:https://hulufei.gitbooks.io/react-tutorial/content/flux.html

           http://reactjs.cn/react/docs/flux-overview.html

作者:hsbirenjie 发表于2016/8/21 17:21:31 原文链接
阅读:69 评论:0 查看评论

【Android】让HeaderView也参与回收复用机制,自我感觉是优雅的为 RecyclerView 添加 HeaderView (FooterView)的解决方案

$
0
0

本文站在巨人的肩膀上 自我感觉又进了一步而成。

基于翔神的大作基础之上写的一个为RecyclerView添加HeaderView FooterView 的另一种解决方案, 

翔神链接文首镇楼:http://blog.csdn.net/lmj623565791/article/details/51854533 

上次翔神发表这篇文章时,我就提了个问题:说headerView和FooterView都是强引用在Adapter中的,这样即使他所属的ViewHolder被回收复用,但是View本身的实例还是在被强引用,内存空间也无法释放的。 这样做虽然速度没任何问题,(甚至还有小小提升,但是HeaderView过大内存空间就会吃紧了吧) 所以我想了好久 改写了一下,换了种思路,给RecyclerView提供数据和布局,UI的创建 和 数据的绑定分开来做,都交由Adapter维护。

墙裂建议大家先阅读翔神文章后 再立刻阅读此文,威力翻倍。这样对本文使用到的一些吊炸天的东西就不会陌生了,例如通用的CommonAdapter和ViewHolder。

敲黑板,如果只是伸手党,建议直接看 【2 使用方法】,并直接到文末下载链接里的工程,拷贝recyclerview包下的几个文件即可使用。

工程里已经参考解决,HeaderView适配GridLayoutManager 和StaggeredGridLayoutManager。

========================================================================

【1 引言】

众所周知,RecyclerView已经是主流,ListView已经成为过去式,而两者之间有些许的不同,其中比较重要的一点就是ListView自带addHeaderView,addFooterView方法,而RecyclerView并没有提供。So,我们开发者要自己想办法实现这个功能。

市面上大多为RecyclerView添加HeaderView的方案,都是在使用RecyclerView的类中(Activity Fragment)里构建一个View,并绑定好数据,然后通过XXXAdapter提供的addHeaderView方法,将这个View set进Adapter里。

Adapter内部使用ArrayList、或者翔神使用的是SparseArray存储这个View,并为HeaderView FooterView分配不同的itemViewType,然后Adapter在onCreateViewHolder和onBindViewHolder方法里,根据ViewType的不同来判断这是HeaderView 还是普通item。

这种方法目前为止我只发现一个弊端(也是本文改进的地方),就是这个HeaderView由于在Adapter里是被ArrayList、SparseArray强引用的,就算其所属的RecyclerView.ViewHolder被回收服用了,但是这个View会因为被ArrayList等强引用着,依然停留在内存中。所以该HeaderView并没有被回收,只是被复用了而普通的item都只有数据和layoutId被保存在Adapter中,并没有View的实例。

一般情况下 这并没有任何问题,因为普通项目的HeaderView也不大,但是若HeaderView过于庞大,(就像我司的项目,动辄HeaderView就三个屏幕长度,三屏之后才是普通的item),在这个页面已经往下滑了很多距离,浏览了很多内容,HeaderView早已不可见,此时按照RecyclerView的思路,这个庞大的HeaderView所属的VIewHolder应该被系统回收复用,以腾出空间,ok,那么RecyclerView做了它该做的事,回收了这个HeaderView寄身的VIewHolder给其他类型的Item使用了,可惜上文提到,此时HeaderView被强引用住,被回收复用的只是其所属的那个ViewHolder,这个庞大的VIew所占的内存空间依然没有被释放。

其实我们仔细想一想,RecyclerView Adapter里是不保存View对象的,它保存的只是数据和layout,而我们也应该遵循此原则 为其添加HeaderView(FooterView)。

(题外话,和ListView相比,RecyclerView更是进一步的 将 UI的创建 和数据的绑定 分成了两步,(oncreateViewHolder,onBindViewHolder))


敲黑板,本文就参考翔神的装饰者模式,为RecyclerView 添加 HeaderView(FooterView),并且将HeaderView的UI创建,和数据绑定强制分开,令HeaderView实例在Adapter中不再被强引用,让HeaderView和普通的ItemView没有两样~。

先上预览动图:



========================================================================

【2 使用方法】

//HeaderView使用方法小窥: 以下为Rv添加两个HeaderView
mHeaderAdapter = new HeaderRecyclerAndFooterWrapperAdapter(mAdapter) {
    @Override
    protected void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o) {
        switch (layoutId) {
            case R.layout.item_header_1:
                TestHeader1 header1 = (TestHeader1) o;
                holder.setText(R.id.tv, header1.getText());
                break;
            case R.layout.item_header_2:
                TestHeader2 header2 = (TestHeader2) o;
                holder.setText(R.id.tv1, header2.getTxt1());
                holder.setText(R.id.tv2, header2.getTxt2());
                break;
            default:
                break;
        }
    }
};
mHeaderAdapter.addHeaderView(R.layout.item_header_1,new TestHeader1("第一个HeaderView"));
mHeaderAdapter.addHeaderView(R.layout.item_header_2,new TestHeader2("第二个","HeaderView"));
mRv.setAdapter(mHeaderAdapter);
粗略这么一看,我擦 什么辣鸡,比翔神那个真是差十万八千里,人家只要4行代码就加一个HeaderView,而且还不用实现父类Adapter的方法,你这还要switch case 看起来就一坨好麻烦的样子,走了走了。

客官留步留步,如果客官有这种想法,先冷静一下,里听我港。

这个写法猛地看起来是略复杂了一些,但是它强制的让我们将UI的创建和数据的绑定分开了,我们重写的onBindHeaderHolder()方法,就是数据的绑定过程, 试想一下,基本上每个带HeaderView的页面都有下拉刷新功能,如果你使用传统方法添加HeaderView,那么你必须要持有HeaderView的引用才能在数据刷新时改变头部数据,而且那些烦人的set方法一样是要写一遍,你可能需要将 写在Activity(Fragment)里的 创建HeaderView时的set数据方法抽成一个函数,再调用一遍。所以工作量是一点没减少的。

所以我们这种做法,你的工作量也是一点没增加滴!反而还是方便滴!优雅滴!

(躲开丢过来的鸡蛋)废话不多说,用法已经看到,下面看我们是怎么实现的。 如果伸手党看到这里觉得已经够了,那么就可以去文末直接下载源码copy使用了,里面使用的几个类版权大多归翔神所有。

========================================================================

【三,实现】

直接贴出核心代码:

private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值
//存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data,
// 在createViewHOlder里根据layoutId创建UI,
// 在onbindViewHOlder里依据这个data渲染UI,同时也将layoutId回传出去用于判断何种Header
private SparseArrayCompat<SparseArrayCompat> mHeaderDatas = new SparseArrayCompat<SparseArrayCompat>();
@Override
public int getItemViewType(int position) {
    if (isHeaderViewPos(position)) {
        return mHeaderDatas.keyAt(position);
    }
    return super.getItemViewType(position - getHeaderViewCount());
}

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

    if (mHeaderDatas.get(viewType) != null) {//不为空,说明是headerview
        return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1);
    } 
    return mInnerAdapter.onCreateViewHolder(parent, viewType);
}

protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回传一个layoutId出去,用于判断是第几个headerview

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
    if (isHeaderViewPos(position)) {
        int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0);
        onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId));
        return;
    }
    //举例子,2个header,0 1是头,2是开始,2-2 = 0
    mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
}
/**
 * 添加HeaderView
 *
 * @param layoutId headerView 的LayoutId
 * @param data     headerView 的data(可能多种不同类型的header 只能用Object了)
 */
public void addHeaderView(int layoutId, Object data) {
    //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);
    SparseArrayCompat headerContainer = new SparseArrayCompat();
    headerContainer.put(layoutId, data);
    mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);
}
首先,定义一个很大的int,作为HeaderView的viewType的基准值(我这里是100W),


然后定义一个SparseArray<SparseArray> ,在这个二维的SparseArray中,

外层的SparseArray的Key用来存放HeaderView的ViewType,value存放的是这个ViewType对应的HeaderView的布局layoutId 和 数据data,

即内层SparseArray的key是layoutId,value是Object类型的数据data(因为每个HeaderView的数据类型不同,所以这里我只能想到用Obejct类型)。


在getItemViewType()方法中,我们先根据postion判断是否是HeaderView,如果是,那么返回该HeaderView的viewtype(SparseArray的key)。

在onCreateViewHolder()方法里,根据ViewType判断是否是HeaderView,如果是HeaderView ,那么创建一个该HeaderView的ViewHolder(我这里使用的是翔神的通用ViewHolder)。

在onBindViewHolder()方法中,先根据postion判断是否是HeaderView,如果是,先从mHeaderDatas里取出这个HeaderView的layoutId,并将这个HeaderView的数据也一并取出,回调一个 abstract  的 onBindHeaderHolder()的方法,将这些参数都传入,交由子类去自由处理。 子类在这个方法里 完成数据的绑定即可。

========================================================================

【四,完整代码】

这份代码FooterView并没有用此方法实现,是“强引用VIew方法实现的”。

理由:

1 因为FooterView往往是一个LoadMore相关的提示控件,内存占用很有限。

2 LoadMore相关提示的控件 是需要强引用在Fragment Activity 或者相关类中,即使我在Adapter类里将其引用释放,这个View在内存的空间依然是无法被释放的。

3 两种实现方法都放上来,大家可以根据本文描述的方法,自行尝试将FooterView也改写,可以和我讨论,稍后我也会附加上我修改的版本。

package com.example.headerrv.recyclerview;

import android.support.v4.util.SparseArrayCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.StaggeredGridLayoutManager;
import android.view.View;
import android.view.ViewGroup;

/**
 * 介绍:一个给RecyclerView添加HeaderView FooterView的装饰Adapter类
 * 重点哦~ RecyclerView的HeaderView将可以被系统回收,不像老版的HeaderView是一个强引用在内存里
 * 作者:zhangxutong
 * 邮箱:zhangxutong@imcoming.com
 * 时间: 2016/8/2.
 */
public abstract class HeaderRecyclerAndFooterWrapperAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
    private static final int BASE_ITEM_TYPE_HEADER = 1000000;//headerview的viewtype基准值
    private static final int BASE_ITEM_TYPE_FOOTER = 2000000;//footerView的ViewType基准值

    //存放HeaderViews的layoudID和data,key是viewType,value 是 layoudID和data,
    // 在createViewHOlder里根据layoutId创建UI,
    // 在onbindViewHOlder里依据这个data渲染UI,同时也将layoutId回传出去用于判断何种Header
    private SparseArrayCompat<SparseArrayCompat> mHeaderDatas = new SparseArrayCompat<SparseArrayCompat>();
    private SparseArrayCompat<View> mFooterViews = new SparseArrayCompat<>();//存放FooterViews,key是viewType

    protected RecyclerView.Adapter mInnerAdapter;//内部的的普通Adapter

    public HeaderRecyclerAndFooterWrapperAdapter(RecyclerView.Adapter mInnerAdapter) {
        this.mInnerAdapter = mInnerAdapter;
    }

    public int getHeaderViewCount() {
        return mHeaderDatas.size();
    }

    public int getFooterViewCount() {
        return mFooterViews.size();
    }

    private int getInnerItemCount() {
        return mInnerAdapter != null ? mInnerAdapter.getItemCount() : 0;
    }

    /**
     * 传入position 判断是否是headerview
     *
     * @param position
     * @return
     */
    public boolean isHeaderViewPos(int position) {// 举例, 2 个头,pos 0 1,true, 2+ false
        return getHeaderViewCount() > position;
    }

    /**
     * 传入postion判断是否是footerview
     *
     * @param position
     * @return
     */
    public boolean isFooterViewPos(int position) {//举例, 2个头,2个inner,pos 0 1 2 3 ,false,4+true
        return position >= getHeaderViewCount() + getInnerItemCount();
    }

    /**
     * 添加HeaderView
     *
     * @param layoutId headerView 的LayoutId
     * @param data     headerView 的data(可能多种不同类型的header 只能用Object了)
     */
    public void addHeaderView(int layoutId, Object data) {
        //mHeaderViews.put(mHeaderViews.size() + BASE_ITEM_TYPE_HEADER, v);
        SparseArrayCompat headerContainer = new SparseArrayCompat();
        headerContainer.put(layoutId, data);
        mHeaderDatas.put(mHeaderDatas.size() + BASE_ITEM_TYPE_HEADER, headerContainer);
    }

    /**
     * 设置(更新)某个layoutId的HeaderView的数据
     *
     * @param layoutId
     * @param data
     */
    public void setHeaderView(int layoutId, Object data) {
        boolean isFinded = false;
        for (int i = 0; i < mHeaderDatas.size(); i++) {
            SparseArrayCompat sparse = mHeaderDatas.valueAt(i);
            if (layoutId == sparse.keyAt(0)) {
                sparse.setValueAt(0, data);
                isFinded = true;
            }
        }
        if (!isFinded) {//没发现 说明是addHeaderView
            addHeaderView(layoutId, data);
        }
    }


    /**
     * 设置某个位置的HeaderView
     *
     * @param headerPos 从0开始,如果pos过大 就是addHeaderview
     * @param layoutId
     * @param data
     */
    public void setHeaderView(int headerPos, int layoutId, Object data) {
        if (mHeaderDatas.size() > headerPos) {
            SparseArrayCompat headerContainer = new SparseArrayCompat();
            headerContainer.put(layoutId, data);
            mHeaderDatas.setValueAt(headerPos, headerContainer);
        } else if (mHeaderDatas.size() == headerPos) {//调用addHeaderView
            addHeaderView(layoutId, data);
        } else {
            //
            addHeaderView(layoutId, data);
        }
    }

    /**
     * 添加FooterView
     *
     * @param v
     */
    public void addFooterView(View v) {
        mFooterViews.put(mFooterViews.size() + BASE_ITEM_TYPE_FOOTER, v);
    }

    /**
     * 清空HeaderView数据
     */
    public void clearHeaderView() {
        mHeaderDatas.clear();
    }

    public void clearFooterView() {
        mFooterViews.clear();
    }


    public void setFooterView(View v) {
        clearFooterView();
        addFooterView(v);
    }

    @Override
    public int getItemViewType(int position) {
        if (isHeaderViewPos(position)) {
            return mHeaderDatas.keyAt(position);
        } else if (isFooterViewPos(position)) {//举例:header 2, innter 2, 0123都不是,4才是,4-2-2 = 0,ok。
            return mFooterViews.keyAt(position - getHeaderViewCount() - getInnerItemCount());
        }
        return super.getItemViewType(position - getHeaderViewCount());
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {

        if (mHeaderDatas.get(viewType) != null) {//不为空,说明是headerview
            //return new ViewHolder(parent.getContext(), mHeaderViews.get(viewType));
            //return createHeader(parent, mHeaderViews.indexOfKey(viewType)); 第一种方法是让子类实现这个方法 构建ViewHolder
            return ViewHolder.get(parent.getContext(), null, parent, mHeaderDatas.get(viewType).keyAt(0), -1);
        } else if (mFooterViews.get(viewType) != null) {//不为空,说明是footerview
            return new ViewHolder(parent.getContext(), mFooterViews.get(viewType));
        }
        return mInnerAdapter.onCreateViewHolder(parent, viewType);
    }

    //protected abstract RecyclerView.ViewHolder createHeader(ViewGroup parent, int headerPos);

    protected abstract void onBindHeaderHolder(ViewHolder holder, int headerPos, int layoutId, Object o);//多回传一个layoutId出去,用于判断是第几个headerview

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (isHeaderViewPos(position)) {
            int layoutId = mHeaderDatas.get(getItemViewType(position)).keyAt(0);
            onBindHeaderHolder((ViewHolder) holder, position, layoutId, mHeaderDatas.get(getItemViewType(position)).get(layoutId));
            return;
        } else if (isFooterViewPos(position)) {
            return;
        }
        //举例子,2个header,0 1是头,2是开始,2-2 = 0
        mInnerAdapter.onBindViewHolder(holder, position - getHeaderViewCount());
    }


    @Override
    public int getItemCount() {
        return getInnerItemCount() + getHeaderViewCount() + getFooterViewCount();
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView) {
        mInnerAdapter.onAttachedToRecyclerView(recyclerView);
        //为了兼容GridLayout
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        if (layoutManager instanceof GridLayoutManager) {
            final GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager;
            final GridLayoutManager.SpanSizeLookup spanSizeLookup = gridLayoutManager.getSpanSizeLookup();

            gridLayoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
                @Override
                public int getSpanSize(int position) {
                    int viewType = getItemViewType(position);
                    if (mHeaderDatas.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    } else if (mFooterViews.get(viewType) != null) {
                        return gridLayoutManager.getSpanCount();
                    }
                    if (spanSizeLookup != null)
                        return spanSizeLookup.getSpanSize(position);
                    return 1;
                }
            });
            gridLayoutManager.setSpanCount(gridLayoutManager.getSpanCount());
        }

    }

    @Override
    public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) {
        mInnerAdapter.onViewAttachedToWindow(holder);
        int position = holder.getLayoutPosition();
        if (isHeaderViewPos(position) || isFooterViewPos(position)) {
            ViewGroup.LayoutParams lp = holder.itemView.getLayoutParams();

            if (lp != null
                    && lp instanceof StaggeredGridLayoutManager.LayoutParams) {

                StaggeredGridLayoutManager.LayoutParams p =
                        (StaggeredGridLayoutManager.LayoutParams) lp;

                p.setFullSpan(true);
            }
        }
    }
}


========================================================================

源码链接:http://download.csdn.net/detail/zxt0601/9608941




作者:zxt0601 发表于2016/8/21 17:22:01 原文链接
阅读:33 评论:0 查看评论

Android动画

$
0
0

一、简单介绍

Android动画主要有4种:

Tween Animation 变换动画

Frame Animation 帧动画

Layout Animation 布局动画

Property Animation 属性动画

二、变换动画

变换动画有以下四种:

Alpha:渐变透明度动画

Scale:渐变尺寸缩放动画

Translate:位置移动动画

Rotate:旋转动画

它们的共同属性:

(1)Duration:动画持续时间(单位:毫秒)

(2)fillAfter:设置为true,动画转化在动画结束后被应用

(3)fillBefore:设置为true,动画转化在动画开始前被应用

(4)interpolator:动画插入器(加速、减速插入器)

(5)repeatCount:动画重复次数

(6)repeatMode:顺序重复/倒序重复

(7)startOffset:动画之间的时间间隔

实现方式

(1)配置文件(/res/anim)—— alpha scale translate rotate  

特点:简单

Animation alpha = new AlphaAnimation(0.1f,1.0f);//设置动画透明度从10%~100%
alpha.setDuration(5000);//设置动画时间为5秒
img.startAnimation(alpha);

(2)JAVA代码实现 —— AlphaAnimation ScaleAnimation TranslateAnimation RotateAnimation  

特点:灵活

Animation scale = AnimationUtils.loadAnimation(TweenActivity.this,R.anim.scale_anim);
img.startAnimation(scale);

四种动画基本实现

AlphaAnimation(透明度动画)

(1)fromAlpha:动画起始时的透明度

(2)toAlpha:动画终止时的透明度

0.0表示完全透明,1.0表示完全不透明

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <alpha
        android:duration="1000"
        android:fromAlpha="0.1"
        android:toAlpha="1.0" >
    </alpha>

</set>

ScaleAnimation(缩放动画)

(1)formX,toX:分别是起始和结束时X坐标上的伸缩尺寸

(2)fromY,toY:分别是起始和结束时Y坐标上的伸缩尺寸

(3)pivotX,pivotY:分别是伸缩动画相对于x,y坐标开始的位置

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <scale
        android:duration="2000"
        android:fillAfter="false"
        android:fromXScale="0.0"
        android:fromYScale="0.0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toXScale="1.0"
        android:toYScale="1.0" />

</set>
TranslateAnimation(位移动画)

(1)fromXDelta,fromYDelta:分别是起始时X、Y的坐标

(2)toXDelta,toYDelta:分别是结束时X、Y的坐标

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <translate
        android:duration="1000"
        android:fromXDelta="10"
        android:fromYDelta="10"
        android:toXDelta="100"
        android:toYDelta="100" />

</set>

RotateAnimation(旋转动画)

(1)fromDegrees:起始的角度

(2)toDegrees:终止的角度

(3)pivotX,pivotY:分别为旋转动画相对于x,y的坐标开始位置

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <rotate
        android:duration="1000"
        android:fromDegrees="0"
        android:interpolator="@android:anim/accelerate_decelerate_interpolator"
        android:pivotX="50%"
        android:pivotY="50%"
        android:toDegrees="+360" />

</set>

组合动画

1)续播

实现方法一:分别两个动画A和B,先播放动画A,设置A的AnimationListener。当onAnimationEnd触发(即A播放完毕)时,开始播放B

loadAnimation = AnimationUtils.loadAnimation(this, R.anim.translate);
image.startAnimation(loadAnimation);
final Animation loadAnimation2 = AnimationUtils.loadAnimation(this,R.anim.rotate);
loadAnimation.setAnimationListener(new AnimationListener() {
	@Override
	public void onAnimationStart(Animation arg0) {
		// TODO Auto-generated method stub
	}
	@Override
	public void onAnimationRepeat(Animation arg0) {
		// TODO Auto-generated method stub
	}
	@Override
	public void onAnimationEnd(Animation arg0) {
		// TODO Auto-generated method stub
		image.startAnimation(loadAnimation2);
	}
});
实现方法二:写一个动画集AnimationSet,在其中定义动画A和B,为动画B设置startOffset,其值就是前一个动画播放的所需时间

<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android" >

    <alpha
        android:duration="3000"
        android:fromAlpha="0.2"
        android:toAlpha="1.0" />
    <alpha
        android:duration="3000"
        android:fromAlpha="1.0"
        android:startOffset="3000"
        android:toAlpha="0.2" />

</set>

2)重复

利用Animation的setRepeatCount、setRepeatMode来实现动画循环

AlphaAnimation alphaAnimation = new AlphaAnimation(0.1f, 1.0f);
alphaAnimation.setDuration(100);
alphaAnimation.setRepeatCount(10);
//倒序重复REVERSE  正序重复RESTART
alphaAnimation.setRepeatMode(Animation.REVERSE);
image.startAnimation(alphaAnimation);

3)Activity切换

使用overridePendingTransition方法

参数:第二个activity进入动画、第一个activity退出时的动画

Intent intent=new Intent(MainActivity.this,MainActivity2.class);
startActivity(intent);
overridePendingTransition(R.anim.zoom_in,R.anim.zoom_out);

三、逐帧动画

使用animation-list标签来分组一个item标签集合,定义要显示的图片,制定显示它的时间(以毫秒为单位)。

<?xml version="1.0" encoding="utf-8"?>
<animation-list xmlns:android="http://schemas.android.com/apk/res/android" >

    <item
        android:drawable="@drawable/one"
        android:duration="500"/>
    <item
        android:drawable="@drawable/two"
        android:duration="500"/>
    <item
        android:drawable="@drawable/three"
        android:duration="500"/>
    <item
        android:drawable="@drawable/four"
        android:duration="500"/>
    <item
        android:drawable="@drawable/five"
        android:duration="500"/>
    <item
        android:drawable="@drawable/six"
        android:duration="500"/>

</animation-list>

image.setImageResource(R.drawable.anim_list);

四、布局动画

为View Group添加动画,使用LayoutAnimationController

public class ListActivity extends Activity{
	
	private ListView listView;
	@Override
	protected void onCreate(Bundle savedInstanceState) {
		// TODO Auto-generated method stub
		super.onCreate(savedInstanceState);
		setContentView(R.layout.list_layout);
		listView=(ListView) findViewById(R.id.listView);
		List<String>list=new ArrayList<String>();
		for(int i=0;i<20;i++){
			list.add("liyue"+i);
		}
		ArrayAdapter<String> adapter=new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, list);
	    listView.setAdapter(adapter);
	    LayoutAnimationController lac=new LayoutAnimationController(AnimationUtils.loadAnimation(this, R.anim.zoom_in));
	    lac.setOrder(LayoutAnimationController.ORDER_NORMAL);
	    listView.setLayoutAnimation(lac);
	    listView.startLayoutAnimation();
	}

}

五、属性动画

其他动画不适合做具有交互的效果,只能做显示性的效果。例如:有个一个按钮一个图标,点击图标会显示Toast,点击按钮图标会从左到右移动。使用变换动画中的Translate完成位移。发现位移后点击图标并不会显示Toast,但是在初始位置点击会产生Toast。传统动画Animation是通过重绘来实现的十分耗费资源,而属性动画Animator是通过set和get方法来改变对象的属性从而来实现动画效果的。Android 3.0之后添加的属性动画框架。

 待续……


作者:liyue199512 发表于2016/8/21 18:02:06 原文链接
阅读:30 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>