我们一路奋斗,不是为了改变世界,而是为了不被世界改变。
ViewPager使用指南
ViewPager 是Android SDK提供的用于实现左右滑动切换页面效果的控件,接入非常简单,可以实现如下图的效果。
自顶向下地看,一个完整的包含ViewPager的页面由以下几个对象构成。
- Host:容器页面,可以是Activity,或者Fragment
- ViewPager:关联到页面上的一个View,可以左右滑动切换子页面
- Adapter:ViewPager内部用以获取每个子页面的适配器,参考RecyclerView/ListView的Adapter
- SubFragment:ViewPager内嵌的子页面
让我们逐个分析(Host就是一个普通页面,略过不提,SubFragment也一样,与常见写法没有区别,同样略过)
ViewPager
相当于一个ViewGroup容器,使用的时候,首先在xml布局里声明android.support.v4.view.ViewPager,接着在代码里通过findViewById获取到这个ViewPager,并为其设置Adapter。
1 | mViewPager = findViewById(R.id.view_pager) |
在使用ViewPager时,往往需要对当前选中页面的行为进行监听,比如当用户左右滑动切换页面时,对应地改变标题栏的文字,对应的是addOnPageChangeListener接口,注意这里是add并非set,意味着不要对同一个对象多次调用,否则会多次触发。
1 | // ViewPager.java |
接口还是比较简单的,同时,如果我们只关注三个回调中的一个(往往是onPageSelected),可以用另一个内部类来创建监听对象,以减少样板代码,SimpleOnPageChangeListener同样位于ViewPager.java中。
这是很好的一种编程思想,对于包含多个回调函数的监听接口,增加一个内部类,为每个回调函数创建一个空函数,在使用时只覆写业务需要的接口。
1 | public static class SimpleOnPageChangeListener implements OnPageChangeListener { |
Hint:如果要在onPageSelected回调里获取相应的SubFragment,不要使用Adapter.getItem,它会返回一个新创建的Fragment。应当调用的方法是Adapter.instantiateItem,这会返回已创建的Fragment,参考Stack Overflow上面的这个问题。
Adapter
有两种Adapter,FragmentPagerAdapter和FragmentStatePagerAdapter,简单地说,如果你的ViewPager只包含3到4个固定的页面,则使用FragmentPagerAdapter;如果有很多个页面,则使用FragmentStatePagerAdapter。
这里以FragmentStatePagerAdapter为例,介绍Adapter的写法。
1 | // FragmentStatePagerAdapter.java |
可见最简单的FragmentStatePagerAdapter只需要实现getItem和getCount两个方法。值得一提的是,如果需要在创建SubFragment时传递一些参数,用以下写法。
1 | // 创建Fragment时传入Arg_0 |
- Activity 传递大数据
在使用Intent进行Activity之间的跳转时,系统提供了putExtra用于参数传递,如下例。
1 | // caller activity |
如果传递的参数不是基础类型,而是列表,则使用putExtra(String, Parcelable)和getParcelableExtra(String)做相应的存取。
然而,实际上很多人并不知道,通过Intent传递的参数,是有大小限制的。当我们传递占内存非常大的数据,如1000个元素的列表、Bitmap等等时,稍不注意,就会出现TransactionTooLargeException,从异常名就可以看出,这是由于参数过大引起的。究其原因,是因为ActivityManagerService内部使用了Binder通信机制,其事务缓冲区限制了传输数据的大小。Binder事务缓冲区的大小为1MB,而且,这1MB还不是独享的,意味着有时尽管传递的数据没有超出1MB,也会触发异常。
那么,对于需要传递大量数据的场景,有哪些方案?
单例
1 | object Singleton { |
不需要过多解释,注意不要出现内存泄漏,以及单例无法在进程之间共享。
持久化
利用网络、数据库、文件、SharedPreference等方式,将数据持久化保存,随后在新页面读取。优点是保存后可以跨进程甚至跨应用、跨平台使用,缺点则是效率低下,读写时没有控制好事务会发生异常。
使用EventBus
在《阿里巴巴Android开发手册》中写到:“Activity 间的数据通信,对于数据量比较大的,避免使用 Intent + Parcelable 的方式,可以考虑 EventBus 等替代方案,以免造成 TransactionTooLargeException。”
由于EventBus滥用会导致代码结构混乱,因此个人不推荐。
参考资料:https://juejin.im/post/5d8de547e51d45781f73bacc
- Kotlin单例写法
单例模式是日常开发中最常使用到的设计模式,一个良好的单例模式实现应当兼顾代码性能与调用简便两个方面。在Java中我们通过“双锁”或者“静态内部类”来实现单例模式,相比之下我更喜欢静态内部类的写法,《Effective Java》一书的作者也是这样认为的。
1 | // 样例代码,来自 wiki:https://en.wikipedia.org/wiki/Initialization-on-demand_holder_idiom |
无参数写法
今天主要讨论Kotlin的单例写法,在Kotlin中,单例被上升到了语言层面,关键字object可以用来声明一个不需要参数的单例对象。
1 | object SomeSingleton { |
借助于JVM加载类的过程,它编译后的等效Java代码也是线程安全的。
1 | // 上述Kotlin代码的Java等价版本 |
有参数写法
有时我们需要在单例初始化时传入一些参数,比如Glide.with(Context),此时object关键字就捉襟见肘了。在Stack Overflow这个问题下面可以学习到,借助伴生对象的“伪静态方法”,能达到传入初始化参数的目的。
1 | class UsersDatabase : RoomDatabase() { |
以上这种写法,需要关注以下几点。
- 单例成员
INSTANCE需要有@Volatile声明,以保证对象唯一 synchronized加锁防止重复初始化- 借助
also返回原对象
如果代码里只有一个单例类要实现,上面这种写法就足够了。但是,若有很多个单例类,这种写法产生的样板代码可不少。是不是可以把样板代码逻辑抽出,一次书写,多处调用?答案是肯定的。
有参数写法,Write Once,Use Many
首先区分上述实现方式里,可变的部分与不变的部分,思路是把不变的部分抽象成流程,把可变的部分提取作为参数。
不变的部分是检查、维护、调用构建函数,将其抽出一个类,这个类一定是用于被继承,因此我们将其声明为open,通过lambda表达式参数constructor,开放出构建对象的能力
1 | // SingletonHolder.kt |
此时,有一个类需要增加单例实现,并且其构造函数需要一个Context类型的参数,我们只需要在其内部声明一个伴生对象,继承自SingletonHolder<MyManager, Context>
1 | // MyManager.kt |
对单例的调用者而言,写法与Java无异。
1 | MyManager.getInstance(context).doSomething() |
怎么样,是不是与Glide的Glide.with(context).load(img_url)完全一致?Bravo!
参考资料
- https://medium.com/@BladeCoder/kotlin-singletons-with-argument-194ef06edd9e
- https://stackoverflow.com/questions/40398072/singleton-with-parameter-in-kotlin
Vimium的页面检索技巧
在使用浏览器时,有时我们会打开很多个页面,此时如果想要在打开的页面里找到特定页面,往往需要从头翻到尾,十分之麻烦。Vimium考虑到了这一点,并为我们提供快捷键T解决。这个功能属于Vomnibar功能集,是Vimium提供的一组页面新建、搜索快捷键,一共有5个。
o,在当前Tab打开URL、书签或浏览历史O,新建Tab打开URL、书签或浏览历史b,在当前Tab打开书签B,新建Tab打开书签T,也就是刚刚介绍过的,在已打开的Tab中进行搜索
(顺带提一下,Sublime的copy line快捷键是Ctrl+Shift+D,在写着一段时用到的。)
使用默认参数简化自定义View的构造函数
在编写自定义View的类时,如果自定义View继承自android.view.View,通常需要覆写多个构造函数,以支持View的多种构建方式。这种处理不仅麻烦,还带来大量样板代码,稀释了我们的代码质量。
1 | // View.java |
联想到Kotlin函数的默认参数功能,是不是可以将其应用在这种场景中呢?答案当然是可以。结合@JvmOverloads注解和默认参数,写法如下。
1 | class CustomView constructor( |
在此基础上,借助init{ ... }代码块,可以执行自定义的初始化代码。
参考:https://stackoverflow.com/questions/20670828/how-to-create-constructor-of-custom-view-with-kotlin
MediaPlayer状态机
MediaPlayer是Android SDK提供的音视频播放组件,尽管目前有更优秀的IJKPlayer、EXOPlayer等开源项目,MediaPlayer作为功能单一、接口清晰的播放器,有其值得学习的意义。一切故事,从一张状态机图片开始。
图例说明:单箭头表示同步调用,双箭头表示异步调用,双层椭圆(仅End)表示终结态。
这张图乍一看像是一团乱麻,其实可以按照播放前、播放中、播放后的阶段进行区分。
播放前
- 以
Prepared为界,之前的状态都可以认为是“播放前” - 通过
new创建一个播放器,或者对已有播放器调用reset,均可以得到一个处于Idle状态的播放器。不过这两种方式有一个显著区别,即对Idle态播放器调用getCurrentPosition(),getDuration(),getVideoHeight(),getVideoWidth(),setAudioAttributes(),setLooping(),setVolume(),pause(),start(),stop(),seekTo(),prepare(),prepareAsync()方法时,如果是新构建的播放器,不会抛出任何一场,而如果是通过reset得到的Idle播放器,则会进入OnErrorListener.onError()回调 - 播放器在开始播放前,必须进入
Prepared态。有两种方法,分别是同步的prepare()和异步的prepareAsync()。同步方法的返回是很快的,几乎是瞬间。对于异步调用,可以通过setOnPreparedListener()设置监听 - 当播放器处于
Prepared态时,可以设置音量、屏幕常亮、循环播放等属性
播放中
- 播放过程可能因为各种原因发生异常,诸如不支持的音视频格式、受损的文件、分辨率过高、解码超市等等原因,或者是对于播放器调用了不属于其状态的方法。在这些错误发生时,会走到
OnErrorListener.onError()回调中,因此在播放前设置监听setOnErrorListener()是非常重要的 - 设置
onError监听并不能避免播放器进入Error态,只是在进入时发出程序可以观测到的监听事件 - 如果在错误的状态调用
prepare(),prepareAsync(),setDatasource(),会导致IllegalStateException - 基于上一条,在调用
setDatasource以及它的众多重载方法时,必须捕获IllegalArgumentException和IOException - 通过
start()启动播放,通过isPlaying()判断当前是否处于播放中,可以在start()后继续调用start(),但这不会产生任何影响 - 在开始播放后,可以通过
setOnBufferingUpdateListener()监听视频缓冲进度 - 对于播放中的视频,调用
pause()进入Paused态,这是一个略微有延迟(seconds)的调用,意味着isPlaying()可能不会立即反映当前状态,反之亦然 - 对于
Started,Paused,Prepared,PlaybackCompleted态的播放器调用stop(),使其进入Stopped态;对于Stopped态的播放器,必须使其再次进入Prepared态后,方可用于播放 - 与
start()一样,多次调用stop()不会产生任何影响 - 用
seekTo()设置播放进度,这是一个异步方法,OnSeekComplete.onSeekComplete()用于监听;可以在Prepared,Paused,PlaybackCompleted多个态调用,且调用seekTo()后播放器仍保持原状态,同时改变当前帧;相应的,getCurrentPosition()可以返回当前的播放进度
播放后
- 一旦播放器不再使用,建议立即调用
release()释放资源,此后播放器进入End态,且再也无法通过任何方法使其恢复 - 如果设置了Looping,播放完成后会保持
Started态,否则会进入OnCompletionListener回调,并进入PlaybackCompleted态
播放器的权限要求
视需求而定,可能需要WAKE_LOCK以及Internet权限。
线程限制
必须在UI线程创建播放器,只有这样才能正常收到为播放器设置的各种回调。
参考:https://developer.android.com/reference/android/media/MediaPlayer