Rocky_ruan

Rocky_ruan 查看完整档案

杭州编辑河南财经政法大学  |  计算机科学与技术 编辑  |  填写所在公司/组织填写个人主网站
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 个人简介什么都没有

个人动态

Rocky_ruan 发布了文章 · 2月23日

Android的ripple,Space,merge,include,ViewStub标签的使用

Android的ripple,Space,merge,include,ViewStub标签的使用

1.Android5.0 ripple标签
在Android 5.0后加入ripple标签,使用这个Drawable做控件的背景,在点击的时候就可以达到波浪效果。
ripple标签对应是一个rippleDrawable,当使用它作为背景的时候,在控件按下去的时候,就是显示水波效果。
在res目录下的drawable目录下创建ripple标签
image.png
ripple主要有两种形式
1.没有边界的ripple
这种没有边界的ripple只需要设置 ripple color属性就行了,不用给他添加item

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
 android:color="@color/bg_press">
</ripple>

2.有边界的ripple
这种有边界的需要给ripple添加一个item,item可以是图片,纯颜色,shape,selector.

<ripple xmlns:android="http://schemas.android.com/apk/res/android"
 android:color="@color/bg_press">
 <!--Android 5.0加入的,点击时候可以达到波浪效果-->
 <item android:drawable="@drawable/bg_my_right" />//这是一个shape
</ripple>

注意:如果想控件在不被点击的时候背景不显示,这个时候需要给item设置id=@android:id/mask 否则控件的背景就是 item 的资源了
在 5.0 之前 forground = ?attr/selectableItemBackground 可以实现波纹效果
2.Android的Space标签
Android Space标签是Android4.0添加,是一个轻量级的View,一般用于分割组件,布局或者在组件布局之间产生间隔。

<Space
 android:layout_width="0dp"
 android:layout_height="0dp"
 android:layout_weight="1" />

3.Android的include标签
创建一个include_test布局文件

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent"
 android:orientation="vertical">
 <TextView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="我是include标签"/>
</LinearLayout>

使用include标签引入布局文件达到共享相同布局文件

<include
 android:id="@+id/my_include"
 layout="@layout/include_test"//通过layout来引入布局文件
 android:layout_width="wrap_content"
 android:layout_height="wrap_content" />

4.Android的merge标签
1.merge标签可用于减少视图层级来优化布局可以配合include使用
2.<merge/>只可以作为xml layout的根节点,创建一个merge_test布局

<merge xmlns:android="http://schemas.android.com/apk/res/android">
 <TextView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="我是merge标签"/>
</merge>

3..当需要inflate的xml layout本身是由merge作为根节点的话,需要将被导入的xml layout置于viewGroup中,同时需要设置attachToRoot为True。


 // 必须attachToRoot为true
 View view = LayoutInflater.from(context).inflate(R.layout.merge_test, this, true);

3.缺点:跟布局merge不能设置padding(设置的padding不起作用),可以把设置在跟布局的padding设置到代码;不可以设置固定值的宽高

<merge xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="280dp"
    android:paddingLeft="12dp"
    android:paddingRight="12dp"
    android:paddingTop="10dp"
    android:paddingBottom="10dp">
    //layout_height="280dp"和:paddingLeft="12dp"这些都不会起作用,需要在代码中设置;

5.Android的ViewStub标签
ViewStub标签按需加载,顾名思议需要的时候再去加载,不需要的时候可以不用加载,节约内存使用
通常情况下会使用setVisibility方法来控制视图的显示和隐藏,但是这种情况视图已经加载了。
创建一个stub_test布局

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
 android:layout_width="match_parent"
 android:layout_height="match_parent">
 <TextView android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:text="我是ViewStub标签"/>
</LinearLayout>

使用ViewStub标签加载stub_test布局
静态界面没有显示只有调用才会显示

<ViewStub
 android:id="@+id/viewstub"
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 android:layout="@layout/stub_test"/>

注意:ViewStub的inflate()方法只能被调用一次,一旦调用后,ViewStub将从视图中移除,被对应的layout布局取代,同时会保留ViewStub上设置的属性效果

ViewStub viewstub = findViewById(R.id.viewstub);
viewstub.inflate();
查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 2月20日

Android四大组件详解之ContentProvider内容提供者

Android四大组件详解--ContentProvider内容提供者

contentProvider作用是进程间进行数据交互&共享,即跨进程通信
image.png
ContentProvider的跨进程通信底层原理=Android中的Binder机制来实现
,想要了解ContentProvider需要了解URI(统一资源标识符)
一:统一资源标识符(URI)
作用:唯一标识ContentProvider&其中的数据
URI 是统一资源标识符,而 URL 是统一资源定位符。因此,笼统地说,每个 URL 都是 URI,但不一定每个 URI 都是 URL。这是因为 URI 还包括一个子类,即统一资源名称 (URN),它命名资源但不指定如何定位资源。上面的 mailto、news 和 isbn URI 都是 URN 的示例。
外界进程通过URI找到对应的ContentProvider&其中的数据,再进行数据操作
具体使用
URI分为系统预置&自定义,分别对应系统内置的数据(如通讯录,日程表等)和自定义数据库
image.png
URI格式
scheme:path[#fragment]——最详细的划分形式
同样以http://www.baidu.com:8080/yourpath/fileName.html?id=15&name=du#dmk为例
getScheme() :获取Uri中的scheme字符串部分,在这里是http
getSchemeSpecificPart():获取Uri中的scheme-specific-part:部分,这里是:http://www.baidu.com:8080/yourpath/fileName.html?
getFragment():获取Uri中的fragment部分,即dmk
getAuthority():获取Uri中Authority部分,即www.baidu.com:8080
getPath():获取Uri中path部分,即/yourpath/fileName.html
getQuery():获取Uri中的query部分,即id=15&name=du
getHost():获取Authority中的Host字符串,即www.baidu.com
getPost():获取Authority中的Port字符串,即8080
二:MIME数据类型
作用:指定某个扩展名的文件用某种应用程序来打开
如指定.html文件采用text应用程序打开,指定.pdf文件采用flash应用程序打开
MIME类型组成
每种MIME类型 由2部分组成=类型+子类型

text / html
// 类型 = text、子类型 = html
text/css
text/xml
application/pdf

ContentProvider主要以表格的形式组织数据,同时也支持文件数据,知识表格形式形式用得比较多
每一个表格中包含多张表,每一张表包含行&列,分别对应记录&字段
三:主要方法
进程间共享数据的本质是:添加,删除,获取&修改(更新)数据


<-- 4个核心方法 -->
  public Uri insert(Uri uri, ContentValues values) 
  // 外部进程向 ContentProvider 中添加数据

  public int delete(Uri uri, String selection, String[] selectionArgs) 
  // 外部进程 删除 ContentProvider 中的数据

  public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)
  // 外部进程更新 ContentProvider 中的数据

  public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs,  String sortOrder)  
  // 外部应用 获取 ContentProvider 中的数据

// 注:
  // 1. 上述4个方法由外部进程回调,并运行在ContentProvider进程的Binder线程池中(不是主线程)
 // 2. 存在多线程并发访问,需要实现线程同步
   // a. 若ContentProvider的数据存储方式是使用SQLite & 一个,则不需要,因为SQLite内部实现好了线程同步,若是多个SQLite则需要,因为SQL对象之间无法进行线程同步
  // b. 若ContentProvider的数据存储方式是内存,则需要自己实现线程同步

<-- 2个其他方法 -->
public boolean onCreate() 
// ContentProvider创建后 或 打开系统后其它进程第一次访问该ContentProvider时 由系统进行调用
// 注:运行在ContentProvider进程的主线程,故不能做耗时操作

public String getType(Uri uri)
// 得到数据类型,即返回当前 Url 所代表数据的MIME类型

ContentProvider类并不会直接与外部进程交互,而是通过ContentResolver类
五:ContentResolver类
统一管理不同ContentProvider间的操作

1.即通过URI 即可操作不同的ContentProvider中的数据
2.外部进程通过ContentResolver类从而与ContentProvider类进行交互

为什么要通过使用ContentResolver类从而与ContentProvider类进行交互,而不直接访问ContentProvider类?
答:一般来说,一款应用要使用多个ContentProvider,若需要了解每个ContentProvider的不同实现再完成数据交互,操作成本高&难度大,所以再ContentProvider类上加多一个ContentResolver类对所有的ContentProvider进行统一管理。
具体使用
ContentResolver 类提供了与ContentProvider类相同名字 & 作用的4个方法


// 外部进程向 ContentProvider 中添加数据
public Uri insert(Uri uri, ContentValues values)  

// 外部进程 删除 ContentProvider 中的数据
public int delete(Uri uri, String selection, String[] selectionArgs)

// 外部进程更新 ContentProvider 中的数据
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs)  

// 外部应用 获取 ContentProvider 中的数据
public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

实例

   /**
 * 对user表进行操作
 */
 // 设置URI
 Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");
 // 插入表中数据
 ContentValues values = new ContentValues();
 values.put("_id", 4);
 values.put("name", "Jordan");
 // 获取ContentResolver
 ContentResolver resolver =  getContentResolver();
 // 通过ContentResolver 根据URI 向ContentProvider中插入数据
 resolver.insert(uri_user,values);
 // 通过ContentResolver 向ContentProvider中查询数据
 Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
 while (cursor.moveToNext()){
        System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
 // 将表中数据全部输出
 }
    cursor.close();
 // 关闭游标
 /**
 * 对job表进行操作
 */
 // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
 Uri uri_job = Uri.parse("content://scut.carson_ho.myprovider/job");
 // 插入表中数据
 ContentValues values2 = new ContentValues();
 values2.put("_id", 4);
 values2.put("job", "NBA Player");
 // 获取ContentResolver
 ContentResolver resolver2 =  getContentResolver();
 // 通过ContentResolver 根据URI 向ContentProvider中插入数据
 resolver2.insert(uri_job,values2);
 // 通过ContentResolver 向ContentProvider中查询数据
 Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
 while (cursor2.moveToNext()){
        System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
 // 将表中数据全部输出
 }
    cursor2.close();
 // 关闭游标
}

六:Android提供了3个用于辅助ContentProvider的工具类
1.ContentUris
2.UriMatcher
3.ContentObserver
ContentUris类
作用:操作URI
核心方法有两个:withAppendedId()和parseId()

// withAppendedId()作用:向URI追加一个id
Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");
Uri resultUri = ContentUris.withAppendedId(uri_user,5);
 // 最终生成后的Uri为:content://cn.scu.myprovider/user/5
// parseId()作用:从URL中获取ID
Uri uri = Uri.parse("content://scut.carson_ho.myprovider/user/5");
long personid = ContentUris.parseId(uri);
//获取的结果为:5

UriMatcher类
作用:1.在ContentProvider中注册URI
2.根据URI匹配ContentProvider中对应的数据表
具体使用


// 步骤1:初始化UriMatcher对象
    UriMatcher matcher = new UriMatcher(UriMatcher.NO_MATCH); 
    //常量UriMatcher.NO_MATCH  = 不匹配任何路径的返回码
    // 即初始化时不匹配任何东西

// 步骤2:在ContentProvider 中注册URI(addURI())
    int URI_CODE_a = 1;
    int URI_CODE_b = 2;
    matcher.addURI("cn.scu.myprovider", "user1", URI_CODE_a); 
    matcher.addURI("cn.scu.myprovider", "user2", URI_CODE_b); 
    // 若URI资源路径 = content://cn.scu.myprovider/user1 ,则返回注册码URI_CODE_a
    // 若URI资源路径 = content://cn.scu.myprovider/user2 ,则返回注册码URI_CODE_b

// 步骤3:根据URI 匹配 URI_CODE,从而匹配ContentProvider中相应的资源(match())

@Override   
    public String getType(Uri uri) {   
      Uri uri = Uri.parse(" content://cn.scu.myprovider/user1");   

      switch(matcher.match(uri)){   
     // 根据URI匹配的返回码是URI_CODE_a
     // 即matcher.match(uri) == URI_CODE_a
      case URI_CODE_a:   
        return tableNameUser1;   
        // 如果根据URI匹配的返回码是URI_CODE_a,则返回ContentProvider中的名为tableNameUser1的表
      case URI_CODE_b:   
        return tableNameUser2;
        // 如果根据URI匹配的返回码是URI_CODE_b,则返回ContentProvider中的名为tableNameUser2的表
    }   
}

ContentObserver类
作用:观察Uri引起ContentProvider中数据变化&通知外界(即访问该数据访问者)

当ContentProvider中的数据发生变化(增,删&改)时,就会触发该ContentObserver类

具体使用

// 步骤1:注册内容观察者ContentObserver
    getContentResolver().registerContentObserver(uri);
    // 通过ContentResolver类进行注册,并指定需要观察的URI

// 步骤2:当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
    public class UserContentProvider extends ContentProvider { 
      public Uri insert(Uri uri, ContentValues values) { 
      db.insert("user", "userid", values); 
      getContext().getContentResolver().notifyChange(uri, null); 
      // 通知访问者
   } 
}

// 步骤3:解除观察者
 getContentResolver().unregisterContentObserver(uri);
    // 同样需要通过ContentResolver类进行解除 

七:进程内通信
步骤:1.创建数据库操作类
2.自定义ContentProvider类
3.注册创建ContentProvider类
4.进程内访问ContentProvider的数据
1.创建数据库操作类

public class MyDBHelper extends SQLiteOpenHelper {
    //数据库版本号
 public MyDBHelper(Context context) {
        super(context, DATABASE_NAME, null, DATABASE_VERSION);
 }
    //数据库版本号
 private static final int DATABASE_VERSION = 1;
 // 数据库名
 private static final String DATABASE_NAME = "finch.db";
 // 表名
 public static final String USER_TABLE_NAME = "user";
 public static final String JOB_TABLE_NAME = "job";
 @Override
 public void onCreate(SQLiteDatabase db) {
        // 创建两个表格:用户表 和职业表
 db.execSQL("CREATE TABLE IF NOT EXISTS " + USER_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " name TEXT)");
 db.execSQL("CREATE TABLE IF NOT EXISTS " + JOB_TABLE_NAME + "(_id INTEGER PRIMARY KEY AUTOINCREMENT," + " job TEXT)");
 }
    @Override
 public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
    }
}

2.自定义ContentProvider类

public class MyProvider extends ContentProvider {
    private Context mContext;
 MyDBHelper mDbHelper=null;
 SQLiteDatabase db=null;
 // 设置ContentProvider的唯一标识
 public static final String AUTOHORITY = "cn.ruan.myprovider";
 public static final int User_Code = 1;
 public static final int Job_Code = 2;
 // UriMatcher类使用:在ContentProvider 中注册URI
 private static final UriMatcher mMatcher;
 static {
        mMatcher=new UriMatcher(UriMatcher.NO_MATCH);
 //初始化
 mMatcher.addURI(AUTOHORITY,"user",User_Code);//user对应的是finch.db数据库的user表
 mMatcher.addURI(AUTOHORITY, "job", Job_Code);//job对应的是finch.db数据库的job表
 // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
 // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
 }
    /**
 * 初始化ContentProvider
 */ @Override
 public boolean onCreate() {
        mContext = getContext();
 // 在ContentProvider创建时对数据库进行初始化
 // 运行在主线程,故不能做耗时操作,此处仅作展示
 mDbHelper = new MyDBHelper(getContext());
 db = mDbHelper.getWritableDatabase();
 // 初始化两个表的数据(先清空两个表,再各加入一个记录)
 db.execSQL("delete from user");
 db.execSQL("insert into user values(1,'Rocky');");
 db.execSQL("insert into user values(2,'Kobe');");
 db.execSQL("delete from job");
 db.execSQL("insert into job values(1,'Android');");
 db.execSQL("insert into job values(2,'NBAPlayer');");
 return true; }
    @Nullable
 @Override public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
 // 该方法在最下面
 String table = getTableName(uri);
//        // 通过ContentUris类从URL中获取ID
//        long personid = ContentUris.parseId(uri);
//        System.out.println(personid);
 // 查询数据
 return db.query(table,projection,selection,selectionArgs,null,null,sortOrder,null);
 }
    @Nullable
 @Override public String getType(@NonNull Uri uri) {
        return null;
 }
    /**
 * 添加数据
 */
 @Nullable
 @Override public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
 // 该方法在最下面
 String table = getTableName(uri);
 return null; }
    @Override
 public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
 }
    @Override
 public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
        return 0;
 }
    /**
 * 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
 */
 private String getTableName(Uri uri){
        String tableName = null;
 switch (mMatcher.match(uri)) {
            case User_Code:
                tableName = MyDBHelper.USER_TABLE_NAME;
 break; case Job_Code:
                tableName = MyDBHelper.JOB_TABLE_NAME;
 break; }
        return tableName;
 }
}

3.注册创建ContentProvider类

<provider
 android:authorities="cn.ruan.myprovider"
 android:name="MyProvider"/>

4.进程内访问 ContentProvider的数据

public class MyActivity extends AppCompatActivity {
    @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_my);
 /**
 * 对user表进行操作
 */
 // 设置URI
 Uri uri_user = Uri.parse("content://cn.ruan.myprovider/user");
 // 插入表中数据
 ContentValues values = new ContentValues();
 values.put("_id", 3);
 values.put("name", "Iverson");
 // 获取ContentResolver
 ContentResolver resolver =  getContentResolver();
 // 通过ContentResolver 根据URI 向ContentProvider中插入数据
 resolver.insert(uri_user,values);
 // 通过ContentResolver 向ContentProvider中查询数据
 Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
 while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
 // 将表中数据全部输出
 }
        cursor.close();
 // 关闭游标
 /**
 * 对job表进行操作
 */
 // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
 Uri uri_job = Uri.parse("content://cn.ruan.myprovider/job");
 // 插入表中数据
 ContentValues values2 = new ContentValues();
 values2.put("_id", 3);
 values2.put("job", "NBA Player");
 // 获取ContentResolver
 ContentResolver resolver2 =  getContentResolver();
 // 通过ContentResolver 根据URI 向ContentProvider中插入数据
 resolver2.insert(uri_job,values2);
 // 通过ContentResolver 向ContentProvider中查询数据
 Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
 while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
 // 将表中数据全部输出
 }
        cursor2.close();
 // 关闭游标
 }
}

进程间通信的实现
进程1:
前面2步是一样的
步骤3:注册创建ContentProvider类


// 声明本应用 可允许通信的权限
<permission
 android:name="scut.carson_ho.Read"
 android:protectionLevel="normal" /> //自定义权限
 
 <provider
 android:name="MyProvider"
 android:authorities="scut.carson_ho.myprovider"
// 设置此provider是否可以被其他进程使用
 android:exported="true"
// 声明外界进程可访问该Provider的权限(读 & 写)
 android:permission="scut.carson_ho.PROVIDER" />

进程2:
步骤1:AndroidManifest.xml声明访问权限

// 声明本应用可允许通信的权限(全权限)
<uses-permission android:name="scut.carson_ho.PROVIDER"/>
// 注:声明的权限必须与进程1中设置的权限对应

步骤2:访问ContentProvider的类

public class MainActivity extends AppCompatActivity {

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

        /**
         * 对user表进行操作
         */

        // 设置URI
        Uri uri_user = Uri.parse("content://scut.carson_ho.myprovider/user");

        // 插入表中数据
        ContentValues values = new ContentValues();
        values.put("_id", 4);
        values.put("name", "Jordan");

        // 获取ContentResolver
        ContentResolver resolver =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver.insert(uri_user,values);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor = resolver.query(uri_user, new String[]{"_id","name"}, null, null, null);
        while (cursor.moveToNext()){
            System.out.println("query book:" + cursor.getInt(0) +" "+ cursor.getString(1));
            // 将表中数据全部输出
        }
        cursor.close();
        // 关闭游标

        /**
         * 对job表进行操作
         */
        // 和上述类似,只是URI需要更改,从而匹配不同的URI CODE,从而找到不同的数据资源
        Uri uri_job = Uri.parse("content://scut.carson_ho.myprovider/job");

        // 插入表中数据
        ContentValues values2 = new ContentValues();
        values2.put("_id", 4);
        values2.put("job", "NBA Player");

        // 获取ContentResolver
        ContentResolver resolver2 =  getContentResolver();
        // 通过ContentResolver 根据URI 向ContentProvider中插入数据
        resolver2.insert(uri_job,values2);

        // 通过ContentResolver 向ContentProvider中查询数据
        Cursor cursor2 = resolver2.query(uri_job, new String[]{"_id","job"}, null, null, null);
        while (cursor2.moveToNext()){
            System.out.println("query job:" + cursor2.getInt(0) +" "+ cursor2.getString(1));
            // 将表中数据全部输出
        }
        cursor2.close();
        // 关闭游标
    }
}

END:苦心人天不负卧薪尝胆三千越甲可吞吴,有志者天不负釜底抽薪百二秦川终属楚

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 2月5日

Android四大组件详解之BroadcastReceiver广播接收者

Android四大组件详解---BroadcastReceicer广播接收者
广播有两个角色,一个是广播发送者,另一个是广播接收者。
广播按照类型分为两种,一种是全局广播,另一种是本地广播
全局广播:就是发出的广播被其他任意应用程序接收,或者可以接收来自其他任意应用程序的广播。
本地广播:则是只能在应用程序内部进行传递的广播,广播接收器也只能接收内部的广播,不能接收其他应用程序的广播
广播按照机制分两种,一种是标准广播,一种是有序广播
标准广播:是一种异步的方式来进行传播的,所有接收者都会接收事件,不可以被拦截,不可以被修改
有序广播:是一种同步执行的广播,按照优先级,一级一级的向下传递,接收者可以修改广播数据,也可以终止广播事件。
一:使用广播接收器接收广播
1.定义一个TestReceiver类继承广播接收者BroadcastReceiver,复写其中的onReceive()方法

public class TestReceiver extends BroadcastReceiver {
    private static final String TAG="TestReceiver";
 @Override
 public void onReceive(Context context, Intent intent) {
        Log.d(TAG,"onReceive()");
 }
}

2.对广播进行注册
注册方式有两种,一种是动态注册,一种是静态注册
动态注册

public class ReceActivity extends AppCompatActivity {
    private TestReceiver receiver;
 private IntentFilter intentFilter;
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_rece);
 receiver=new TestReceiver();
 intentFilter=new IntentFilter();
 intentFilter.addAction("android.net.conn.CONNECTIVITY_CHANGE");
 //当网络发生变化的时候,系统广播会发出值为android.net.conn.CONNECTIVITY_CHANGE这样的一条广播
 registerReceiver(receiver,intentFilter);
 }
}
结果:切换网络变化
D/TestReceiver: onReceive()

静态注册,像这种系统广播,android8.0以上对静态广播注册做了严格限制,就没法接受到网络变化的广播
image.png

<receiver android:name=".TestReceiver"
 android:enabled="true"
 android:exported="true">
 <!--//表示是否允许这个广播接收器接收本程序以外的广播-->
 <intent-filter>
 <action android:name="ndroid.net.conn.CONNECTIVITY_CHANGE"/>
 <!--网络变化完成后发出一条广播android.intent.action.BOOT_COMPLETED的广播-->
 </intent-filter>
</receiver>

静态注册一个自定义的广播。

<receiver android:name=".TestReceiver"
 android:enabled="true"
 android:exported="true">
 <!--//表示是否允许这个广播接收器接收本程序以外的广播-->
 <intent-filter>
 <action android:name="11"/>//定义action为“11”
 </intent-filter>
</receiver>

//方式1
Intent intent=new Intent("11");
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
    intent.setPackage(getPackageName());//包名发送广播com.soudao.test
}
sendBroadcast(intent);
//方式2
Intent intent=new Intent("11");
if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
    //该方式适用:给其他应用的广播接收者发送消息(指定应用的包名、指定类的全类名)
 intent.setComponent(new ComponentName(getPackageName(), getPackageName()+".TestReceiver"));
 intent.setClassName(getPackageName(), getPackageName()+".TestReceiver");
}
sendBroadcast(intent);

结果:
都调用到onReceive()
D/TestReceiver: onReceive()

动态自定义广播

public class ReceActivity extends AppCompatActivity {
    private TestReceiver receiver;
 private IntentFilter intentFilter;
 private Button btn_b;
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_rece);
 //动态注册广播
 receiver=new TestReceiver();
 intentFilter=new IntentFilter();
 intentFilter.addAction("11");
 registerReceiver(receiver,intentFilter);//注册广播
 Log.d("aa",getPackageName());
 
 //点击按钮发送广播
 btn_b=findViewById(R.id.btn_b);
 btn_b.setOnClickListener(new View.OnClickListener() {
            @Override
 public void onClick(View v) {
 
 Intent intent=new Intent("11");
 sendBroadcast(intent);//发送广播,这是一个无序的广播
 }
        });
 }
 
 @Override
protected void onDestroy() {
    super.onDestroy();
 unregisterReceiver(receiver);//解除广播注册
}
}

动态发送有序广播,设置了优先级Priority属性(-1000-1000)

//动态注册一个广播接收者TestReceiver
receiver=new TestReceiver();
intentFilter=new IntentFilter();
intentFilter.addAction("11");
intentFilter.setPriority(200);
registerReceiver(receiver,intentFilter);
//动态注册一个广播接收者TestReceiver2
receiver2=new TestReceiver2();
intentFilter=new IntentFilter();
intentFilter.addAction("11");
intentFilter.setPriority(100);
registerReceiver(receiver2,intentFilter);
public class TestReceiver2 extends BroadcastReceiver {
    private static final String TAG="TestReceiver2";
 @Override
 public void onReceive(Context context, Intent intent) {
        Log.d(TAG,"我是TestReceiver2的onReceive()");
 }

点击按钮发送广播

 Intent intent=new Intent("11");
sendOrderedBroadcast(intent,null);
结果:
D/TestReceiver: onReceive()
D/TestReceiver2: 我是TestReceiver2的onReceive()

如果我们想拦截广播
可以在onReceive()中调用abortBroadcast();即广播就不会再传递下去了

本地广播使用
业界常见的一些增加安全性的方案包括:
1.对于同一App内部发送和接收广播,将exported属性人为设置成false,使得非本App内部发出的此广播不被接收
2.在广播发送和接收时,都增加相应的permission,用于权限验证
3.发送广播时,指定特定广播所在的包名,具体是通过intent.setPackage(packageName)指定,这样此广播将只会发送到此包中的App内与之相匹配的有效广播接收器中。
4.采用LocalBroadcastManager的方式
本地广播LocalBroadcastManager使用该机制发出的广播只能够在应用程序内部进行传递,并且广播接收器也只能接收来自本地应用程序发出的广播,这样所有的安全性问题都不存在了。

public class LocalReceiver  extends BroadcastReceiver {
    @Override
 public void onReceive(Context context, Intent intent) {
        Log.d("LocalReceiver","我是本地广播接收器");
 }
}
//注册本地广播
LocalBroadcastManager localBroadcastManager=LocalBroadcastManager.getInstance(this);//获取实例
LocalReceiver localReceiver=new LocalReceiver();
IntentFilter intentFilter=new IntentFilter();
intentFilter.addAction("cccc");
localBroadcastManager.registerReceiver(localReceiver,intentFilter);//注册本地广播监听

@Override
protected void onDestroy() {
    super.onDestroy();
 //unregisterReceiver(receiver);
 localBroadcastManager.unregisterReceiver(localReceiver);//注销本地广播
}

发送本地广播

 Intent intent=new Intent("cccc");
localBroadcastManager.sendBroadcast(intent);//发送本地广播
 //localBroadcastManager.sendBroadcastSync(intent);//发送本地有序广播
 
 结果:
 D/LocalReceiver: 我是本地广播接收器

广播权限有关
静态注册的方式:

自定义广播权限
<uses-permission android:name="com.ruan.rocky.permission"/>
<permission
 android:name="com.ruan.rocky.permission"
 android:label="BroadcastReceiverPermission"
 android:protectionLevel="signature">
</permission>
//permission的目录和<application>同级的位置配置使用到的权限
//protectionLevel="signature"的属性有哪些:
//normal:默认的,应用安装前,用户可以看到相应的权限,但无需用户主动授权。
//dangerous:normal安全级别控制以外的任何危险操作。需要dangerous级别权限时,Android会明确要求用户进行授权。常见的如:网络使用权限,相机使用权限及联系人信息使用权限等。
//signature:它要求权限声明应用和权限使用应用使用相同的keystore进行签名。如果使用同一keystore,则该权限由系统授予,否则系统会拒绝。并且权限授予时,不会通知用户。它常用于应用内部。**把protectionLevel声明为signature。如果别的应用使用的不是同一个签名文件,就没办法使用该权限,从而保护了自己的接收者**



<receiver android:name=".TestReceiver"
 android:enabled="true"
 android:exported="true"
 android:permission="com.ruan.rocky.permission">//添加了一个自定义的权限
 <!--//表示是否允许这个广播接收器接收本程序以外的广播-->
 <intent-filter>
 <action android:name="11"/>
 </intent-filter>
</receiver>

动态注册权限

receiver=new TestReceiver();
intentFilter=new IntentFilter();
intentFilter.addAction("11");
//intentFilter.setPriority(200);
registerReceiver(receiver,intentFilter,"com.ruan.rocky.permission",null);

在注册的时候,最关键的一点是用registerReceiver(BroadcastReceiver, IntentFilter, String, Handler)进行注册,而不是平常用的是registerReceiver(BroadcastReceiver, IntentFilter)。相较于后者,前者在注册的时候要求了发送者必须具有的权限。如果发送者没有该权限,那么发送者发送的广播即使经过IntentFilter的过滤,也不会被receiver接收。此时如果再自定义一个权限,并且将权限的protectionLevel设置为signature,那么外部应用便无法使用该权限,也就无法触及到该receiver。
发送广播:

Intent intent=new Intent("11");
sendBroadcast(intent,"com.ruan.rocky.permission");//发送无序的权限 广播
//sendOrderedBroadcast(intent,"com.ruan.rocky.permission");发送有序的权限,广播

Android广播机制
1.同一app内部的同一组件内的消息通信(单个或多个线程之间);
2.同一app内部的不同组件之间的消息通信(单个进程);
3.同一app具有多个进程的不同组件之间的消息通信;
4.不同app之间的组件之间消息通信;
5.Android系统在特定情况下与App之间的消息通信
从实现原理上,Android中的广播采用了观察者模式,基于消息的发布/订阅事件模型,因此,从实现的角度来看,Android中的广播将广播的发送者和接收者极大程度的解耦,使得系统能方便集成,更易扩展
1.广播接收者BroadcastReceiver通过Binder机制向AMS(Activity Manager Service)进行注册;
2.广播发送者通过binder机制向AMS发送广播;
3.AMS查找符合相应条件(IntentFilter/Permission等)的BroadcastReceiver,将广播发送到BroadcastReceiver(一般情况下是Activity)相应的消息循环队列中;
4.消息循环执行拿到此广播,回调BroadcastReceiver中的onReceive()方法
由此看来,广播发送者和广播接收者分别属于观察者模式中的消息发布和订阅两端,AMS属于中间的处理中心。广播发送者和广播接收者的执行是异步的,发出去的广播不会关心有无接收者接收,也不确定接收者到底是何时才能接收到。显然,整体流程与EventBus非常类似。
在上文说列举的广播机制具体可以使用的场景中,现分析实际应用中的适用性
第一种情形:同一app内部的同一组件内的消息通信(单个或多个线程之间),实际应用中肯定是不会用到广播机制的(虽然可以用),无论是使用扩展变量作用域、基于接口的回调还是Handler-post/Handler-Message等方式,都可以直接处理此类问题,若适用广播机制,显然有些“杀鸡牛刀”的感觉,会显太“重”;
第二种情形:同一app内部的不同组件之间的消息通信(单个进程),对于此类需求,在有些教复杂的情况下单纯的依靠基于接口的回调等方式不好处理,此时可以直接使用EventBus等,相对而言,EventBus由于是针对统一进程,用于处理此类需求非常适合,且轻松解耦。
第三、四、五情形:由于涉及不同进程间的消息通信,此时根据实际业务使用广播机制会显得非常适宜
结尾:拨开云雾见天日 守得云开见月明

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 2月5日

Android四大组件详解之Service篇

Android四大组件详解--Service篇

Android服务是一个后台运行的组件,执行长时间运行且不需要用户交互的任务。即使应用被销毁也依然可以工作。
一:Service基本用法
1.StartService开启服务
新建一个TestService继承Service,并重写父类的onCreate(),onStartCommand(),onDestroy()的方法

public class TestService  extends Service {
    public static final String TAG="TestService";
 /**
 * 继承一个Service必然要实现的onBind方法*/
 @Nullable
 @Override public IBinder onBind(Intent intent) {
        Log.d(TAG,"onBind()");
 return null; }
    @Override
 public void onCreate() {
        super.onCreate();
 Log.d(TAG,"onCreate()");
 }
    @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG,"onStartCommand()");
 return super.onStartCommand(intent, flags, startId);
 }
    @Override
 public void onDestroy() {
        super.onDestroy();
 Log.d(TAG,"onDestroy()");
 }
}

在AndroidManifest中配置Service

<application>
<service android:name=".TestService"/>
</application>

点击执行Service

four_teen_btn=findViewById(R.id.four_teen_btn);
four_teen_btn_service=findViewById(R.id.four_teen_btn_service);
four_teen_btn.setOnClickListener(new View.OnClickListener() {
    @Override
 public void onClick(View v) {
        Intent intent=new Intent(FourTeenActivity.this,TestService.class);
 startService(intent);//开启服务
 }
});
four_teen_btn_service.setOnClickListener(new View.OnClickListener() {
    @Override
 public void onClick(View v) {
        Intent intent=new Intent(FourTeenActivity.this,TestService.class);
 stopService(intent);//停止服务
 }
});
结果:
D/TestService: onCreate()
D/TestService: onStartCommand()
startService开启服务后会走onCreate()和onStartCommand()方法
多次点击只会执行onStartCommand(),onCreate()只会调用一次
 D/TestService: onStartCommand()
 调用stopService方法关闭服务
 D/TestService: onDestroy()

context.startService()开启服务流程:
context.startService()-->onCreate()-->onStartCommand()--->context.stopService()-->onDestroy()-->Service Stop
2.bindService绑定服务
观察发现上面发现Service和Activity之间关联不大,发现上面一直有一个onBind()方法没有用到
故Activity和Service通信,可以通过绑定一个Service
应用组件可以调用bindService()绑定一个service,Android系统之后调用service的onBind()方法,它返回一个用来与service交互的IBinder
绑定是异步的,bindService()会立即返回,它不会返回IBinder给客户端,要接收IBinder,客户端必须创建一个ServiceConnection的实例并传给bindService()ServiceConnection包含一个回调方法,系统调用这个方法来传递要返回的IBinder.

public class TestService  extends Service {
    public static final String TAG="TestService";
 private MyBinder myBinder=new MyBinder();
 /**
 * 继承一个Service必然要实现的onBind方法*/
 @Nullable
 @Override public IBinder onBind(Intent intent) {
        Log.d(TAG,"onBind()");
 return myBinder;//返回IBinder接口
 }
    @Override
 public void onCreate() {
        super.onCreate();
 Log.d(TAG,"onCreate()");
 }
    @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG,"onStartCommand()");
 return super.onStartCommand(intent, flags, startId);
 }
    @Override
 public void onDestroy() {
        super.onDestroy();
 Log.d(TAG,"onDestroy()");
 }
    class MyBinder extends Binder{//Binder实现了IBinder接口了
        public void startDownload(){
            Log.d(TAG,"startDownload()");
 //执行具体的任务
 }
    }
}
 four_teen_btn_bind_service.setOnClickListener(new View.OnClickListener() {
        @Override
 public void onClick(View v) {
            Intent bindIntent = new Intent(FourTeenActivity.this, TestService.class);
 bindService(bindIntent, connection, BIND_AUTO_CREATE);
 }
    });
 four_teen_btn_unbind_service.setOnClickListener(new View.OnClickListener() {
        @Override
 public void onClick(View v) {
            Intent bindIntent = new Intent(FourTeenActivity.this, TestService.class);
 unbindService(connection);
 }
    });
}
ServiceConnection connection=new ServiceConnection() {
//这个方法系统调用这个来传送在Service的onBind()中返回的IBinder
    @Override
 public void onServiceConnected(ComponentName name, IBinder service) {
        //拿到后台服务的代理对象
 TestService.MyBinder myBinder= (TestService.MyBinder) service;
 //调用后台服务的方法
 myBinder.startDownload();
 }
 //Android系统在同service连接意外丢失调用这个
    @Override
 public void onServiceDisconnected(ComponentName name) {
    }
};
通过调用bindService传递给他ServiceConnection的实现
 bindService(bindIntent, connection, BIND_AUTO_CREATE);
 第一个参数明确指定要绑定的ServiceIntent
 第二个参数是ServiceConnection对象
 第三个参数是一个标志,表明绑定中的操作,它一般是BIND_AUTO_CREATE
 结果:
 点击bindService后
 Service走了onBind方法
 D/TestService: onCreate()
D/TestService: onBind()
调用Binder类自定义的方法
D/TestService: startDownload()

点击unbindService
D/TestService: onUnbind()
D/TestService: onDestroy()

通过bindService绑定服务
bindService()-->onCreate()-->onBind()-->unBindService()-->onUnbind()-->onDestory()
image.png
3.Service和线程Thread的区别
Thread:是程序执行的最小单元,可以用线程来执行一些异步操作。
Service:是android的一种机制,当它运行的时候如果是Local Service,那么对应的Service是运行在主线程main线程上;如果是Remote Service .那么对应的Service则运行在独立进程的main线程上。

//在Activity中
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_four_teen);
 Log.d(TAG,"onCreate()");
 Log.d("TestService","FourTeenActivity thread is"+Thread.currentThread().getId());
 }
 结果:
 D/FourTeenActivity: onCreate()
D/TestService: FourTeenActivity thread is2
//在Service中
public void onCreate() {
    super.onCreate();
 Log.d(TAG,"onCreate()");
 Log.d("TestService","MyService thread is"+Thread.currentThread().getId());
}
结果:
 D/TestService: onCreate()
D/TestService: MyService thread is2
发现2个线程的id相同,说明service运行在主线程main中

故:不要把后台服务和子线程联系在一起,service不能做耗时操作,不然会导致ANR,要做耗时操作需要在服务中创建一个子线程。
4.前台服务
前台服务是那些被认为用户知道且在系统内存不足的时候不允许系统杀死的服务。前台服务必须给状态栏提供一个通知。
最常见的表现形式就是音乐播放服务,应用程序后台进行时,用户可以通过通知栏,知道当前播放内容,并进行暂停,继续,切歌等相关操作。
创建一个前台服务Service

public class ForegroundService extends Service {
    private static final String TAG = "ForegroundService";
 private static final int NOTIFICATION_ID=10;
 @Nullable
 @Override public IBinder onBind(Intent intent) {
        return null;
 }
    @Override
 public void onCreate() {
        super.onCreate();
 Log.d(TAG, "onCreate()");
 //获取服务通知
 Notification notification=createForegroundNotification();
 //将服务置于启动状态,NOTIFICATION_ID指的是创建的通知的ID
 startForeground(NOTIFICATION_ID,notification);
 }
    @Override
 public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand()");
 // 数据获取
 String data = intent.getStringExtra("Foreground");
 Toast.makeText(this, data, Toast.LENGTH_SHORT).show();
 return super.onStartCommand(intent, flags, startId);
 }
    @Override
 public void onDestroy() {
        stopForeground(true);
 super.onDestroy();
 Log.d(TAG, "onDestroy()");
 }
    /**
 * 创建服务通知
 */
 private Notification createForegroundNotification() {
        NotificationManager notificationManager = (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
 //唯一的通知通道的id
 String notificationChannelId="notification_channel_id_01";
 //Android8.0以上的系统,新建消息通道
 if (Build.VERSION.SDK_INT>=Build.VERSION_CODES.O){
            //用户可见的通道名称
 String channelName="Foreground Service Notification";
 //通道的重要程度
 int importance=NotificationManager.IMPORTANCE_HIGH;
 NotificationChannel notificationChannel=new NotificationChannel(notificationChannelId,channelName,importance);
 notificationChannel.setDescription("Channel description");
 //LED灯
 notificationChannel.enableLights(true);
 notificationChannel.setLightColor(Color.RED);
 //震动
 notificationChannel.setVibrationPattern(new long[]{0,1000,500,1000});
 notificationChannel.enableVibration(true);
 if (notificationManager!=null){
                notificationManager.createNotificationChannel(notificationChannel);
 }
        }
        NotificationCompat.Builder builder=new NotificationCompat.Builder(this,notificationChannelId);
 //通知小图标
 builder.setSmallIcon(R.mipmap.ic_launcher);
 //通知标题
 builder.setContentTitle("ContentTitle");
 //通知内容
 builder.setContentText("ContentText");
 //设置通知时间
 builder.setWhen(System.currentTimeMillis());
 //设置启动内容
 Intent activityIntent=new Intent(this,NotificationActivity.class);
 PendingIntent pendingIntent=PendingIntent.getActivity(this,1,activityIntent,PendingIntent.FLAG_UPDATE_CURRENT);
 builder.setContentIntent(pendingIntent);
 return builder.build();
 }
}
btn_start_service=findViewById(R.id.btn_start_service);
btn_stop_service=findViewById(R.id.btn_stop_service);
btn_start_service.setOnClickListener(new View.OnClickListener() {
    @Override
 public void onClick(View v) {
            // Android 8.0使用startForegroundService在前台启动新服务
 Intent mForegroundService = new Intent(NotificationActivity.this, ForegroundService.class);
 mForegroundService.putExtra("Foreground", "This is a foreground service.");
 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                startForegroundService(mForegroundService);
 } else {
                //启动服务
 startService(mForegroundService);
 }
        }
});
btn_stop_service.setOnClickListener(new View.OnClickListener() {
    @Override
 public void onClick(View v) {
        //停止服务
 Intent mForegroundService = new Intent(NotificationActivity.this, ForegroundService.class);
 stopService(mForegroundService);
 }
});

前台服务需要配置一个权限

<!--前台服务权限-->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>

前台服务和Notification密不可分,Notification具体需要详细研究
**总结:
前台服务Service的系统优先级更高,不易被回收
前台服务Service会一直有一个正在运行的图标在系统的状态栏显示,下拉状态栏后可以看到更详细的信息,非常类似于通知的效果**
5.远程服务Remote Service
1.什么是远程服务
远程服务也被称之为独立进程,它不受其他进程影响,可以为其他应用进程提供调用的接口--实际上就是进程间通信(IPC),Android 提供了AIDL工具来帮助进程间接口的建立。
注:在Android中,不同的应用属于不同的进程(Process),一个进程不能访问其它进程的存储(可以通过ContentProvider实现,如:通讯录的读取)。
1.AS创建AIDL文件,创建一个MyRemote接口
image.png
新建一个MyRemote接口,创建一个getMessage()方法
image.png
注:如果服务端与客户端不在同一App上,需要在客户端、服务端两侧都建立该aidl文件。
3.新建一个Remote Service
在远程服务中通过Service的onBind(),在客户端与服务端建立连接时,用来传递对象

public class RemoteService extends Service {
    private static final String TAG="RemoteService";
 @Nullable
 @Override public IBinder onBind(Intent intent) {
        Log.d(TAG,"onBind");
        Log.d(TAG,Thread.currentThread().getName());
 return stub;// 在客户端连接服务端时,Stub通过ServiceConnection传递到客户端
 }
    @Override
 public boolean onUnbind(Intent intent) {
        Log.d(TAG,"onUnbind");
 return super.onUnbind(intent);
 }
    @Override
 public void onDestroy() {
        Log.d(TAG,"onDestroy");
 super.onDestroy();
 }
    // 实现接口中暴露给客户端的Stub--Stub继承自Binder,它实现了IBinder接口
 private MyRemote.Stub stub = new MyRemote.Stub() {
        //实现AIDL文件中定义的方法
 @Override
 public String getMessage() throws RemoteException {
            // 在这里我们只是用来模拟调用效果,因此随便反馈值给客户端
 return "Remote Service方法调用成功";
 }
    };
}
//我们在AndroidManifest文件定义Service
<service android:name=".RemoteService"
 android:process="com.ruan.test.remote">//或者android:process=":remote"
 <intent-filter> <action android:name="com.soudao.test.RemoteService"/>
 </intent-filter>
</service>

注:如果客户端与服务端在同个App中,AndroidManifest.xml中设置Remote Service的andorid:process属性时,如果被设置的进程名是以一个冒号(:)开头的,则这个新的进程对于这个应用来说是私有的,当它被需要或者这个服务需要在新进程中运行的时候,这个新进程将会被创建。如果这个进程的名字是以小写字符开头的,则这个服务将运行在一个以这个名字命名的全局的进程中,当然前提是它有相应的权限。这将允许在不同应用中的各种组件可以共享一个进程,从而减少资源的占用。
4.在Activity中去开启远程服务和关闭远程服务

public class RemoteActivity extends AppCompatActivity {
    private Button btn_start_service;
 private Button btn_stop_service;
 private MyRemote myRemote;//定义接口变量
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_remote);
 btn_start_service = findViewById(R.id.btn_start_service);
 btn_stop_service = findViewById(R.id.btn_stop_service);
 btn_stop_service.setOnClickListener(new View.OnClickListener() {
            @Override
 public void onClick(View v) {
                unbindService(connection);
 }
        });
 btn_start_service.setOnClickListener(new View.OnClickListener() {
            @Override
 public void onClick(View v) {
                Intent intentService = new Intent();
 intentService.setClassName(RemoteActivity.this, "com.soudao.test.RemoteService");
 bindService(intentService, connection, BIND_AUTO_CREATE);
 }
        });
 }
    ServiceConnection connection = new ServiceConnection() {
        @Override
 public void onServiceConnected(ComponentName name, IBinder iBinder) {
            //从连接中获取Stub对象
 myRemote = MyRemote.Stub.asInterface(iBinder);
 // 调用Remote Service提供的方法
 try {
                Log.d("RemoteActivity", "获取到消息:" + myRemote.getMessage());
 } catch (RemoteException e) {
                e.printStackTrace();
 }
        }
        @Override
 public void onServiceDisconnected(ComponentName name) {
            // 断开连接
 myRemote=null;
 }
    };
 @Override
 protected void onDestroy() {
        super.onDestroy();
 if (connection!=null){
            unbindService(connection);//解除绑定
 }
    }
}

点击开启服务,远程服务会开启,进程com.soudao.test:remote
image.png
对应的RemoteActivity与Remote Service的连接,获取Stub,然后调用Remote Service提供的方法获取对应的数据
image.png
image.png
远程服务的优缺点:
优点:
1.远程服务有自己的独立进程,不会受其他进程的影响
2.可以被其他进程复用,提供公共服务
3.具有很高的灵活性
缺点:
相对普通服务占用系统资源较多,使用AIDL进行IPC也相对麻烦
5.AndroidManifest.xml中Service元素常见属性
1.android:name=".RemoteService"//服务的类名,可以是完整的包名+类名。也可以使用.代替包名
2.android:exported="true"//其他应用能否访问该服务,如果不能,则只有本应用获取有相同用户ID的应用能访问,默认是false
3.android:enabled="true"//标志服务是否可以被系统实例化。true系统默认启动,false 不启动
4.android:label=""//显示给用户的服务名称,如果没有进行服务名称的设置,默认显示服务的类名
5.android:process=":remote"//服务所在的进程名,默认是在当前进程下运行,与包名一致。如果进行设置,警徽在包名后加上设置的集成名。
6.android:icon=""//服务的图标
7.android:permission=""//申请使用该服务的权限,如果没有配置下相关权限,服务将不执行,使用startService,和bindService方法将得不到执行

6.无障碍服务Service
无障碍服务是为了增强用户界面以帮助残障人士,它的具体实现是通过AccessibilityService服务运行在后台中,通过AccessibilityEvent接收指定事件的回调。这样的事件表示用户在界面中的一些状态转换,例如:焦点改变了,一个按钮被点击,等等。这样的服务可以选择请求活动窗口的内容的能力。简单的说AccessibilityService就是一个后台监控服务,当你监控的内容发生改变时,就会调用后台服务的回调方法
1.AccessibilityService的使用

public class AutoScriptService extends AccessibilityService {
    public static final String ACTION_SERVICE_STATE_CHANGE = "ACTION_SERVICE_STATE_CHANGE";
    //当服务启动的时候就会别调用
 @Override
 protected void onServiceConnected() {
        super.onServiceConnected();
 LogUtils.d("AccessibilityService onServiceConnected");
 sendAction(true);
 }
 //监听窗口变化的回调
    @Override
 public void onAccessibilityEvent(AccessibilityEvent event) {
        LogUtils.d("AccessibilityService onAccessibilityEvent");
 }
 //中断服务的回调
    @Override
 public void onInterrupt() {
    }
    private void sendAction(boolean state) {
        Intent intent = new Intent(ACTION_SERVICE_STATE_CHANGE);
 intent.putExtra("state", state);
 sendBroadcast(intent);
 }
}

对应的AndroidManfest.xml文件里

<service android:name=".AutoScriptService"
 android:exported="true"
 android:enabled="true"
 android:label="@string/app_name"
 android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
 <intent-filter> <action android:name="android.accessibilityservice.AccessibilityService" />
 </intent-filter> <meta-data android:name="android.accessibilityservice"
 android:resource="@xml/accessibilityconfig" />
</service>

在res文件下新建一个xml文件

<accessibility-service xmlns:android="http://schemas.android.com/apk/res/android"
 android:accessibilityEventTypes="typeAllMask"
 android:accessibilityFeedbackType="feedbackGeneric"
 android:accessibilityFlags="flagIncludeNotImportantViews|flagReportViewIds|flagRequestEnhancedWebAccessibility|flagRetrieveInteractiveWindows"
 android:canRequestEnhancedWebAccessibility="true"
 android:canRetrieveWindowContent="true"
 android:description="@string/my_accessibility_description"
 android:notificationTimeout="100" />

7.IntentService使用

public class MyIntentService extends IntentService {
    private static final String TAG="MyIntentService";
 private int count=0;
 public MyIntentService() {
        super(TAG);
 }
    @Override
 protected void onHandleIntent(@Nullable Intent intent) {
        count++;
 Log.d(TAG,"count:"+count);
 }
}
1.IntentService 是继承于 Service 并处理异步请求的一个类,在 IntentService 内有一个工作线程来处理耗时操作,启动 IntentService 的方式和启动传统 Service 一样
2.当任务执行完后,IntentService 会自动停止,而不需要我们去手动控制关闭
3.可以启动 IntentService 多次
4.而每一个耗时操作会以工作队列的方式在IntentService 的 onHandleIntent 回调方法中执行,并且,每次只会执行一个工作线程,执行完第一个再执行第二个,以此类推。就是在一个单队列
5.请求都在一个单线程中,不会阻塞应用程序的主线程(UI Thread),同一时间只处理一个请求

8.Android 系统服务

获取系统的网络连接服务:
ConnectivityManager mConnectivityManager = (ConnectivityManager)context.getSystemService(CONNECTIVITY_SERVICE);
获取系统的 WiFi 服务:
WifiManager wifimanager = (WifiManager) this.getSystemService(WIFI_SERVICE);
获取系统的 Auido(音响/声音)服务:
AudioManager mAudioManager = (AudioManager) this.getSystemService(AUDIO_SERVICE);
获取系统的 Activity 服务:
ActivityManager mActivityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
别忘了添加权限:
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>//网络状态权限
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>//读取WiFi状态
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />//改变WiFi状态
<uses-permission android:name="android.permission.GET_TASKS"/>//允许一个程序获取信息有关当前或最近运行的任务,一个缩略的任务状态,是否活动等等

END:道虽迩,不行不至;事虽小,不为不成

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 2月3日

Android 四大组件的详解之Activity篇

Android 四大组件详解---Activity

一:Activity

Activity是一个Android的应用组件,它提供屏幕进行交互。每一个Activity都会获得一个用于绘制其用户界面的窗口,窗口可以充满屏幕也可以小于屏幕并浮动在其他窗口上。

//清单文件中配置启动
//Intent在由以下几个部分组成:动作(action),数据(data),分类(Category),类型(Type),组件(Component),和扩展信息(Extra)。  
<activity android:name=".FourTeenActivity">
 <intent-filter> 
 <action android:name="android.intent.action.MAIN" />//动作,应用程序的入口
 <category android:name="android.intent.category.LAUNCHER" />// 表示目标Activity是应用程序中最优先被执行的Activity    
 </intent-filter>
 </activity>
 
 public class FourTeenActivity extends AppCompatActivity {
 private static final String TAG= FourTeenActivity.class.getSimpleName();
 @Override
 protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_four_teen);
 Log.d(TAG,"onCreate()");
 }
 }

image.png

**常见的Activity Action Intent常量** 
常量名称   常量值  意义 
ACTION_MAIN  android.intent.action.MAIN   应用程序入口 
ACTION_VIEW  android.intent.action.VIEW  显示数据给用户 
ACTION_ATTACH_DATA  android.intent.action.ATTACH_DATA  指明附加信息给其他地方的一些数据 
ACTION_EDIT  android.intent.action.EDIT  显示可编辑的数据 
ACTION_PICK  android.intent.action.PICK  选择数据 
ACTION_CHOOSER  android.intent.action.CHOOSER  显示一个Activity选择器 
ACTION_GET_CONTENT  android.intent.action.GET_CONTENT  获得内容 
ACTION_DIAL  android.intent.action.GET_CONTENT  显示打电话面板 
ACITON_CALL  android.intent.action.DIAL  直接打电话 
ACTION_SEND  android.intent.action.SEND  直接发短信 
ACTION_SENDTO  android.intent.action.SENDTO  选择发短信 
ACTION_ANSWER  android.intent.action.ANSWER  应答电话 
ACTION_INSERT  android.intent.action.INSERT  插入数据 
ACTION_DELETE  android.intent.action.DELETE  删除数据 
ACTION_RUN  android.intent.action.RUN  运行数据 
ACTION_SYNC  android.intent.action.SYNC  同步数据 
ACTION_PICK_ACTIVITY  android.intent.action.PICK_ACTIVITY  选择Activity 
ACTION_SEARCH  android.intent.action.SEARCH  搜索 
ACTION_WEB_SEARCH  android.intent.action.WEB_SEARCH  Web搜索 
ACTION_FACTORY_TEST  android.intent.action.FACTORY_TEST  工厂测试入口点

**常见的Category常量** 
Category字符串常量  描述 
CATEGORY_BROWSABLE  目标Activity能通过在网页浏览器中点击链接而激活(比如,点击浏览器中的图片链接) 
CATEGORY_GADGET  表示目标Activity可以被内嵌到其他Activity当中 
CATEGORY_HOME  目标Activity是HOME Activity,即手机开机启动后显示的Activity,或按下HOME键后显示的Activity 
CATEGORY_LAUNCHER  表示目标Activity是应用程序中最优先被执行的Activity 
CATEGORY_PREFERENCE  表示目标Activity是一个偏爱设置的Activity

1.Activity的生命周期
Activity本质上有四种状态:
运行(Active/Running):Activity处于活跃状态,此时Activty处于栈顶,是可见状态,可以与用户交互
暂停(Paused):当Activity失去焦点时,Activity就转化成Paused状态。此刻并不会被销毁,知识失去了与用户交互的能力,只有在烯烃内存资源不足的情况下,才有可能被系统回收掉
停止(Stopped):当Activity被系统完全覆盖时,被覆盖的Activity就会进去Stopped状态,此时已不再可见,但是资源还是没有被回收
系统回收(Killed):当Activity被系统回收掉,Activity就处于Killed状态
当一个活动在处于停止或者暂停的状态下,系统内存缺乏时会将其结束(finish)或者杀死(Kill).这种非正常的情况下,系统在杀死或者结束之前会调用onSaveInstance()方法来保存信息,同时当Actvity被移动带前台时,重启该Activity并调用onRestoreInstance()方法加载保留的信息,以保持原来的状态
image.png
onCreate() 首次创建Activity时调用,此方法执行所有正常的静态设置--创建视图,数据绑定到列表等
onStart() 此方法被回调表示Activity已处于可见状态,只是还没有在前台显示,因此无法与影虎进行交互。可以简单理解为Activity已显示但是我们无法看见
onResume() 回调此方法时,说明Activity已在前台可见,可以与用户交互了(Activie/Running形态)
onPause()当前Activity正在停止(Paused形态),系统即将开始另一个Activity时调用此方法
onStop() Activity对用户不再可见的时调用,如果Activity被销毁或者另一个Activity继续执行并将其覆盖
onDestory() 在Activity被销毁前调用。这是Activity将收到的最后调用,当Activity结束(调用finish())或者系统为节省空间而销毁该Activity实例
onRestart() 在Activity已停止并即将再次启动前调用,后接onStart()
1.当在手机加载应用至界面时,Activity启动-->onCreate()-->onStart()-->onResume()

FourTeenActivity: onCreate()
FourTeenActivity: onStart()
FourTeenActivity: onResume()

2.按下home键回到主界面(Activity不可见)--->onPause()--->onStop()

FourTeenActivity: onPause()
FourTeenActivity: onStop()

3.当重新回到原Activity时,调用onRestart()-->onStart()-->onResume()

FourTeenActivity: onRestart()
FourTeenActivity: onStart()
FourTeenActivity: onResume()

4.当按下返回键(back),应用退出Activity被销毁

FourTeenActivity: onPause()
FourTeenActivity: onStop()
FourTeenActivity: onDestroy()

5.当点击按钮进行页面跳转时

FourTeenActivity: onPause()

FiveTeenActivity: onCreate()
FiveTeenActivity: onStart()
FiveTeenActivity: onResume()

FourTeenActivity: onStop()

6.点击back键回退时(回退的话会销毁这个Activity),但是如果我们在FiveTeenActivity点击跳转的话FiveTeenActivity不走onDestory()方法

FiveTeenActivity: onPause()

FourTeenActivity: onRestart()
FourTeenActivity: onStart()
FourTeenActivity: onResume()

FiveTeenActivity: onStop()
FiveTeenActivity: onDestroy()

Activity活动,理解为手机屏幕,与用户交互的可视化界面;Activity存储在Activity存储在Activity栈中,后进先出
Activity 的 onSaveInstanceState()方法调用

static final String STATE_SCORE = "playerScore";
static final String STATE_LEVEL = "playerLevel";
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
// 保存用户自定义的状态
    savedInstanceState.putInt(STATE_SCORE, mCurrentScore);
    savedInstanceState.putInt(STATE_LEVEL, mCurrentLevel);
    super.onSaveInstanceState(outState);
}
在Android P之前:这个方法将在onStop()之后被调用
在targetSdkVersion小于11时,如果要执行onSaveInstanceState方法,则会在onPause之前执行。当targetSdkVersion大于等于11时,是在onPause之后执行的,但是都一定会在onStop之前。

数据保存:Activity申明周期结束时候,需要保存Activity状态的时候会将保存的数据已键值对的形式保存在Bundle对象中。
恢复你的Activity 状态
当你的Activity之前被破坏后重新创建时,你可以从Bundle系统通过你的保存状态。这2个方法onCreate()和onRestoreInstanceState()回调方法都会收到Bundle

@Override
protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
 super.onRestoreInstanceState(savedInstanceState);
 mCurrentScore = savedInstanceState.getInt(STATE_SCORE);
 mCurrentLevel = savedInstanceState.getInt(STATE_LEVEL);

}

onCreate()和onRestoreInstanceState()调用的恢复数据的区别
1.因为onSaveInstanceState 不一定会被调用,所以onCreate()里的Bundle参数可能为空,如果使用onCreate()来恢复数据,一定要做非空判断。
2.而onRestoreInstanceState的Bundle参数一定不会是空值,因为它只有在上次activity被回收了才会调用。
3.而且onRestoreInstanceState是在onStart()之后被调用的。有时候我们需要onCreate()中做的一些初始化完成之后再恢复数据,用onRestoreInstanceState会比较方便。
二:Android四大启动模式
1.standard(默认标准)
系统默认的启动模式,Android是使用返回栈来管理活动的,每次启动一个Activity都会又一次创建一个新的实例入栈,无论这个实例是否存在
1.在A中跳转B,B中跳转A
A Activity-->B Activity-->A Activity-->B Activity
这样栈里有4个Activity 栈顶是B
2.SingleTop 栈顶复用模式
需要创建的Activity已经处于栈顶时,此时会直接复用栈顶的Activity,不会再创建新的Activity,若不在栈顶,此时会一次又一次创建Activity同Standard模式一样

<activity android:name=".FiveTeenActivity"
 android:launchMode="singleTop"/>//在AndroidMainfest中设置启动模式为singleTop
当我们设置了启动模式,在FiveTeenActivity去跳转到自己,就会走栈顶复用模式,走onNewIntent()方法

D/FiveTeenActivity: onCreate()
D/FiveTeenActivity: onStart()
D/FiveTeenActivity: onResume()
D/FiveTeenActivity: onPause()
//第二次调用
D/FiveTeenActivity: onNewIntent()
D/FiveTeenActivity: onResume()

3.SingleTask 栈内复用模式
若需要创建Activity已经处于栈中时,此时不会创建新的Activity,而是将存在栈中的Activity上面的其他Activity所有销毁,使它成为栈顶。
当前栈内已经有了FourTeenActivity,FiveTeenActivity且位于栈顶,
在栈顶FiveTeenActivity中去启动FourTeenActivity且FourTeenActivity启动模式为SingleTask,这样

<activity android:name=".FourTeenActivity"
 android:launchMode="singleTask">//这样就会移除FourTeenActivity上面所有的Activity且置于栈顶
//代表FiveTeenActivity呈现出来
 D/FiveTeenActivity: onCreate()
D/FiveTeenActivity: onStart()
D/FiveTeenActivity: onResume()
D/FourTeenActivity: onStop()
//点击跳转到FourTeenActivity,这样FourTeenActivity会回调onNewIntent()
D/FiveTeenActivity: onPause()
D/FourTeenActivity: onNewIntent()
D/FourTeenActivity: onRestart()
D/FourTeenActivity: onStart()
D/FourTeenActivity: onResume()
//然后跳转销毁栈顶上的FiveTeenActivity
D/FiveTeenActivity: onStop()
D/FiveTeenActivity: onDestroy()
//在点击back,直接就退出应用说明栈内只有一个Activity
D/FourTeenActivity: onPause()
D/FourTeenActivity: onStop()
D/FourTeenActivity: onDestroy()

4.SingleInstance单例模式
是全局单例模式,此模式的Activity仅仅能单独位于一个任务栈中

<activity android:name=".FiveTeenActivity"
 android:launchMode="singleInstance"/>
 

除了在Manifest.xml中指定Activity启动模式
还可以在Intent中指定启动模式
intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);//singleTop模式
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);//singleTask模式
这种没法为Activity指定 singleInstance 模式。
三:Android横竖屏切换
1.竖屏切换横屏不配置

D/FourTeenActivity: onPause()
D/FourTeenActivity: onStop()
D/FourTeenActivity: onDestroy()
//重走一次生命周期
D/FourTeenActivity: onCreate()
D/FourTeenActivity: onStart()
D/FourTeenActivity: onResume()

2.横屏切换竖屏不配置

 D/FourTeenActivity: onPause()
D/FourTeenActivity: onStop()
 D/FourTeenActivity: onDestroy()
 //也只走一次生命周期方法,之前Android系统走两次
D/FourTeenActivity: onCreate()
D/FourTeenActivity: onStart()
D/FourTeenActivity: onResume()

备注:关于部分资料说的不配置orientation属性切换竖屏会执行两次的结论,我在4.0之后的手机上测试都是只执行一次,不知道之前版本的系统是不是这样,待验证
3.Activity配置configChanges=""

<activity android:name=".FourTeenActivity"
 android:configChanges="orientation|screenSize">

横竖屏切换后生命周期方法

//切换横屏
 D/FourTeenActivity: onConfigurationChanged()
D/FourTeenActivity: 横屏
 D/FourTeenActivity: onConfigurationChanged2
 //切换竖屏
D/FourTeenActivity: onConfigurationChanged()
D/FourTeenActivity: 竖屏
D/FourTeenActivity: onConfigurationChanged1

重写onConfigurationChanged,这里处理切屏布局变化和数据的存储恢复

@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
    super.onConfigurationChanged(newConfig);
 Log.d(TAG,"onConfigurationChanged()");
 int orientation=newConfig.orientation;
 if (orientation==ORIENTATION_LANDSCAPE){
        Log.d(TAG,"横屏");
 }else {
        Log.d(TAG,"竖屏");
 }
    Log.d(TAG,"onConfigurationChanged"+orientation);
}

configChanges的可配置属性如下:
mcc :IMSI移动台国家代码(MCC)发生变化
keyboardHidden:键盘的可访问性发生变化
orientation:屏幕方向发生变化-用户旋转了屏幕
screenSize :当前屏幕大小发生变化
。。。。
我们还可以直接通过Activity的screenOrientation属性控制,

<activity android:name=".FourTeenActivity"
 android:screenOrientation="portrait">横屏
 <activity android:name=".FourTeenActivity"
 android:screenOrientation="landscape">竖屏

screenOrientation属性控制
unspecified:默认值,系统自动选择屏幕方向
behind:跟activity堆栈中的下面一个activity的方向一致
landscape:横屏方向
portrait:竖屏方向
sensor:由设备的物理方向传感器决定,如果用户旋转了设备,这屏幕就会横竖屏切换
nosensor:忽略物理方向传感器
reverseLandscape:api9以上,反向横屏
reversePortrait:api9以上,反向竖屏

END:凡心所向,素履以往;生如逆旅,一苇以航。

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 1月30日

Android 动画之Lottie动画使用

Android 动画之Lottie动画使用

一:简介
Lottie是Airbnb开源的一套跨平台的完整解决方案,设计师只需要使用After Effects(简称AE)设计动画之后,使用Lottic提供的Bodymovin插件将设计好的动画导出成JSON格式,就可以直接iOS,Android和React Native之上,无需关心中间的实现细节。
image.png
二:Android下使用Lottie
1.添加依赖在app下的build.gradle下添加
//lottie
implementation 'com.airbnb.android:lottie:3.4.4'
2.把动画的JSON文件,放在app/src/main/assets目录下
image.png
JSON字符串的最外层的结构

{
    "v": "4.6.0",               //bodymovin的版本
    "fr": 29.9700012207031,     //帧率
    "ip": 0,                    //起始关键帧
    "op": 141.000005743048,     //结束关键帧
    "w": 800,                   //动画宽度
    "h": 800,                   //动画高度
    "ddd": 0, 
    "assets": [...]             //资源信息
    "layers": [...]             //图层信息
}

3.最简单的使用是直接使用LottieAnimationView,LottieAnimationView 直接继承自 AppCompatImageView在Layout布局文件中

<com.airbnb.lottie.LottieAnimationView
 android:layout_width="wrap_content"
 android:layout_height="wrap_content"
 app:lottie_autoPlay="true"
 app:lottie_fileName="draw_rose.json"
 app:lottie_loop="true"/>

它支持几个属性:
lottie_fileName:播放动画的json文件
lottie_loop:是否循环播放,默认为false
lottie_autoPlay:是否加载完之后,自动播放,默认是false
或者代码中实现

LottieAnimationView lottieAnimationView=findViewById(R.id.animation_view);
lottieAnimationView.setAnimation("heart.json");
lottieAnimationView.loop(true);
lottieAnimationView.playAnimation();

3.1添加一个动画监听

lottieAnimationView.addAnimatorListener(new Animator.AnimatorListener() {
    @Override
 public void onAnimationStart(Animator animation) {
        Log.d("aa","动画开始");
 }
    @Override
 public void onAnimationEnd(Animator animation) {
        Log.d("aa","动画结束");
 }
    @Override
 public void onAnimationCancel(Animator animation) {
        Log.d("aa","动画取消");
 }
    @Override
 public void onAnimationRepeat(Animator animation) {
        Log.d("aa","动画每一次调用执行属性动画");
 }
});

3.2动画里有图片,不仅是画上去的,还有些图片资源使用本地的图片资源,可以使用setImageAssetsFolder("/images");
这个是加载资产目录assets下images目录的图片
image.png

设置一个 Lottie 动画中使用到的图片文件夹的相对路径,并确保他们和 bodymovin 插件输出的用到的图片文件名称,保持不变

3.3从网络上下载图片

lottieAnimationView.setImageAssetDelegate(new ImageAssetDelegate() {
    @Nullable
 @Override public Bitmap fetchBitmap(LottieImageAsset asset) {
        return null;
 }
});

3.4缓存

//Lottie内部有两个缓存map(强引用缓存,弱引用缓存),在动画文件加载完成后会根据设置的缓存策略缓存动画,方便下次使用。
lottieAnimationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Strong);    //强缓存

lottieAnimationView.setAnimation(animation, LottieAnimationView.CacheStrategy.Weak);      //弱缓存

3.5硬件加速,解决lottie卡顿问题

lottieAnimationView.useHardwareAcceleration(true);

LottieFiles网站https://lottiefiles.com/

Github路径:https://github.com/airbnb/lottie-android/tree/v3.4.4

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 1月29日

Android json数据解析之Gson和FastJson

Android json数据解析之Gson和FastJson的使用

一:Gson使用

Gson是谷歌提供的一个开源库,可以用来解析JSON数据。
1.什么是json
json就是用来进行网络数据传输的一种格式,目前的使用很广泛,其格式就是一种键值对的形式,很方便进行解析。json有Json对象(JsonObject)和Json数组(JsonArray)两种形式。凡是以{}就是一个Json对象,凡是[]就是一个Json数组。

{//一个Json对象
 "user": "user1",
  "pwd": "123456"
}
{//这个对象有4个字段,其中一个是数组
  "user": "user1",
   "pwd": "123456",
   "jsonArray": [//一个Json数组里面有2个Json对象,每个Json对象有2个字段
             {"user": "user1",
               "pwd": "123456"},
      
              {"user": "user2",
              "pwd": "123456"}
                        ],
   "result": "成功"            
}

2.添加Gson依赖
这是github上19.1kStar项目
image.png
在app下build.gradle添加依赖
implementation 'com.google.code.gson:gson:2.8.6'
3.使用GsonFormat工具
Gson是采用对象映射的方式,所有要为JSON数据创建一个Java对象来与之对应。
1.安装GsonFormat插件
2.新建一个类,在类里面按Alt+S键,并将要转换的Json数据复制进去
4.Gson使用

public class User implements Serializable {
    /**
 * user : user1 * pwd : 123456 * jsonArray : [{"user":"user1","pwd":"123456"},{"user":"user2","pwd":"123456"}] * result : 成功
 */
 private String user;
 private String pwd;
 private String result;
 private List<JsonArrayBean> jsonArray;
 public static class JsonArrayBean implements Serializable {
        /**
 * user : user1 * pwd : 123456 */
 private String user;
 private String pwd;
 public String getUser() {
            return user;
 }
        public void setUser(String user) {
            this.user = user;
 }
        public String getPwd() {
            return pwd;
 }
        public void setPwd(String pwd) {
            this.pwd = pwd;
 }
    }
    public String getUser() {
        return user;
 }
    public void setUser(String user) {
        this.user = user;
 }
    public String getPwd() {
        return pwd;
 }
    public void setPwd(String pwd) {
        this.pwd = pwd;
 }
    public String getResult() {
        return result;
 }
    public void setResult(String result) {
        this.result = result;
 }
    public List<JsonArrayBean> getJsonArray() {
        return jsonArray;
 }
    public void setJsonArray(List<JsonArrayBean> jsonArray) {
        this.jsonArray = jsonArray;
 }
}

解析JSON对象

String jsonData="要解析的Json字符串";
Gson gson=new Gson();
User user=gson.fromJson(jsonData, User.class);
String result=user.getResult();
Log.d("aa",result);
===========
aa: 成功

解析Json对象里面的数组

  // 因为jsonArray字段对应的是一个JSON数组,所以要用一个List来对应
List<User.JsonArrayBean> jsonArrayBeans=user.getJsonArray();
//取值
for (int i=0;i<jsonArrayBeans.size();i++){
  User.JsonArrayBean jsonArrayBean=jsonArrayBeans.get(i);
 String u=jsonArrayBean.getUser();
 Log.d("bb",u);
 ================
 取到的值
bb: user1
bb: user2

解析纯数组

[
    {"user": "user8",
      "pwd": "123456"},
      
      {"user": "user9",
      "pwd": "123456"}
]
//使用方式
Gson gson=new Gson();
List<Data> datas=gson.fromJson(jsonData, new TypeToken<ArrayList<Data>>(){}.getType());
for (int i=0;i<datas.size();i++){
  Data data=datas.get(i);
 String u=data.getUser();
 Log.d("cc",u);
}
//打印结果
cc: user8
cc: user9

序列化对象成Json(Teacher对象下4个属性 age,email,isDeveloper,name)

Gson gson=new Gson();
Teacher teacher=new Teacher("Rocky","111@qq.com",22,true);
String teacherJson=gson.toJson(teacher);
Log.d("bb",teacherJson);

打印结果
 bb {"age":22,"email":"111@qq.com","isDeveloper":true,"name":"Rocky"}

如果变量为空值的话序列化和反序列化
1.可见某个变量为null时,Gson在序列化的时候直接把这个变量忽略了
2.对于JSON字符串里没有的变量,Gson在反序列化时会给它一个默认值,int类型默认为0,bool类型默认为false,String类型默认为null

Gson gson=new Gson();
Teacher teacher=new Teacher("Rocky",null,22,true);
String teacherJson=gson.toJson(teacher);
Log.d("bb",teacherJson);

image.png
控制序列化/反序列化的变量名称
以Teacher对象为例,假如JSON字符串的变量名name变成fullName,无需紧张,我们也不用把Teacher对象的变量name改成fullName,然后把get,set方法全改了。只需要用Gson提供的注解@SerializedName就行。

public class Teacher{
@SerializedName("fullName")
//@SerializedName(value = "name", alternate = "fullName")
//然而现实远比想象中复杂,这个JSON有时候传的是`fullName`,有时候传的是`name`,这时该怎么办呢? 不用担心,`@SerializedName` 接受两个参数,`value` 和 `alternate` ,顾名思义,`alternate` 是备选变量名
private String name;
private String email;
private int age;
private boolean isDeveloper;
}
打印Json结果
{"age":22,"isDeveloper":true,"fullName":"Rocky"}

序列化/反序列化过程中忽略某些变量

public class Teacher  {
    @SerializedName("fullName")
    private String name;
 private String email;
 //@Expose()参与序列化和反序列化
 //@Expose(serialize = false)不参与序列化,只参与反序列化
 // @Expose(deserialize = false)只参与序列化,不参与反序列化
 @Expose(serialize = false,deserialize = false)//序列化和反序列化都不参与
 private int age;
 private boolean isDeveloper;
 }

然而:要使用这个注解来控制序列化和反序列化,就不能使用默认的Gson对象

//Gson gson=new Gson();不能通过这种方式构建Gson对象
GsonBuilder builder=new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
//builder.setPrettyPrinting();
Gson gson=builder.create();
Teacher teacher=new Teacher("Rocky","11@qq.com",22,true);
String teacherJson=gson.toJson(teacher);
Log.d("bb",teacherJson);
打印结果
bb: {}  我不知道为什么这种方式出现的结果是空,按理说我只是限定一个属性的序列化方式按照网上说的方式构建的Gson也不能
我把builder.excludeFieldsWithoutExposeAnnotation();换成builder.setPrettyPrinting();可以打印出来
但是没有限制字段的序列化和反序列化

方式二:transient字段
 通过private transient int age;
 
  Gson gson=new Gson();
 Teacher teacher=new Teacher("Rocky","11@qq.com",22,true);
 String teacherJson=gson.toJson(teacher);
 Log.d("bb",teacherJson);
 打印结果
{"email":"11@qq.com","isDeveloper":true,"fullName":"Rocky"}

二:FastJson使用
FastJson是一个Java语言的编写的高性能的Json处理器,由阿里巴巴公司开发的,github上23kStar
image.png
1.添加依赖
implementation 'com.alibaba:fastjson:1.2.73'
2.序列化Json,把对象转成Json串调用JSON.toJSONString

tudent student=new Student("Mike",12);
String studentJson =JSON.toJSONString(student,true);//为true就是要格式化Json,flase就是非格式化Json
//Gson的builder.setPrettyPrinting();格式化Json
Log.d("aa",studentJson);
打印结果:
aa: {
        "age":12,
        "name":"Mike"
    }

3.反序列化Json,把Json串转成对象JSON.parseObject

String json = "{"age":18, "name":"Rocky"}";
Student student=JSON.parseObject(json,Student.class);//要记得Student 添加一个无参的构造方法
Log.d("bb",student.getName());
Log.d("bb",student.getAge()+"");

打印结果
bb: Rocky
bb: 18

如果没有无参的构造方法会报错误
image.png
Json转成复杂的Bean,如List ,Set,Map

public class Student {
 private String name;
 private int age;
 ....忽略get,set方法
 }
一:List方式
String json = "[{"age":18, "name":"Rocky"},{"age":19, "name":"jack"}]";
List<Student> lists=JSON.parseObject(json,new TypeReference<ArrayList<Student>>(){});
for (int i=0;i<lists.size();i++){
    Student student=lists.get(i);
 Log.d("cc",student.getName());
 Log.d("cc",student.getAge()+"");
}

二:Set方式
String json = "[{"age":18, "name":"Rocky"},{"age":19, "name":"jack"}]";
Set<Student> lists=JSON.parseObject(json,new TypeReference<HashSet<Student>>(){});
for (Student student:lists){
    Log.d("cc",student.getName());
 Log.d("cc",student.getAge()+"");
}
打印结果
cc: Rocky
cc: 18

cc: jack
cc: 19

@JSONField注解的使用
1.作用于get方法上:当把注解作用于get方法上的时候,在把Java对象转化成Json字符串的时候,Java对象中的字段名会转化为注解中的name指定的字段名。

@JSONField(name = "myAge")
public int getAge() {
    return age;
}

Student student=new Student();
student.setAge(15);
student.setName("Rock");
String studentJson =JSON.toJSONString(student,true);
Log.d("aa",studentJson);

打印结果
aa: {
        "myAge":15,//变成我们注释的属性名
        "name":"Rock"
    }

2 作用于set方法上:在把json字符串转化成java对象的时候,注解中的name指定的字段名会映射为Java对象中的字段名。

@JSONField(name = "myName")
public void setName(String name) {
    this.name = name;
}

 String json = "{"age":2,"myName":"ss"}";//这个Json串的属性换成标记的
Student student= JSON.parseObject(json,Student.class);
 Log.d("cc",student.getName());
 Log.d("cc",student.getAge()+"");
 
 打印结果
 cc: ss
 cc: 2

3 @JSONField作用于java类的字段上

public class Student {
    @JSONField(name = "name", alternateNames = {"myName", "meName"})
    private String name;
 private int age;
 }
 
  String json = "{"age":2,"meName":"ss"}";//这个name字段可以是myName,也可以是meName都可以映射到name字段上
Student student= JSON.parseObject(json,Student.class);
 Log.d("cc",student.getName());
 Log.d("cc",student.getAge()+"");
 
 打印结果
 cc: ss
 cc: 2

Java Bean对象与JSONObject 对象相互转换
Java Bean对象--->JSONObject对象--->json字符串

 student.setAge(15);
 student.setName("Rock");
 JSONObject jsonObject= (JSONObject) JSON.toJSON(student);//JSONObject对象
 String studentJson= jsonObject.toJSONString();//转成Json字符串,toString()也可以
 Log.d("aa",studentJson);

json字符串与JSONObject对象转换
json字符串--->JSONObject对象

String json = "{"age":2,"meName":"ss"}";
JSONObject studentObject= JSON.parseObject(json);

json字符串转成Java集合可以直接使用JSON.parseArray()

String json = "[{"age":18, "name":"Rocky"},{"age":19, "name":"jack"}]";
List<Student> lists=JSON.parseArray(json,Student.class);
for (Student student:lists){
    Log.d("cc",student.getName());
 Log.d("cc",student.getAge()+"");
}

END:所求皆吾愿,所行化坦途

查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 提出了问题 · 1月29日

Gson解析的@Expose()注释使用问题

public class Teacher implements Serializable {
    @SerializedName("fullName")
    private String name;
 private String email;
 //@Expose()参与序列化和反序列化
 //@Expose(serialize = false)不参与序列化,只参与反序列化
 // @Expose(deserialize = false)只参与序列化,不参与反序列化
 @Expose(serialize = false,deserialize = false)//序列化和反序列化都不参与
 private  int age;
 private boolean isDeveloper;
 public Teacher(String name, String email, int age, boolean isDeveloper) {
        this.name = name;
 this.email = email;
 this.age = age;
 this.isDeveloper = isDeveloper;
 }
    public String getName() {
        return name;
 }
    public void setName(String name) {
        this.name = name;
 }
    public String getEmail() {
        return email;
 }
    public void setEmail(String email) {
        this.email = email;
 }
    public int getAge() {
        return age;
 }
    public void setAge(int age) {
        this.age = age;
 }
    public boolean isDeveloper() {
        return isDeveloper;
 }
    public void setDeveloper(boolean developer) {
        isDeveloper = developer;
 }
}
GsonBuilder builder=new GsonBuilder();
builder.excludeFieldsWithoutExposeAnnotation();
Gson gson=builder.create();
Teacher teacher=new Teacher("Rocky","11@qq.com",22,true);
String teacherJson=gson.toJson(teacher);
Log.d("bb",teacherJson);

打印结果
bb {}

使用@Expose注解方式限制一个属性字段的序列化和反序列化结果,确实不能使用
Gson对象构建是按照网上说的使用,没有效果

关注 1 回答 0

Rocky_ruan 发布了文章 · 1月27日

Android 使用序列化Serializable和Parcelable

Android 序列化Serializable和Parcelable使用和区别

一:Serializable

1.什么是序列化
将一个类对象转换成可存储,可传输状态的过程。
2.什么是Serializable接口
一个对象序列化的接口,一个类只有实现了Serializable接口,它的对象才能被序列化。
3.为什么要序列化对象
1.把对象转换为字节序列的过程称为对象的序列化
2.把字节序列恢复为对象的过程称为对象的反序列化
4.序列化用途
1.想把对象的状态信息通过网络进行传输
2.需要把对象持久化存储,存储在文件,数据库等

public class User implements Serializable {
 private static final long serialVersionUID = 1L;
 private String name;
 private int age;
 public String getName() {
        return name;
 }
    public void setName(String name) {
        this.name = name;
 }
    public int getAge() {
        return age;
 }
    public void setAge(int age) {
        this.age = age;
 }
}

当我跟进去,发现Serializable是一个空接口,一个接口什么都没有,我们可以将它理解成一个标识接口

例如:在课堂上有位学生遇到一个问题,于是举手向老师请教,这时老师帮他解答,那么这位学生的举手其实就是一个标识,自己解决不了问题请教老师帮忙解决。在Java中的这个Serializable接口其实是给jvm看的,通知jvm,我不对这个类做序列化了,你(jvm)帮我序列化就好了。

image.png
5.为什么要定义serialVersionUID变量
image.png
从说明中可以看出,如果我们没有自己申明一个serialVersionUID变量,接口会默认生成一个serialVersionUID
*但是强烈建议用户自定义一个serialVersionUID,因为默认的serialVersionUID对于class的细节非常敏感,反序列化时可能会导致
InvalidClassException这个异常。*
5.transient关键字
用来表示一个成员变量不是该对象序列化的一部分。当一个对象被序列化的时候,transient型变量的值不包括在序列化结果中。注:static修饰的静态变量天然的就是不可序列化的

private static final long serialVersionUID = 1L;
private String name;
private transient int age;

transient关键字只能修饰变量,而不能修饰方法和类。
被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。

二:Parcelable

Parcelable是Android为我们提供的序列化的接口,Parcelable的效率相对于Serializable也高许多。
Parcelable不能使用在要将数据存储在磁盘上情况
在内存中使用Parcelable性能优于Serializable

public class User implements Parcelable {
 private String name;
 private int age;
 public User(String name, int age) {
 this.name = name;
 this.age = age;
}
 /**
 * 从序列化后的对象中创建原始数据*/
 protected User(Parcel in) {
        name = in.readString();
 age = in.readInt();
 }
    /**
 * 序列化:将当前对象写入序列化结构中
 */
 @Override
 public void writeToParcel(Parcel dest, int flags) {
 dest.writeString(name);
 dest.writeInt(age);
 }
    /**
 * 当前对象的内容描述,存在文件描述符时返回1,其余全返回0
 */
 @Override
 public int describeContents() {
        return 0;
 }
    /**
 * 反序列化
 */
 public static final Creator<User> CREATOR = new Creator<User>() {
        /**
 * 将序列化对象中创建原始数据*/
 @Override
 public User createFromParcel(Parcel in) {
            return new User(in);
 }
        /**
 * 创建指定长度的原始对象数组*/
 @Override
 public User[] newArray(int size) {
            return new User[size];
 }
    };
 @Override
 public String toString() {
        return "User{" +
                "name='" + name + ''' +
                ", age=" + age +
                '}';
 }
}

1.Parcel英文译为包裹,Android采用这个词表示封装消息的数据。这个通过IBinder通信的消息载体。Parcel用来存放数据的内存(RAM),而不是永久介质。

//获取一个Parcel容器
Parcel parcel=Parcel.obtain();
//需要序列化的对象
User user=new User("mike",13);
//把对象写入Parcel
parcel.writeParcelable(user,0);
//Parcel读写共用一个位置计数,这里一定要重置一下当前的位置
parcel.setDataPosition(0);
//读取Parcel
User user1=parcel.readParcelable(User.class.getClassLoader());
Log.d("LoginActivity",user1.toString());

调用parcel.writeParcelable(user,0);源码解析

public final void writeParcelable(@Nullable Parcelable p, int parcelableFlags) {
//判断p是否为空
    if (p == null) {
        writeString(null);
 return; }
 //先写入P的类名(user就是p类类名)
    writeParcelableCreator(p);
  //调用我们重写的writeToParcel方法,按顺序写入
 p.writeToParcel(this, parcelableFlags);
}
/** @hide */
@UnsupportedAppUsage
public final void writeParcelableCreator(@NonNull Parcelable p) {
//写入p类的类名
    String name = p.getClass().getName();
 writeString(name);
}

调用parcel.readParcelable(User.class.getClassLoader());源码解析

public final <T extends Parcelable> T readParcelable(@Nullable ClassLoader loader) {
//调用readParcelableCreator
//这时我们获取就是我们自定义的CREATOR
    Parcelable.Creator<?> creator = readParcelableCreator(loader);
 if (creator == null) {
        return null;
 }
 // 判断当前creator是不是Parcelable.ClassLoaderCreator<?>的实例
    if (creator instanceof Parcelable.ClassLoaderCreator<?>) {
    //如果是的话,,我们调用reateFromParcel(this, loader)
      Parcelable.ClassLoaderCreator<?> classLoaderCreator =
          (Parcelable.ClassLoaderCreator<?>) creator;
 return (T) classLoaderCreator.createFromParcel(this, loader);
 }
   //调用我们自定义的CREATOR中重写的createFromParcel方法
    return (T) creator.createFromParcel(this);
}
public final Parcelable.Creator<?> readParcelableCreator(@Nullable ClassLoader loader) {
//首先把类名读取出来
    String name = readString();
 if (name == null) {
        return null;
 }
    Parcelable.Creator<?> creator;
     //mCreators做了一下缓存,如果之前某个classloader把一个parcelable的Creator获取过
    //那么就不需要通过反射去查找了
 synchronized (mCreators) {
        HashMap<String,Parcelable.Creator<?>> map = mCreators.get(loader);
 if (map == null) {
            map = new HashMap<>();
 mCreators.put(loader, map);
 }
        creator = map.get(name);
 if (creator == null) {
            try {
                // If loader == null, explicitly emulate Class.forName(String) "caller
 // classloader" behavior. ClassLoader parcelableClassLoader =
                        (loader == null ? getClass().getClassLoader() : loader);
 // Avoid initializing the Parcelable class until we know it implements
 // Parcelable and has the necessary CREATOR field. http://b/1171613. Class<?> parcelableClass = Class.forName(name, false /* initialize */,
 parcelableClassLoader);
 if (!Parcelable.class.isAssignableFrom(parcelableClass)) {
                    throw new BadParcelableException("Parcelable protocol requires subclassing "
 + "from Parcelable on class " + name);
 }
                Field f = parcelableClass.getField("CREATOR");
 if ((f.getModifiers() & Modifier.STATIC) == 0) {
                    throw new BadParcelableException("Parcelable protocol requires "
 + "the CREATOR object to be static on class " + name);
 }
                Class<?> creatorType = f.getType();
 if (!Parcelable.Creator.class.isAssignableFrom(creatorType)) {
                    // Fail before calling Field.get(), not after, to avoid initializing
 // parcelableClass unnecessarily. throw new BadParcelableException("Parcelable protocol requires a "
 + "Parcelable.Creator object called "
 + "CREATOR on class " + name);
 }
                creator = (Parcelable.Creator<?>) f.get(null);
 }
            catch (IllegalAccessException e) {
                Log.e(TAG, "Illegal access when unmarshalling: " + name, e);
 throw new BadParcelableException(
                        "IllegalAccessException when unmarshalling: " + name);
 }
            catch (ClassNotFoundException e) {
                Log.e(TAG, "Class not found when unmarshalling: " + name, e);
 throw new BadParcelableException(
                        "ClassNotFoundException when unmarshalling: " + name);
 }
            catch (NoSuchFieldException e) {
                throw new BadParcelableException("Parcelable protocol requires a "
 + "Parcelable.Creator object called "
 + "CREATOR on class " + name);
 }
            if (creator == null) {
                throw new BadParcelableException("Parcelable protocol requires a "
 + "non-null Parcelable.Creator object called "
 + "CREATOR on class " + name);
 }
            map.put(name, creator);
 }
    }
    return creator;
}
查看原文

赞 0 收藏 0 评论 0

Rocky_ruan 发布了文章 · 1月19日

Android 组件化开发ButterKnife配置和相关问题

ButterKnife 组件化相关配置

一:官方给butterKnife的模块化使用方式和我的组件化项目结构
image.png

image.png
二:组件化中ButterKnife使用
1.在工程project的build.gradle下添加butterKnife插件依赖

classpath "com.android.tools.build:gradle:3.5.4"
classpath 'com.jakewharton:butterknife-gradle-plugin:10.2.0'//butterKnife组件化第一步

注:这有一个Gradle和ButterKnife版本编译冲突问题
2.在common_base库公共依赖库build.gradle中添加

//ButterKnife第三步功能基础组件中依赖
api rootProject.ext.dependencies["butterknife"]//等价于:api 'com.jakewharton:butterknife:10.2.0'
//annotationProcessor rootProject.ext.dependencies["butterknifeCompiler"]//这个每个用到butterKnife都需要添加故基础组件可以不添加

使用api是为了其他依赖这个库都能使用,不用重复添加,具体可以看看api,implementation ,compile区别
3.App壳模块中添加,是application模块

apply plugin: 'com.android.application'
implementation project(path: ':common_base')
//黄油刀ButterKnife
//这个东西一定要放在这个里面,否则会报错的(空指针)!
annotationProcessor rootProject.ext.dependencies["butterknifeCompiler"]//等价于:annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'

4.在其他lib库模块需要添加

apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'//相应的库module模块添加这个
implementation project(path: ':common_base')
//黄油刀ButterKnife
annotationProcessor rootProject.ext.dependencies["butterknifeCompiler"]//等价于:annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'

三:ButterKnife使用相关问题
1.Gradle和ButterKnife版本冲突相关问题,具体不知,一般是降低Gradle版本
2.butterknife报空指针问题 需要添加annotationProcessor 'com.jakewharton:butterknife-compiler:10.2.0'
3.在其它Module中使用Butterknfie怎么使用(主要是解决R资源的问题)

@BindView(R2.id.login_tv)//通过R2
TextView login_tv;

ButterKnife.bind(this);

原因:当作为库的一部分构建时,R.java生成的类中的值不会被声明为“final”.用于@BindView()的注释需要这些R.值是最终的,以后不会更改.
故:ButterKnife就使用了一个插件,拷贝R中所有的值到R2中,并且把R2中的值都设置成了final类型的,这样就能蒙混过关,反正子模块中的值可能还是要重新设置的。

查看原文

赞 0 收藏 0 评论 0

认证与成就

  • 获得 0 次点赞
  • 获得 1 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 1 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 1月8日
个人主页被 688 人浏览