Dependency Injection with Dagger 2
原文:Dependency Injection with Dagger 2
创建单例
下面展示一个简单的Dagger来管理所有单例创建的例子:
原文:Dependency Injection with Dagger 2
下面展示一个简单的Dagger来管理所有单例创建的例子:
Dagger 是一款依赖注入框架。
项目地址:dagger
官方文档:Dagger
依赖注入(Dependency Injection),简称DI,又叫控制反转(Inversion of Control),简称IOC。
当一个类的实例需要另另一个类的实例进行协助时,在传统的设计中,通常由调用者来创建被调用者的实例,然而依赖注入的方式,创建被调用者不再由调用者创建实例,创建被调用者的实例的工作由IOC容器来完成,然后注入到调用者。因此也被称为依赖注入。
作用:将各层的对象以松耦合的方式组织在一起,解耦,各层对象的调用完全面向接口。当系统重构或者修改的时候,代码的改写量将大大减少。
1 | implementation 'com.google.dagger:dagger-android:2.17' |
由于Android系统的开放性,任何用户、开发者、硬件厂商、运营商都可以对Android系统和硬件进行定制,修改成他们想要的样子。 那么这种“碎片化”到达什么程度呢?
以上每一个矩形都代表一种机型,且它们屏幕尺寸、屏幕分辨率大相径庭。随着Android设备的增多,设备碎片化、系统碎片化、屏幕尺寸碎片化、屏幕碎片化的程度也在不断加深。
当 Android 系统、屏幕尺寸、屏幕密度出现碎片化的时候,就很容易出现同一元素在不同手机上显示不同的问题。试想一下这么一个场景: 为 4.3 寸屏幕准备的 UI 设计图,运行在 5.0 寸的屏幕上,很可能在右侧和下侧存在大量的空白;而 5.0 寸的 UI 设计图运行到 4.3 寸的设备上,很可能显示不下。
为了保证用户获得一致的用户体验效果,使得某一元素在 Android 不同尺寸、不同分辨率的、不同系统的手机上具备相同的显示效果,能够保持界面上的效果一致,我们需要对各种手机屏幕进行适配!
单位:px(pixel),像素就是手机屏幕的最小构成单元
px(pixel),1px = 1像素点,手机在横向、纵向上的像素点数总和 一般描述成 宽高 ,即横向像素点个数 纵向像素点个数(如1080 x 1920)。
单位 英寸(inch),手机对角线的物理尺寸
单位:dpi,每英寸的像素点数。 例如每英寸内有 160 个像素点,则其像素密度为 160dpi。计算公式:像素密度 = 像素 / 尺寸 (dpi = px / in)
density-independent pixel,叫dp或dip,与终端上的实际物理像素点无关。单位:dp,可以保证在不同屏幕像素密度的设备上显示相同的效果,是安卓特有的长度单位。dp 与 px 的转换:1dp = (dpi / 160 ) * 1px
scale-independent pixel,叫sp或sip。单位:sp,字体大小专用单位 Android开发时用此单位设置文字大小。
DPI 的存在,不就是为了让大屏能显示更多的内容
参考项目地址:https://github.com/JessYanCoding/AndroidAutoSize
今日头条适配方案默认项目中只能以高或宽中的一个作为基准。
density 是 DisplayMetrics 中的成员变量,而 DisplayMetrics 实例通过 Resources#getDisplayMetrics 可以获得,而Resouces 通过 Activity 或者 Application 的 Context 获得。
如果每个 View 的 dp 值是固定不变的,那我们只要保证每个设备的屏幕总 dp 宽度不变,就能保证每个 View 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 dp 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 dp 值
屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度
在这个公式中我们要保证 屏幕的总 dp 宽度 和 设计图总宽度 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?屏幕的总 px 宽度 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了
当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density
这个公式就是把上面公式中的 屏幕的总 dp 宽度 换成 设计图总宽度,原理都是一样的,只要 density 根据不同的设备进行实时计算并作出改变,就能保证 设计图总宽度 不变,也就完成了适配。
布局文件中 dp 的转换,最终都是调用 TypedValue#applyDimension(int unit, float value, DisplayMetrics metrics)
来进行转换:
1 | public static float applyDimension(int unit, float value, DisplayMetrics metrics){ |
这里用到的 DisplayMetrics 正是从 Resources 中获得的。图片的decode,也是通过 DisplayMetrics 中的值来计算的。
当然还有些其他 dp 转换的场景,基本都是通过 DisplayMetrics 来计算的,这里不再详述。因此,想要满足上述需求,我们只需要修改 DisplayMetrics 中和 dp 转换相关的变量即可。
适配后的 density = 设备真实宽(单位px) / 360,接下来只需要把我们计算好的 density 在系统中修改下即可,代码实现如下:
1 | private static float sNonCompatDensity; |
会影响第三方控件
解决方案:
这个方案的的使用方式和我们平时在布局中引用 dimens 无异,核心点在于生成 dimens.xml 文件,但是已经有大神帮我们做了这 一步。
1 | ├── src/main |
如果有人还记得上面这种 宽高限定符屏幕适配方案 的话,就可以把 smallestWidth 限定符屏幕适配方案 当成这种方案的升级版,smallestWidth 限定符屏幕适配方案 只是把 dimens.xml 文件中的值从 px 换成了 dp,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,smallestWidth 限定符屏幕适配方案 长这样
1 | ├── src/main |
其实 smallestWidth 限定符屏幕适配方案 的原理也很简单,开发者先在项目中根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-sw
如果系统根据当前设备屏幕的 最小宽度 (smallestWidth) 没找到对应的 values-sw
根据 今日头条屏幕适配方案 优化的屏幕适配框架。
例如 Android 屏幕适配方案,不过已经停止维护
1 | ├── src/main |
Android 屏幕适配终结者 ,也是基于头条的原理,不过是操作 pt
,所以不是改 DisplayMetrics#density
,而是 DisplayMetrics#xdpi
,由于适配不会失效
下面是摘自 ReactiveX 官网 的一段话。
ReactiveX is a library for composing asynchronous and event-based programs by using observable sequences.(一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库)
其中提到了几个概念,观察者模式、响应式编程。
概念:响应式编程是一种通过异步和数据流来构建事物关系的编程模型。
一般的编码模式中,“人”在其中扮演了过重的角色,关心程序中的每一部分。某种意义上这是一种顺序性思维的编程,我要做什么,然后做什么,最后做什么,按部就班编写就好了。具体如下图:
而响应式编程,全都是事物与事物之间的关系,解脱了”人”,之后一个事物发生变化另一个事物就自动响应。如下:
个人感觉响应式编程就是用异步数据流进行编程。流是响应式的核心,可以基于任何东西创建数据流,响应式编程就是根据数据流的流向进行一系列的操作。
观察者模式(Observer Pattern):定义了对象间的一种一对多的依赖关系,当一个对象状态发生改变时,其相关依赖对象皆得到通知并被自动更新。观察者模式又叫做发布-订阅(Publish/Subscribe)模式、模型-视图(Model/View)模式、源-监听器(Source/Listener)模式或从属者(Dependents)模式。
观察者模式面向的需求是:A 对象(观察者)对 B 对象(被观察者)的某种变化高度敏感,需要在 B 变化的一瞬间做出反应。举个例子,新闻里喜闻乐见的警察抓小偷,警察需要在小偷伸手作案的时候实施抓捕。在这个例子里,警察是观察者,小偷是被观察者,警察需要时刻盯着小偷的一举一动,才能保证不会漏过任何瞬间。程序的观察者模式和这种真正的『观察』略有不同,观察者不需要时刻盯着被观察者(例如 A 不需要每过 2ms 就检查一次 B 的状态),而是采用 注册(Register) 或者称为 订阅(Subscribe) 的方式,告诉被观察者:我需要你的某某状态,你要在它变化的时候通知我。 Android 开发中一个比较典型的例子是点击监听器 OnClickListener
。对设置 OnClickListener
来说, View
是被观察者, OnClickListener
是观察者,二者通过 setOnClickListener()
方法达成订阅关系。订阅之后用户点击按钮的瞬间,Android Framework 就会将点击事件发送给已经注册的 OnClickListener
。采取这样被动的观察方式,既省去了反复检索状态的资源消耗,也能够得到最高的反馈速度。当然,这也得益于我们可以随意定制自己程序中的观察者和被观察者,而警察叔叔明显无法要求小偷『你在作案的时候务必通知我』。
我所理解的RxJava的核心优势应该是它可以对复杂逻辑进行拆分成为一个一个的Observable后,RxJava的各种操作符予这些解耦的Observable能够合理的进行再组织的能力,并且它给予了你足够丰富的再组织能力。这种分拆再组织的能力是十分强大的,只有运用好RxJava这种强大的能力,才能真正意义上使你原来非常复杂的揉在一团的逻辑代码变得清晰、简洁,本质上是因为RxJava给你提供了这种强大方便的组织能力,我觉得有点像一种编程模式,你可以放心的将复杂的逻辑拆块,最后RxJava给你提供了丰富的组织、变换、串联、控制这些块的能力,只有这个时候你才会真正觉得这是个好东西,而不应该是跟风使用,但是心里也说不清楚为什么要使用。
RxJava 有三个基本概念: Observable
(被观察者),Observer
(观察者),subscribe
(订阅)。Observable
和 Observer
通过 subscribe()
方法实现订阅关系,从而 Observable
可以在需要的时候发出事件来通知 Observer
。
Github 链接:
RxJava https://github.com/ReactiveX/RxJava
RxAndroid https://github.com/ReactiveX/RxAndroid
gradle 依赖:
implementation 'io.reactivex.rxjava2:rxjava:2.x.y'
implementation 'io.reactivex.rxjava2:rxandroid:2.x.y'
1 | Observable<String> observable = Observable |
可以看到,这里传入了一个 ObservableOnSubscribe
对象作为参数。ObservableOnSubscribe
会被存储在返回的 Observable
对象中,当 Observable
被订阅的时候,subscribe
方法就会自动被调用,事件序列就会依照设定依次触发(对于上面的代码,就是观察者 observer
将会被调用三次 onNext()
和一次 onComplete()
)。
此外,还可以通过其他方法来创建被观察者。
just(T...)
: 将传入的参数依次发送出来。
1 | Observable observable = Observable.just("Hello", "World"); |
fromArray(T...items)
: 将传入的数组或 Iterable 拆分成具体对象后,依次发送出来。
1 | String[] words = {"Hello", "Hi", "Aloha"}; |
除 Observable
外还有 Flowable
等被观察者类型。
1 | Observer<String> observer = new Observer<String>() { |
在观察者中进行响应事件对应的相关操作。
1 | observable.subscribe(observer); |
这里的写法是被观察者订阅了观察者,而不是观察者订阅被观察者,是为了保证流式API调用风格。
1 | observable |
上面就是一个非常简易的RxJava流式API的调用:同一个调用主体一路调用下来,一气呵成。
RxJava 的这个实现,是一条从上到下的链式调用,没有任何嵌套,这在逻辑的简洁性上是具有优势的。
整个流程如下图所示:
结合流程图的相应代码实例如下:
1 | //创建被观察者,是事件传递的起点 |
注意:当调用订阅操作(即调用Observable.subscribe()方法)的时候,被观察者才真正开始发出事件。
至此,在 RxJava 的默认规则中,事件的发出和消费都是在同一个线程的。
在 RxJava 中,通过 Scheduler
来指定每一段代码应该运行在什么样的线程。下表展示了RxJava中可用的调度器种类:
调度器类型 | 效果 |
---|---|
Schedulers.computation( ) | 用于计算任务,如事件循环或和回调处理,不要用于IO操作(IO操作请使用Schedulers.io());默认线程数等于处理器的数量 |
Schedulers.from(executor) | 使用指定的Executor作为调度器 |
Schedulers.immediate( ) | 在当前线程立即开始执行任务 |
Schedulers.io( ) | 用于IO密集型任务,如异步阻塞IO操作,这个调度器的线程池会根据需要增长;对于普通的计算任务,请使用Schedulers.computation();Schedulers.io( )默认是一个CachedThreadScheduler,很像一个有线程缓存的新线程调度器 |
Schedulers.newThread( ) | 为每个任务创建一个新线程 |
Schedulers.trampoline( ) | 当其它排队的任务完成后,在当前线程排队开始执行 |
AndroidSchedulers.mainThread() | Android 主线程 |
subscribeOn()
: 指定 subscribe()
所发生的线程,或者叫做事件产生的线程。
observeOn()
: 指定 Observer
所运行在的线程。或者叫做事件消费的线程。
1 | //new Observable.just()执行在新线程 |
注意:
subscribeOn()
它指示 Observable
在一个指定的调度器上创建(只作用于被观察者创建阶段)。只能指定一次,如果指定多次则以第一次为准observeOn()
指定在事件传递(加工变换)和最终被处理(观察者)的发生在哪一个调度器。可指定多次,每次指定完都在下一步生效。=======================================================
操作符 | 说明 |
---|---|
Create | 使用一个函数从头开始创建一个Observable |
Defer | 直到有观察者订阅时才创建Observable,并且为每个观察者创建一个新的Observable |
Empty | 创建一个不发射任何数据但是正常终止的Observable |
Never | 创建一个不发射数据也不终止的Observable |
Throw | 创建一个不发射数据以一个错误终止的Observable |
From | 将其它种类的对象和数据类型转换为Observable |
Interval | 创建一个按固定时间间隔发射整数序列的Observable |
Just | 创建一个发射指定值的Observable |
Range | 创建一个发射特定整数序列的Observable |
Repeat | 创建一个发射特定数据重复多次的Observable |
Start | 返回一个Observable,它发射一个类似于函数声明的值 |
Timer | 创建一个Observable,它在一个给定的延迟后发射一个特殊的值 |
操作符 | 说明 |
---|---|
操作符 | 说明 |
---|---|
=======================================================
1 | private void testRxJava() { |
1 |
|
参考资料:
1 | def room_version = "1.1.1" |
1 | @Entity(tableName = "user") |
1 | @Dao |
1 | @Database(entities = {User.class}, version = 1) |
1 | private fun testRoom() { |
1 | // product |
1 | class BaseApplication : Application() { |
1 | // 新建一个对象,并进行存储 |
1 | // product |
1 | class BaseApplication : Application() { |
1 | @Database(version = VERSION, name = NAME) |
1 | @Table(database = DBFlowDataBase.class) |
编译之后
1 | private fun testDBFlow() { |
下载地址:http://ormlite.com/releases/
需要下载最新的 core.jar 和 android.jar 并添加依赖
1 | @DatabaseTable(tableName = "user") |
1 | public class DatabaseHelper extends OrmLiteSqliteOpenHelper { |
1 | public abstract class BaseLazyFragment extends Fragment { |
1 | import android.os.Bundle; |
对于可见状态的生命周期调用顺序,父 Fragment总是优先于子 Fragment,而对于不可见事件,内部的 Fragment 生命周期总是先于外层 Fragment。
标识符命名法最要有四种:
<scope_> + <prefix_> + <qualifier>
范围前缀,类型前缀,限定词。尽可能的用最少的字符而又能完整的表达标识符的含义。
英文缩写原则:
下面为常见的英文单词缩写:
名称 | 缩写 |
---|---|
icon | ic |
color | cl |
divider | di |
selector | sl |
background | bg |
image | img |
password | pwd |
position | pos |
TextView | tv |
ImageView | iv |
EditText | et |
注意: 单词缩写原则:不要用缩写,除非该缩写是约定俗成的。
采用反域名命名规则,全部使用小写字母。一级包名为com,二级包名为xx(可以是公司或则个人的随便),三级包名根据应用进行命名,四级包名为模块名或层级名。
包名 | 此包中包含 |
---|---|
包名.activities | Activity类 |
包名.base | 自定义基类 |
包名.adapter | Adapter类 |
包名.tools | 公共工具方法类 |
包名.bean | 包中包含:实体类 |
包名.view (或 widget) | 自定义的View类等 |
包名.service | Service服务 |
… | … |
采用大驼峰命名法,尽量避免缩写,除非该缩写是众所周知的,比如 HTML , URL,如果类名称中包含单词缩写,则单词缩写的每个字母均应大写。
类 | 描述 | 例如 |
---|---|---|
activity | Aty或者Activity为后缀标识 | WelcomeAty.或者WelcomeActivity |
Adapter | Adapte 为后缀标识 | NewsAdp或NewsAdapter |
基础类 | 以Base开头 | BaseActivity,BaseFragment |
公共方法类 | Tools或Manager为后缀标识 | ThreadPoolManager, LogTools |
Service | 以Service为后缀标识 | TimeService |
… | … | … |
动词或动名词,采用小驼峰命名法例如: onCreate(), run()
采用小驼峰命名法。类中控件名称必须与xml布局id保持一致。
用统一的量词通过在结尾处放置一个量词,就可创建更加统一的变量,它们更容易理解,也更容易搜索。例如,请使用 strCustomerFirst
和 strCustomerLast
,而不要使用 strFirstCustomer
和 strLastCustomer
。
全部大写,采用下划线命名法.例如:MIN_WIDTH
全部小写,采用下划线命名法,加前缀区分
名称 | 功能 |
---|---|
btn_xx |
按钮图片使用btn_整体效果(selector) |
btn_xx_normal |
按钮图片使用btn_正常情况效果 |
btn_xx_press |
按钮图片使用btn_点击时候效果 |
bg_head |
背景图片使用bg_功能_说明 |
ic_more_help |
图标图片使用icon_功能_说明 |
… | … |
大小写规范与方法名一致,采用小驼峰命名法。命名规范为“资源控件的缩写 名”+“变量名”。注意:页面控件名称应该和控件id名保持一致
命名模式为:view缩写_模块名称_view的逻辑名称
控件 | 缩写 |
---|---|
RelativeView | rv |
TextView | tv |
Button | btn |
ImageView | iv |
ProgressBar | pb |
… | … |
注意: 如果layout文件很复杂,建议将layout分成多个模块,每个模块定义一个moduleViewHolder,其成员变量包含所属view
将layout中不断重现的style提炼出通用的style通用组件,放到styles.xml中
Android6.0以上把权限分为普通权限和危险权限,所以危险权限是需要动态申请,给予用户提示的。
看到上面的 permissions,会发现一个问题,危险权限都是一组一组的。
分组对权限机制的申请是有一定影响的。例如app运行在android 6.x的机器上,对于授权机制是这样的。如果你申请某个危险的权限,假设你的app早已被用户授权了同一组的某个危险权限,那么系统会立即授权,而不需要用户去点击授权。比如你的app对 READ_CONTACTS
已经授权了,当你的app申请 WRITE_CONTACTS
时,系统会直接授权通过。
此外,对于申请时的弹窗上面的文本说明也是对整个权限组的说明,而不是单个权限。
1.首先要保证在AndroidManifest中写明需要的权限。1
2
3<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CAMERA"/>
2.权限申请示例 以获取定位权限为例。
1 | btn.setOnClickListener(new View.OnClickListener() { |
如果有权限,执行获取位置逻辑,如果没权限,则进行请求权限。
1 | @Override |
如果同意,执行获取位置逻辑,如果拒绝,重写 shouldShowRequestPermissionRationale
方法,返回 true,向用户弹窗给出一个获取权限的提示,点击后再次申请权限。
1 | public boolean shouldShowRequestPermissionRationale(@NonNull String permission) { |
重写shouldShowRequestPermissionRationale,在申请位置权限时,返回true,给用户解释。
完整代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79public class PermissionActivity extends AppCompatActivity implements View.OnClickListener {
private static final String TAG = "PermissionActivity";
public static final int REQUEST_CAMERA = 1;
private ConstraintLayout constraintLayout;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_permission);
initView();
}
private void initView() {
constraintLayout = findViewById(R.id.mainLayout);
findViewById(R.id.btn_camera).setOnClickListener(this);
findViewById(R.id.btn_contact).setOnClickListener(this);
}
@RequiresApi(api = Build.VERSION_CODES.M)
private boolean checkPermission(String permission) {
int i = checkSelfPermission(permission);
return i != PackageManager.PERMISSION_DENIED;
}
private void showSnackbar(String message) {
Snackbar.make(constraintLayout, message, Snackbar.LENGTH_SHORT).show();
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.btn_camera:
showCamera();
break;
case R.id.btn_contact:
break;
}
}
private void showCamera() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (checkPermission(Manifest.permission.CAMERA)) {
showSnackbar("已授权,可以打开");
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) {
Snackbar.make(constraintLayout, "Camera permission is needed", Snackbar.LENGTH_SHORT)
.setAction("确定", new View.OnClickListener() {
@Override
public void onClick(View v) {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}).show();
} else {
requestPermissions(new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
}
}
} else {
showSnackbar("可以直接打开");
}
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
switch (requestCode) {
case REQUEST_CAMERA:
if (grantResults.length > 0) {
if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
showSnackbar("已授权");
} else {
showSnackbar("拒绝授权");
}
}
break;
}
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}
另外,在看一些 权限申请 的三方库的时候,发现了一件很有意思的事情。通常代码很难追踪到 Activity 的生命周期,一些库就新建一个 Fragment 在其生命周期的各个回调方法里去调用相应的方法。很棒。
参考文章
Lottie 是 Airbnb 开源的一个面向 iOS、Android、React Native 的动画库,能分析 Adobe After Effects 导出的动画,并且能让原生 App 像使用静态素材一样使用这些动画,完美实现动画效果。
现在使用各平台的 native 代码实现一套复杂的动画是一件很困难并且耗时的事,我们需要为不同尺寸的屏幕加载不同的素材资源,还需要写大量难维护的代码,而 Lottie 可以做到同一个动画文件在不同平台上实现相同的效果,极大减少开发时间,实现不同的动画,只需要设置不同的动画文件即可,极大减少开发和维护成本。
官方效果图:
Lottie 支持多平台,使用同一个 JSON 动画文件,可在不同平台实现相同的效果。其中,Android 通过 Airbnb 的开源项目 lottie-android 实现,最低支持 API 16。
implementation 'com.airbnb.android:lottie:2.5.4'
Lottie 默认读取 Assets 中的文件,我们需要把动画文件 react.json 保存在 app/src/main/assets
文件里。在 官网 可以免费下载动画文件。
1 | // react.json |
在布局文件中直接添加 Lottie 的 LottieAnimationView 控件,即可在界面显示 React logo 动画效果
1 | <com.airbnb.lottie.LottieAnimationView |
属性说明:
app:lottie_fileName
: assets 文件夹下对应的动画文件的文件名app:lottie_loop
: 是否循环播放动画app:lottie_autoPlay
: 是否自动播放动画app:lottie_cacheStrategy
: 设置缓存策略。默认为none,可选值有strong和weakapp:lottie_colorFilter
: 设置背景颜色app:lottie_progress
: 设置动画播放进度app:lottie_imageAssetsFolder
: 动画所需图片资源在assets中的绝对路径。如果没有图片资源,可以省略代码使用:
1 | LottieAnimationView animationView = (LottieAnimationView) findViewById(R.id.animation_view); |
1 | StringBuilder stringBuilder = new StringBuilder(); |
1 | animationView.setImageAssetDelegate(new ImageAssetDelegate() { |
1 | animationView.setFontAssetDelegate(new FontAssetDelegate(){ |
1 | /** |
Lottie 的使用的资源是需要先通过 bodymovin( bodymovin 插件本身是用于网页上呈现各种AE效果的一个开源库) 将 Adobe After Effects (AE) 生成的aep动画工程文件转换为通用的json格式描述文件。Lottie则负责解析动画的数据,计算每个动画在某个时间点的状态,准确地绘制到屏幕上。
Lottie 对外通过控件 LottieAnimationView 暴露接口,控制动画。
LottieAnimationView 继承自 ImageView,通过当前时间绘制 canvas 显示到界面上。这里有两个关键类:LottieComposition 负责解析json描述文件,把json内容转成Java数据对象;LottieDrawable负责绘制,把LottieComposition转成的数据对象绘制成drawable显示到View上。顺序如下:
探究更多原理解析内容请在 Lottie–让动画如此简单 查看。
内容来自 腾讯音乐技术团队 。
LottieAnimationView.setAnimation(String, CacheStrategy)
。以下性能对比是以K歌内单个礼物动画效果
- | 属性动画 | lottie使用硬件加速 | lottie未使用硬件加速 |
---|---|---|---|
帧率 | |||
内存 | |||
cpu |
Lottie使用简单,易于上手,非常值得尝试。
最近使用 RecyclerView 的 notifyItemInserted
和 notifyItemRemoved
的时候,经常遇到这个异常。
1 | java.lang.IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter positionViewHolder{2064e5c6 position=2 id=-1, oldPos=2, pLpos:-1 scrap [attachedScrap] tmpDetached no parent} |
查了一些资料,大多是说因为数据源改变了,但是 adapter 却没有及时调用 notifyItemRangeChanged
。
自己写一个继承 LinearLayoutManager 的包装类,在 onLayoutChildren() 方法里 try-catch 捕获该异常。然后给 recyclerView 设置该 LayoutManager1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class WrapContentLinearLayoutManager extends LinearLayoutManager {
//... constructor
@Override
public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) {
try {
super.onLayoutChildren(recycler, state);
} catch (IndexOutOfBoundsException e) {
Log.e("probe", "meet a IOOBE in RecyclerView");
e.printStackTrace();
}
}
}
...
RecyclerView recyclerView = (RecyclerView)findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(activity, LinearLayoutManager.HORIZONTAL, false));
在进行数据移除和数据增加时,务必要保证RecyclerView的Adapter中的数据集和移除/添加等操作后的数据集保持一致!
这里,前者是在该Adapter内部,不妨叫做内部数据集,后者是开发人员传过给Adapter的,不妨叫外部数据集。更新RecyclerView数据时,需要保证外部数据集和内部数据集实时保持一致。每一次对外部数据集做改动时,都需要紧接着主动对外部数据集和内部数据集做一次同步操作
外部数据集同步到内部数据集,使用如下的方法:
这里对 notifyDataSetChange() 做些说明:
使用该方法的更新内部数据集,没有默认的动画效果,同时更新数据的效率页不如上面的方法,官方不推荐使用这种方式更新数据集。
使用notifyDataSetChanged同步外部数据集和内部数据集。该方法简单,但是失去了动画效果,并且更新数据的性能低。
另外,如果对外部数据集做了二次以上的操作,却只调用notifyDataSetChanged同步一次,也很有可能会报上文所示的错误。
参考文章:
Update your browser to view this website correctly. Update my browser now