注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

Android 自定义开源库 EasyView

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。 配置EasyView 1. 工程b...
继续阅读 »

  这是一个简单方便的Android自定义View库,我一直有一个想法弄一个开源库,现在这个想法付诸实现了,如果有什么需要自定义的View可以提出来,不一定都会采纳,合理的会采纳,时间周期不保证,咱要量力而行呀,踏实一点。


1682474222191_095705.gif.gif


配置EasyView


1. 工程build.gradle 或 settings.gradle配置


   代码已经推送到MavenCentral(),在Android Studio 4.2以后的版本中默认在创建工程的时候使用MavenCentral(),而不是jcenter()


   如果是之前的版本则需要在repositories{}闭包中添加mavenCentral(),不同的是,老版本的Android Studio是在工程的build.gradle中添加,而新版本是工程的settings.gradle中添加,如果已经添加,则不要重复添加。


repositories {
...
mavenCentral()
}

2. 使用模块的build.gradle配置


   例如在app模块中使用,则打开app模块下的build.gradle,在dependencies{}闭包下添加即可,之后记得要Sync Now


dependencies {
implementation 'io.github.lilongweidev:easyview:1.0.2'
}

使用EasyView


   这是一个自定义View的库,会慢慢丰富里面的自定义View,我先画个饼再说。


一、MacAddressEditText


   MacAddressEditText是一个蓝牙Mac地址输入控件,点击之后出现一个定制的Hex键盘,用于输入值。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.MacAddressEditText
android:id="@+id/mac_et"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:boxBackgroundColor="@color/white"
app:boxStrokeColor="@color/black"
app:boxStrokeWidth="2dp"
app:boxWidth="48dp"
app:separator=":"
app:textColor="@color/black"
app:textSize="14sp" />


2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。







































属性说明
app:boxBackgroundColor设置输入框的背景颜色
app:boxStrokeColor设置输入框的边框颜色
app:boxStrokeWidth设置输入框的边框大小
app:boxWidth设置输入框大小
app:separatorMac地址的分隔符,例如分号:
app:textColor设置输入框文字颜色
app:textSize设置输入框文字大小

3. 代码中使用


    MacAddressEditText macEt = findViewById(R.id.mac_et);
String macAddress = macEt.getMacAddress();

   macAddress可能会是空字符串,使用之前请判断一下,参考app模块中的MainActivity中的使用方式。


二、CircularProgressBar


   CircularProgressBar是圆环进度条控件。


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.CircularProgressBar
android:id="@+id/cpb_test"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
app:maxProgress="100"
app:progress="10"
app:progressbarBackgroundColor="@color/purple_500"
app:progressbarColor="@color/purple_200"
app:radius="80dp"
app:strokeWidth="16dp"
app:text="10%"
app:textColor="@color/teal_200"
app:textSize="28sp" />

2. 属性介绍


   这里使用了MacAddressEditText的所有属性,可以自行进行设置,使用说明参考下表。















































属性说明
app:maxProgress最大进度
app:progress当前进度
app:progressbarBackgroundColor进度条背景颜色
app:progressbarColor进度颜色
app:radius半径,用于设置圆环的大小
app:strokeWidth进度条大小
app:text进度条中心文字
app:textColor进度条中心文字颜色
app:textSize进度条中心文字大小

3. 代码中使用


    CircularProgressBar cpbTest = findViewById(R.id.cpb_test);
int progress = 10;
cpbTest.setText(progress + "%");
cpbTest.setProgress(progress);

   参考app模块中的MainActivity中的使用方式。


三、TimingTextView


   TimingTextView是计时文字控件


1. xml中使用


   首先是在xml中添加如下代码,具体参考app模块中的activity_main.xml。


    <com.easy.view.TimingTextView
android:id="@+id/tv_timing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="30dp"
android:text="计时文字"
android:textColor="@color/black"
android:textSize="32sp"
app:countdown="false"
app:max="60"
app:unit="s" />

2. 属性介绍


   这里使用了TimingTextView的自定义属性不多,只有3个,TextView的属性就不列举说明,使用说明参考下表。























属性说明
app:countdown是否倒计时
app:max最大时间长度
app:unit时间单位:s(秒)、m(分)、h(时)

3. 代码中使用


    TimingTextView tvTiming = findViewById(R.id.tv_timing);
tvTiming.setMax(6);//最大时间
tvTiming.setCountDown(false);//是否倒计时
tvTiming.setUnit(3);//单位 秒
tvTiming.setListener(new TimingListener() {
@Override
public void onEnd() {
//定时结束
}
});
//开始计时
tvTiming.start();
//停止计时
//tvTiming.end();

   参考app模块中的MainActivity中的使用方式。


作者:初学者_Study
链接:https://juejin.cn/post/7225407341633175613
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Compose的UI刷新机制是啥?和Flutter一样么?

前言 我去年发现Android新出了个UI框架Compose,看了Demo后发现,纳尼!和Flutter太像了吧,无论是编程逻辑、控件名,都有很多相似的地方。谷歌自己抄自己?一直想去尝试,感受一下,直到今年才有时间去写了小项目。 思考 我在写代码的时候和Flu...
继续阅读 »

前言


我去年发现Android新出了个UI框架Compose,看了Demo后发现,纳尼!和Flutter太像了吧,无论是编程逻辑、控件名,都有很多相似的地方。谷歌自己抄自己?一直想去尝试,感受一下,直到今年才有时间去写了小项目。


思考


我在写代码的时候和Flutter去做对比,就想到它俩的UI刷新机制是否有相似点,如果不一样,那Compose的刷新机制是什么?


在看Compose的刷新机制前,我回忆了一下Flutter的刷新机制。



简单讲Flutter是通过调用StatefulWidgetsetState方法,重新走一遍build方法中的代码。那原理就是setState调用时会自己添加到BuildOwnerdirtyElements脏链表中,然后调用window.scheduleFrame来注册Vsync回调,当下一次vsync信号的到来时会重新绘制UI。



所以我觉得Flutter更像是一个屏幕,调用setState方法不断重新构建UI页面,一帧一帧的。那Compose是不是也是这样呢?


尝试


Demo如下,在页面上显示一个Text控件,和一个按钮,每一次点击,Text显示的数字自增。


@Composable
fun demo() {
// 关键代码
var versionCode by remember { mutableStateOf(0) }

Column {
Button(
onClick = { versionCode++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

UI发生变化的关键代码就是 by remember { mutableStateOf(0) } ,这行代码删除后,怎么按UI都不会变化。我发每次versionCode变化的时候会重新走一遍demo()内的代码,这时该类中有其他方法,其他方法代码并不会重走。如果把Text中的versionCode引用删除,写一个固定值,这个再点击按钮,也不会重新走一遍代码。



问题



  1. Compose 是如何进行更新的?

  2. 如何做到更新时只有引用的方法内刷新?

  3. 不引用为何不刷新方法?



分析


带着以上三个问题,点进mutableStateOf中去,看一看源码。
1.png
进入 createSnapshotMutableState
2.png
进入 ParcelableSnapshotMutableState
3.png
进入SnapshotMutableStateImpl,下面这个类中,红框标记的是关键代码,这个方法的注解是



A single value holder whose reads and writes are observed by Compose.Additionally, writes to it are transacted as part of the [Snapshot] system.



我理解的意思是,对value这个值做了监听,只要是 Compose的UI 引用了value,当其发生变化时就能自动更新


4.png


这里的 value 就是给 versionCode 赋的值,这里的 get() 方法中会调用readable() ,把当前 state 保存起来(我认为这里的 state 可以理解为 Flutter 的 state)。set() 方法内会进行对比,在 equivalent() 方法中比对的是对象,当两个对象的地址不一致时,会触发监听通知,重写用的Compose方法,



这也就解释了之前说的三个问题。



  1. Compose的刷新本质是对 value 的监听通知;

  2. 为了避免过度刷新,将刷新范围固定到最小的标记@Compose的注解的方法内,包括方法内的其他方法(和Flutter的StatefulWidget的刷新范围一样),前提是其他Compose方法有参数传入(我理解是有新的参数传入,会生成新的对象),否则除了第一次不会再进行刷新;为提高性能,会把频繁刷新的View(Flutter中的widget)封装为单独的Compose方法(Stful);

  3. 对value的引用,调用了get内的注册方法,不引用,也就不会引起刷新。



以上就是我对Compose的刷新机制的理解,


应用


了解了Compose的刷新机制后,怎么才能高效的使用这中逻辑编程呢?我在踩了一堆坑后,有以下几个写代码的注意点,仅供参考。


一、


如果在方法内使用mutableStateOf,需要包一层remember函数,它的作用是运行完里面的代码后,就会存在缓存里,再次执行这行代码,不会再次初始化,会从缓存中拿出 State的对象,防止多次初始化。 如果你偏不,可以试试看有什么神奇的现象🙃。’
(PS :在remember函数中有个熟悉的参数 Key,熟悉Flutter的估计都明白干啥的了,这就不再啰嗦了)


// 成员变量可以不用套,因为本身就初始化一次
var versionCode by mutableStateOf(0)

@Composable
fun demo() {
// 方法内这么写
var versionCode by remember { mutableStateOf(0) }

Column {
Button(
onClick = { versionCode++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

二、


value的刷新是设置新的值,是对比对象本身,如果只改变了对象内的值,是不会放生刷新的。如代码中A对象并没有改变。


data class A(var a: Int = 0)
@Composable
fun demo() {
val versionCode by remember { mutableStateOf(A(0)) }
Column {
Button(
onClick = { versionCode.a++ }) {
Text("Add +")
}
Text( versionCode.toString() )
}
}

遇到这种情况需要刷新有两种办法



  1. 复制对象,改变对象地址


        Button(
onClick = { versionCode.copy(a = versionCode.a++) }) {
Text("Add +")
}


  1. 给对象内的变量实现mutableStateOf


data class A(var a: MutableState<Int> = mutableStateOf(0)) {
// 或写在下面用 by 代理的方式
var a by mutableStateOf(0)
}


三、


单个对象好复制,好改,遇到列表,数组类的对象刷新,该怎么做呢,换列表对象?不合适。好在官方已经替咱们想到了这一点。神器 mutableStateListOf,当列表内的数据发生变化时,会刷新UI


val data = mutableStateListOf(1, 2, 3)
@Composable
fun demo() {
Column {
Button(onClick = {
data.add(data.last() + 1)
}) {
Text(text = "onclick")
}
for (d in data) {
Text(text = "data:${d}")
}
}
}

我大概遇到的UI刷新上的坑,可以归为以上三种,还有其他的欢迎交流补充。


总结


Compose的刷新机制简单来讲是有范围,有组织的用观察者模式进行刷新。
分析了个大概,更深层次的原理,以后慢慢啃。
Compose的编程模式和Flutter很像,会flutter很容易上手,还挺有意思的。


作者:苏啵曼
链接:https://juejin.cn/post/7080453608231141412
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Kotlin flow实践总结

背景 最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。 Flow是什么 按顺序发出多个值的数据流。 本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开...
继续阅读 »

背景


最近学了下Kotlin Flow,顺便在项目中进行了实践,做一下总结。


image.png


Flow是什么


按顺序发出多个值的数据流。

本质就是一个生产者消费者模型,生产者发送数据给消费者进行消费。


Image.png



  • 冷流:当执行collect的时候(也就是有消费者的时候),生产者才开始发射数据流。

    生产者与消费者是一对一的关系。当生产者发送数据的时候,对应的消费者才可以收到数据。

  • 热流:不管有没有执行collect(也就是不管有没有消费者),生产者都会发射数据流到内存中。

    生产者与消费者是一对多的关系。当生产者发送数据的时候,多个消费者都可以收到数据


实践场景


场景一:简单列表数据的加载状态


简单的列表显示场景,可以使用onStart,onEmpty,catch,onCompletion等回调操作符,监听数据流的状态,显示相应的加载状态UI。



  • onStart:在数据发射之前触发,onStart所在的线程,是数据产生的线程

  • onCompletion:在数据流结束时触发,onCompletion所在的线程,是数据产生的线程

  • onEmpty:当数据流结束了,缺没有发出任何元素的时候触发。

  • catch:数据流发生错误的时候触发

  • flowOn:指定上游数据流的CoroutineContext,下游数据流不会受到影响


private fun coldFlowDemo() {
//创建一个冷流,在3秒后发射一个数据
val coldFlow = flow<Int> {
delay(3000)
emit(1)
}
lifecycleScope.launch(Dispatchers.IO) {
coldFlow.onStart {
Log.d(TAG, "coldFlow onStart, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = true
mBinding.tvLoadingStatus.text = "加载中"
}.onEmpty {
Log.d(TAG, "coldFlow onEmpty, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载为空"
}.catch {
Log.d(TAG, "coldFlow catch, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "数据加载错误:$it"
}.onCompletion {
Log.d(TAG, "coldFlow onCompletion, thread:${Thread.currentThread().name}")
mBinding.progressBar.isVisible = false
mBinding.tvLoadingStatus.text = "加载完成"
}
//指定上游数据流的CoroutineContext,下游数据流不会受到影响
.flowOn(Dispatchers.Main)
.collect {
Log.d(TAG, "coldFlow collect:$it, thread:${Thread.currentThread().name}")
}
}
}

比如上面的例子。
使用flow构建起函数,创建一个冷流,3秒后发送一个值到数据流中。
使用onStart,onEmpty,catch,onCompletion操作符,监听数据流的状态。


日志输出:


coldFlow onStart, thread:main
coldFlow onCompletion, thread:main
coldFlow collect:1, thread:DefaultDispatcher-worker-1

场景二:同一种数据,需要加载本地数据和网络数据


在实际的开发场景中,经常会将一些网络数据保存到本地,下次加载数据的时候,优先使用本地数据,再使用网络数据。

但是本地数据和网络数据的加载完成时机不一样,所以可能会有下面几种场景。



  1. 本地数据比网络数据先加载完成:那先使用本地数据,再使用网络数据

  2. 网络数据比本地数据先加载完成:



  • 网络数据加载成功,那只使用网络数据即可,不需要再使用本地数据了。

  • 网络数据加载失败,可以继续尝试使用本地数据进行兜底。



  1. 本地数据和网络数据都加载失败:通知上层数据加载失败


实现CacheRepositity


将上面的逻辑进行简单封装成一个基类,CacheRepositity。

相应的子类,只需要实现两个方法即可。



  • CResult:代表加载结果,Success 或者 Error。

  • fetchDataFromLocal(),实现本地数据读取的逻辑

  • fetchDataFromNetWork(),实现网络数据获取的逻辑


abstract class CacheRepositity<T> {
private val TAG = "CacheRepositity"

fun getData() = channelFlow<CResult<T>> {
supervisorScope {
val dataFromLocalDeffer = async {
fetchDataFromLocal().also {
Log.d(TAG,"fetchDataFromLocal result:$it , thread:${Thread.currentThread().name}")
//本地数据加载成功
if (it is CResult.Success) {
send(it)
}
}
}

val dataFromNetDeffer = async {
fetchDataFromNetWork().also {
Log.d(TAG,"fetchDataFromNetWork result:$it , thread:${Thread.currentThread().name}")
//网络数据加载成功
if (it is CResult.Success) {
send(it)
//如果网络数据已加载,可以直接取消任务,就不需要处理本地数据了
dataFromLocalDeffer.cancel()
}
}
}

//本地数据和网络数据,都加载失败的情况
val localData = dataFromLocalDeffer.await()
val networkData = dataFromNetDeffer.await()
if (localData is CResult.Error && networkData is CResult.Error) {
send(CResult.Error(Throwable("load data error")))
}
}
}

protected abstract suspend fun fetchDataFromLocal(): CResult<T>

protected abstract suspend fun fetchDataFromNetWork(): CResult<T>

}

sealed class CResult<out R> {
data class Success<out T>(val data: T) : CResult<T>()
data class Error(val throwable: Throwable) : CResult<Nothing>()
}

测试验证


写个TestRepositity,实现CacheRepositity的抽象方法。

通过delay延迟耗时来模拟各种场景,观察日志的输出顺序。


private fun cacheRepositityDemo(){
val repositity=TestRepositity()
lifecycleScope.launch {
repositity.getData().onStart {
Log.d(TAG, "TestRepositity: onStart")
}.onCompletion {
Log.d(TAG, "TestRepositity: onCompletion")
}.collect {
Log.d(TAG, "collect: $it")
}
}
}

本地数据比网络数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay1秒,网络加载delay2秒

日志输出:collect 执行两次,先收到本地数据,再收到网络数据。


onStart
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据比本地数据加载快


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Success("data from fetchDataFromNetWork")
}
}

模拟数据:本地加载delay 2秒,网络加载delay 1秒

日志输出:collect 只执行1次,只收到网络数据。


onStart
fetchDataFromNetWork result:Success(data=data from fetchDataFromNetWork) , thread:main
collect: Success(data=data from fetchDataFromNetWork)
onCompletion

网络数据加载失败,使用本地数据


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Success("data from fetchDataFromLocal")
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地加载delay 2秒,网络数据加载失败

日志输出:collect 只执行1次,只收到本地数据。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Success(data=data from fetchDataFromLocal) , thread:main
collect: Success(data=data from fetchDataFromLocal)
onCompletion

网络数据和本地数据都加载失败


class TestRepositity : CacheRepositity<String>() {
override suspend fun fetchDataFromLocal(): CResult<String> {
delay(2000)
return CResult.Error(Throwable("fetchDataFromLocal Error"))
}

override suspend fun fetchDataFromNetWork(): CResult<String> {
delay(1000)
return CResult.Error(Throwable("fetchDataFromNetWork Error"))
}
}

模拟数据:本地数据加载失败,网络数据加载失败

日志输出: collect 只执行1次,结果是CResult.Error,代表加载数据失败。


onStart
fetchDataFromNetWork result:Error(throwable=java.lang.Throwable: fetchDataFromNetWork Error) , thread:main
fetchDataFromLocal result:Error(throwable=java.lang.Throwable: fetchDataFromLocal Error) , thread:main
collect: Error(throwable=java.lang.Throwable: load data error)
onCompletion

场景三:多种数据源,按照顺序合并进行展示


Image.png


在实际的开发场景中,经常一个页面的数据,是需要发起多个网络请求之后,组合数据之后再进行显示。
比如类似这种页面,3种数据,需要由3个网络请求获取得到,然后再进行相应的显示。


实现目标:



  1. 接口间不需要互相等待,哪些数据先回来,就先展示哪部分

  2. 控制数据的显示顺序


flow combine操作符


可以合并多个不同的 Flow 数据流,生成一个新的流。
只要其中某个子 Flow 数据流有产生新数据的时候,就会触发 combine 操作,进行重新计算,生成一个新的数据。


例子


class HomeViewModel : ViewModel() {

//暴露给View层的列表数据
val list = MutableLiveData<List<String?>>()

//多个子Flow,这里简单都返回String,实际场景根据需要,返回相应的数据类型即可
private val bannerFlow = MutableStateFlow<String?>(null)
private val channelFlow = MutableStateFlow<String?>(null)
private val listFlow = MutableStateFlow<String?>(null)


init {
//使用combine操作符
viewModelScope.launch {
combine(bannerFlow, channelFlow, listFlow) { bannerData, channelData, listData ->
Log.d("HomeViewModel", "combine bannerData:$bannerData,channelData:$channelData,listData:$listData")
//只要子flow里面的数据不为空,就放到resultList里面
val resultList = mutableListOf<String?>()
if (bannerData != null) {
resultList.add(bannerData)
}
if (channelData != null) {
resultList.add(channelData)
}
if (listData != null) {
resultList.add(listData)
}
resultList
}.collect {
//收集combine之后的数据,修改liveData的值,通知UI层刷新列表
Log.d("HomeViewModel", "collect: ${it.size}")
list.postValue(it)
}
}
}

fun loadData() {
viewModelScope.launch(Dispatchers.IO) {
//模拟耗时操作
async {
delay(1000)
Log.d("HomeViewModel", "getBannerData success")
bannerFlow.emit("Banner")
}
async {
delay(2000)
Log.d("HomeViewModel", "getChannelData success")
channelFlow.emit("Channel")
}
async {
delay(3000)
Log.d("HomeViewModel", "getListData success")
listFlow.emit("List")
}
}
}
}

HomeViewModel



  1. 提供一个 LiveData 的列表数据给View层使用

  2. 内部有3个子 flow ,分别负责相应数据的生产。(这里简单都返回String,实际场景根据需要,返回相应的数据类型即可)。

  3. 通过 combine 操作符,组合这3个子flow的数据。

  4. collect 接收生成的新数据,并修改liveData的数据,通知刷新UI


View层使用


private fun flowCombineDemo() {
val homeViewModel by viewModels<HomeViewModel>()
homeViewModel.list.observe(this) {
Log.d("HomeViewModel", "observe size:${it.size}")
}
homeViewModel.loadData()
}

简单的创建一个 ViewModel ,observe 列表数据对应的 LiveData。

通过输出的日志发现,触发数据加载之后,每次子 Flow 流生产数据的时候,都会触发一次 combine 操作,生成新的数据。


日志输出:
combine bannerData:null,channelData:null,listData:null
collect: 0
observe size:0

getBannerData success
combine bannerData:Banner,channelData:null,listData:null
collect: 1
observe size:1

getChannelData success
combine bannerData:Banner,channelData:Channel,listData:null
collect: 2
observe size:2

getListData success
combine bannerData:Banner,channelData:Channel,listData:List
collect: 3
observe size:3

总结


具体场景,具体分析。刚好这几个场景,配合Flow进行使用,整体实现也相对简单了一些。


作者:入魔的冬瓜
链接:https://juejin.cn/post/7065327938064875534
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Kotlin的 :: 符号是个啥?

前言 在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。 正文 这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。 传递函数优化...
继续阅读 »

前言


在阅读Kotlin的代码时,经常有看到 :: 这个符号,这个符号专业术语叫做成员引用,在代码中使用可以简化代码,那到底怎么使用呢以及使用的范围,这篇文章就来好好捋一下。


正文


这里虽然很熟悉,但是我们还是从简单说起,需要了解为什么这样设计。


传递函数优化


这里我们举个栗子,就看这个熟悉的sortBy排序函数,先定义People类:


//测试代码
data class People(val name: String,val age: Int){
//自定义的排序条件
fun getMax() : Int{
return age * 10 + name.length
}
}

然后我们来进行排序:


val people = People("zyh",10)
val people1 = People("zyh1",100)
val peopleList = arrayListOf(people,people1)
//给sortBy传入lambda
peopleList.sortBy { people -> people.getMax() }

这里我们给sortBy函数传递一个lambda,由于sortBy函数是内联的,所以传递给它的lambda会被内联,但是假如现在有个问题,就是这些lambda已经被定义成了函数变量,比如我定义了一个顶层函数:


//定义了一个顶层函数
fun getMaxSort(people: People): Int{
return (people.age) * 10 + people.name.length
}

或者排序条件已经定义成了一个变量值:


//排序条件
val condition = { people: People -> people.getMax() }

那这时如果我想再进行排序必须要这么写了:


//调用一遍函数
peopleList.sortBy { getMaxSort(it) }
//传递参数
peopleList.sortBy(condition)

然后这里我们可以利用成员引用 :: 符号来优化一下:


//直接就会调用顶层函数getMaxSort
peopleList.sortBy(::getMaxSort)
//直接就会调用People类的getMax函数
peopleList.sortBy(People::getMax)

这里看起来就是语法糖,可以简化代码。


成员引用 ::


你有没有想过这里是为什么,这里使用了 :: 符号其实就是把函数转换成了一个值,首先我们使用


val condition = { people: People -> people.getMax() }

这种时,其实condition就是一个函数类型的变量,这个我们之前文章说过,Kotlin支持完整的函数类型,而使用高阶函数可以用lambda,但是getMaxSort()函数它就是一个函数了,它不是一个值,除非你再外面给它包裹一层构成lambda,所以这里调用condition传递进的是sortBy()中,而getMaxSort(it)是以lambda的形式又包裹了一层。


但是使用 :: 符号后,也就是把函数转换成了一个值,比如 People::getMax 这就是一个值,它代表的就是People内的getMax函数。


而 ::getMaxSort 也是一个值,它表示getMaxSort函数。


使用范围


前面2个例子其实也就表明了这种成员引用的使用范围,一个是类的函数或者属性,还有就是顶层函数,它没有类名,可以省略。


绑定引用


这里再额外说一个知识点,前面说成员引用都是 类名:属性名 这种格式,比如 People::getMax ,但是它在后面KT版本变化后进行了优化,可以看下面代码:


//定义一个people实例
val people = People("zyh",10)
//利用成员引用,把函数转换成值
val ageFun = People::age
val age = ageFun(people)
//直接在对象实例上使用 ::
val ageValue = people::age

从上面我们发现,ageValue的值可以从实例上通过成员引用调用得到,不过这里区别就大了,ageFun是一个函数类型,而ageValue则是一个int值。


总结


总结一下,其实成员引用 :: 很简单,它就是把函数给转成了值,而这个值可以看成是函数类型,这样说就十分好理解了。


不过这个真实原理可不是这么简单,并不是利用lambda又把函数包裹了一层,这里应该是反射的相关知识,我们后续再具体来说其原理,刚好后续有反射相关的文章,大家可以点赞、关注一波。


作者:元浩875
链接:https://juejin.cn/post/7056140661409447944
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter好用的轮子推荐02:拥有炫酷光影效果的拟态风格UI套件

前言 Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。 本专栏为大家收...
继续阅读 »

前言


Flutter 是 Google 开源的应用开发框架,仅通过一套代码就能构建支持Android、iOS、Windows、Linux等多平台的应用。Flutter的性能非常高,拥有120fps的刷新率,也是目前非常流行的跨平台UI开发框架。


本专栏为大家收集了Github上近70个优秀开源库,后续也将持续更新。希望可以帮助大家提升搬砖效率,同时祝愿Flutter的生态越来越完善🎉🎉。


正文


一、🚀 轮子介绍




  • 名称:flutter_neumorphic




  • 概述:易用的拟态风格UI套件,几乎可以在任何App中使用它。




  • 作者:idean Team




  • 仓库地址:Flutter-Neumorphic




  • 推荐指数: ⭐️⭐️⭐️⭐️⭐️




  • 常用指数: ⭐️⭐️⭐️⭐️⭐️




  • 效果预览:




flutter_logo_small.gif


二、⚙️ 安装及使用


dependencies:
flutter_neumorphic: ^3.0.3

import 'package:flutter_neumorphic/flutter_neumorphic.dart';

三、🔧 常用属性


1.基本



















































属性描述
LightSource特定于theme或小组件的光源,用于投射浅色/深色阴影
shape拟态容器中使用的曲线形状
Depth小组件与父组件的垂直距离
Intensity光的强度,它影响阴影的颜色
SurfaceIntensity组件表面的明暗效果
Color拟态组件的默认颜色
Accent拟态组件的选中颜色,例如复选框
Variant拟态组件的次要颜色
BoxShape拟态组件形状
Border边框

2.Shapes


image.png


四、🗂 内置组件介绍


tips:为了更直观的展示效果,本文案例已将组件和背景设置为同一色值的浅灰色。


1.Neumorphic


一个基本的拟态容器组件,可根据光源、高度(深度)添加浅色/深色渐变的容器。


container.gif


NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
child: const SizedBox(
width: 200,
height: 200,
),
)

2.NeumorphicButton


拟态按钮,默认按下有高度变化及震动反馈


button-2.gif


NeumorphicButton(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
BorderRadius.circular(12),
),
color: Colors.grey[200],
shape: NeumorphicShape.flat,
),
child: Container(
color: Colors.grey[200],
width: 100,
height: 25,
child: const Center(
child: Text('Click me'),
),
),
onPressed: () {},
)

3.NeumorphicRadio


单选按钮


radio.gif


class NeumorphicButtonWidget extends StatefulWidget {
const NeumorphicButtonWidget({Key? key}) : super(key: key);
@override
State<NeumorphicButtonWidget> createState() => _NeumorphicButtonWidgetState();
}

class _NeumorphicButtonWidgetState extends State<NeumorphicButtonWidget> {
int _groupValue = 1;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
getChild('A', 1),
const SizedBox(width: 12),
getChild('B', 2),
const SizedBox(width: 12),
getChild('C', 3),
],
);
}
Widget getChild(String str, int value) {
return NeumorphicRadio(
child: Container(
color: Colors.grey[200],
height: 50,
width: 50,
child: Center(
child: Text(str))),
value: value,
groupValue: _groupValue,
onChanged: (value) {
setState(() {
_groupValue = value as int;
});
});
}}

4.NeumorphicCheckbox


多选按钮


checkbox.gif


class NeumorphicCheckboxWidget extends StatefulWidget {
const NeumorphicCheckboxWidget({Key? key}) : super(key: key);
@override
State<NeumorphicCheckboxWidget> createState() => _NeumorphicCheckboxWidgetState();
}

class _NeumorphicCheckboxWidgetState extends State<NeumorphicCheckboxWidget> {
bool check1 = false;
bool check2 = false;
bool check3 = false;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check1,
onChanged: (value) {
setState(() {
check1 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check2,
onChanged: (value) {
setState(() {
check2 = value;
});
},
),
const SizedBox(width: 12),
NeumorphicCheckbox(
value: check3,
onChanged: (value) {
setState(() {
check3 = value;
});
},
),
],
);
}
}

5.NeumorphicText


拟态文字


text.png


NeumorphicText(
'Flutter',
textStyle: NeumorphicTextStyle(
fontSize: 80,
fontWeight: FontWeight.w900,
),
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
)

6.NeumorphicIcon


拟态图标


icon.png


NeumorphicIcon(
Icons.public,
size: 180,
style: NeumorphicStyle(
depth: 3,
lightSource: LightSource.left,
color: Colors.grey[200],
),
);

7.material.TextField


拟态文本输入框


textfield.png


Neumorphic(
margin: const EdgeInsets.only(left: 8, right: 8, top: 2, bottom: 4),
style: NeumorphicStyle(
depth: NeumorphicTheme.embossDepth(context),
boxShape: const NeumorphicBoxShape.stadium(),
color: Colors.grey[200]),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 18),
child: const TextField(
decoration: InputDecoration.collapsed(hintText: 'NeumorphicTextField'),
),
);

8.NeumorphicSwitch


拟态开关


switch.gif


class NeumorphicSwitchWidget extends StatefulWidget {
const NeumorphicSwitchWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSwitchWidget> createState() => _NeumorphicSwitchWidgetState();
}
class _NeumorphicSwitchWidgetState extends State<NeumorphicSwitchWidget> {
bool isChecked = false;
bool isEnabled = true;
@override
Widget build(BuildContext context) {
return NeumorphicSwitch(
style: NeumorphicSwitchStyle(
trackDepth: 3,
activeThumbColor: Colors.grey[200], // 开启时按钮颜色
activeTrackColor: Colors.green, // 开启时背景颜色
inactiveThumbColor: Colors.green, // 关闭时按钮颜色
inactiveTrackColor: Colors.grey[200], // 关闭时背景颜色
),
isEnabled: isEnabled,
value: isChecked,
onChanged: (value) {
setState(() {
isChecked = value;
});
},
);
}}

9.NeumorphicToggle


拟态滑动选择器


toggle.gif


class NeumorphicToggleWidget extends StatefulWidget {
const NeumorphicToggleWidget({Key? key}) : super(key: key);
@override
State<NeumorphicToggleWidget> createState() => _NeumorphicToggleWidgetState();
}
class _NeumorphicToggleWidgetState extends State<NeumorphicToggleWidget> {
int _selectedIndex = 0;
@override
Widget build(BuildContext context) {
return NeumorphicToggle(
height: 50,
style: NeumorphicToggleStyle(
backgroundColor: Colors.grey[200],
),
selectedIndex: _selectedIndex,
displayForegroundOnlyIfSelected: true,
children: [
ToggleElement(
background: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This week",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This month",
style: TextStyle(fontWeight: FontWeight.w700),
)),
),
ToggleElement(
background: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w500),
)),
foreground: const Center(
child: Text(
"This year",
style: TextStyle(fontWeight: FontWeight.w700),
)),
)
],
thumb: Neumorphic(
style: NeumorphicStyle(
boxShape: NeumorphicBoxShape.roundRect(
const BorderRadius.all(Radius.circular(12))),
),
),
onChanged: (value) {
setState(() {
_selectedIndex = value;
});
},
);
}}

10.NeumorphicSlider


拟态滑动控制器


slider.gif


class NeumorphicSliderWidget extends StatefulWidget {
const NeumorphicSliderWidget({Key? key}) : super(key: key);
@override
State<NeumorphicSliderWidget> createState() => _NeumorphicSliderWidgetState();
}
class _NeumorphicSliderWidgetState extends State<NeumorphicSliderWidget> {
double num = 0;
@override
Widget build(BuildContext context) {
return NeumorphicSlider(
min: 8,
max: 75,
value: num,
onChanged: (value) {
setState(() {
num = value;
});
},
);
}
}

11.NeumorphicProgress


拟态百分比进度条


progress.gif


NeumorphicProgress(
height: 20,
percent: 0.5,
);

12.NeumorphicProgressIndeterminate


渐进式进度条


indeterminate.gif


NeumorphicProgressIndeterminate(
height: 10,
);

13.NeumorphicBackground


拟态背景,可以使用Radius裁剪屏幕


image.png


class NeumorphicPageView extends StatelessWidget {
const NeumorphicPageView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return NeumorphicBackground(
borderRadius: const BorderRadius.all(Radius.circular(130)),
child: Scaffold(
backgroundColor: Colors.grey[200],
));
}
}

14.NeumorphicApp


使用拟态设计的应用程序。可以处理主题、导航、本地化等


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return NeumorphicApp(
debugShowCheckedModeBanner: false,
themeMode: ThemeMode.light,
title: 'Flutter Neumorphic',
home: FullSampleHomePage(),
);
}
}

15.NeumorphicAppBar


拟态导航条


app_bar.png


五、🏠 使用案例


image.png


作者:晚夜
链接:https://juejin.cn/post/7075355857499717668
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

Flutter通用页面Loading组件

前沿 页面通用Loading组件是一个App必不可少的基础功能,之前只开发过Android原生的页面Loading,这次就按原生的逻辑再开发一个Flutter的Widget,对其进行封装复用 我们先看下效果: 原理 状态 一个通用的页面加载Loading组件...
继续阅读 »

前沿


页面通用Loading组件是一个App必不可少的基础功能,之前只开发过Android原生的页面Loading,这次就按原生的逻辑再开发一个Flutter的Widget,对其进行封装复用


我们先看下效果:
bloggif_60b603bed8875.gif


原理


状态


一个通用的页面加载Loading组件应该具备以下几种状态:


IDLE 初始化

Idle状态,此时的组件还只是初始化


LOADING 加载中

Loading状态,一般在网络请求或者耗时加载数据时调用,通用显示的是一个progress或者自定义的帧动画


LOADING_SUCCESS

LoadingSuccess加载成功,一般在网络请求成功后调用,并将需要展示的页面展示出来


LOADING_SUCCESS_BUT_EMPTY

页面加载成功但是没有数据,这种情况一般是发起列表数据请求但是没有数据,通常我们会展示一个空数据的页面来提醒用户


NETWORK_BLOCKED

网络错误,一般是由于网络异常、网络请求连接超时导致。此时我们需要展示一个网络错误的页面,并且带有重试按钮,让用户重新发起请求


ERROR

通常是接口错误,这种情况下我们会根据接口返回的错误码或者错误文本提示用户,并且也有重试按钮


/// 状态枚举
enum LoadingStatus {
idle, // 初始化
loading, // 加载中
loading_suc, // 加载成功
loading_suc_but_empty, // 加载成功但是数据为空
network_blocked, // 网络加载错误
error, // 加载错误
}

点击事件回调


当网络异常或者接口报错时,会显示错误页面,并且提供重试按钮,让用户点击重新请求。基于这个需求,我们还需要提供点击重试后的事件回调让业务可以处理重新请求。


 /// 定义点击事件
typedef OnTapCallback = Function(LoadingView widget);

提示文案


提供提示文案的自定义,方便业务根据自己的需求展示特定的提示文案


代码实现


根据上面的原理来实现对应的代码



  1. 构造方法


 /// 构造方法
LoadingView({
Key key,
@required this.child, // 需要加载的Widget
@required this.todoAfterError, // 错误点击重试
@required this.todoAfterNetworkBlocked, // 网络错误点击重试
this.networkBlockedDesc = "网络连接超时,请检查你的网络环境",
this.errorDesc = "加载失败",
this.loadingStatus = LoadingStatus.idle,
}) : super(key: key);


  1. 根据不同的Loading状态展示对应的Widget



  • 其中idle、success状态直接展示需要加载的Widget(这里也可以使用渐变动画进行切换过度)


 ///根据不同状态展示不同Widget
Widget _buildBody() {
switch (widget.loadingStatus) {
case LoadingStatus.idle:
return widget.child;
case LoadingStatus.loading:
return _buildLoadingView();
case LoadingStatus.loading_suc:
return widget.child;
case LoadingStatus.loading_suc_but_empty:
return _buildLoadingSucButEmptyView();
case LoadingStatus.error:
return _buildErrorView();
case LoadingStatus.network_blocked:
return _buildNetworkBlockedView();
}
return widget.child;
}


  1. buildLoadingView,这里简单用了系统的CircularProgressIndicator,也可以自己显示帧动画


  /// 加载中 View
Widget _buildLoadingView() {
return Container(
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 22.w,
width: 22.w,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(AppColors.primaryBgBlue),
),
),
),
);
}


  1. 其他提示页面,这里做了一个统一的封装



/// 编译通用页面
Container _buildGeneralTapView({
String url = "images/icon_network_blocked.png",
String desc,
@required Function onTap,
}) {
return Container(
color: AppColors.primaryBgWhite,
width: double.maxFinite,
height: double.maxFinite,
child: Center(
child: SizedBox(
height: 250.h,
child: Column(
children: [
Image.asset(url,
width: 140.w, height: 99.h),
SizedBox(
height: 40.h,
),
Text(
desc,
style: AppText.gray50Text12,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
SizedBox(
height: 30.h,
),
if (onTap != null)
BorderRedBtnWidget(
content: "重新加载",
onClick: onTap,
padding: 40.w,
),
],
),
),
),
);
}

/// 加载成功但数据为空 View
Widget _buildLoadingSucButEmptyView() {
return _buildGeneralTapView(
url: "images/icon_empty.png",
desc: "暂无数据",
onTap: null,
);
}

/// 网络加载错误页面
Widget _buildNetworkBlockedView() {
return _buildGeneralTapView(
url: "images/icon_network_blocked.png",
desc: widget.networkBlockedDesc,
onTap: () {
widget.todoAfterNetworkBlocked(widget);
});
}

/// 加载错误页面
Widget _buildErrorView() {
return _buildGeneralTapView(
url: "images/icon_error.png",
desc: widget.errorDesc,
onTap: () {
widget.todoAfterError(widget);
});
}

使用


  Widget _buildBody() {
var loadingView = LoadingView(
loadingStatus: LoadingStatus.loading,
child: _buildContent(),
todoAfterNetworkBlocked: (LoadingView widget) {
// 网络错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
widget.updateStatus(LoadingStatus.error);
});
},
todoAfterError: (LoadingView widget) {
// 接口错误,点击重试
widget.updateStatus(LoadingStatus.loading);
Future.delayed(Duration(milliseconds: 1000), () {
// widget.updateStatus(LoadingStatus.loading_suc);
widget.updateStatus(LoadingStatus.loading_suc_but_empty);
});
},
);
Future.delayed(Duration(milliseconds: 1000), (){
loadingView.updateStatus(LoadingStatus.network_blocked);
});
return loadingView;
}

总结


至此已经完成了对整个Loading组件的封装,代码已上传Github


作者:Fitem
链接:https://juejin.cn/post/7063025458761170981
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

丢掉丑陋的 toast,会动的 toast 更有趣!

前言我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种.  说实话,这种toast 的体验很糟糕。假设是新手用户,他...
继续阅读 »

前言

我们通常会用 toast(也叫吐司)来显示提示信息,例如网络请求错误,校验错误等等。大多数 App的 toast 都很简单,简单的半透明黑底加上白色文字草草了事,比如下面这种. image.png 说实话,这种toast 的体验很糟糕。假设是新手用户,他们并不知道 toast 从哪里出来,等出现错误的时候,闪现出来的时候,可能还没抓住内容的重点就消失了(尤其是想截屏抓错误的时候,更抓狂)。这是因为一个是这种 toast 一般比较小,而是动效非常简单,用来提醒其实并不是特别好。怎么破?本篇来给大家介绍一个非常有趣的 toast 组件 —— motion_toast

motion_toast 介绍

从名字就知道,motion_toast 是支持动效的,除此之外,它的颜值还很高,下面是它的一个示例动图,仔细看那个小闹钟图标,是在跳动的哦。这种提醒效果比起常用的 toast 来说醒目多了,也更有趣味性。 center_motion_toast_2.gif 下面我们看看 motion_toast 的特性:

  • 可以通过动画图标实现动效;
  • 内置了成功、警告、错误、提醒和删除类型;
  • 支持自定义;
  • 支持不同的主题色;
  • 支持 null safety;
  • 心跳动画效果;
  • 完全自定义的文本内容;
  • 内置动画效果;
  • 支持自定义布局(LTR 和 RTL);
  • 自定义持续时长;
  • 自定义展现位置(居中,底部或顶部);
  • 支持长文本显示;
  • 自定义背景样式;
  • 自定义消失形式。

可以看到,除了能够开箱即用之外,我们还可以通过自定义来丰富 toast 的样式,使之更有趣。

示例

介绍完了,我们来一些典型的示例吧,首先在 pubspec.yaml 中添加依赖motion_toast: ^2.0.0(最低Dart版本需要2.12)。

最简单用法

只需要一行代码搞定!其他参数在 success 的命名构造方法中默认了,因此使用非常简单。

MotionToast.success(description: '操作成功!').show(context);
复制代码

其他内置的提醒

内置的提醒也支持我们修改默认参数进行样式调整,如标题、位置、宽度、显示位置、动画曲线等等。

// 错误提示
MotionToast.error(
description: '发生错误!',
width: 300,
position: MOTION_TOAST_POSITION.center,
).show(context);

//删除提示
MotionToast.delete(
description: '已成功删除',
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromLeft,
animationCurve: Curves.bounceIn,
).show(context);

// 信息提醒(带标题)
MotionToast.info(
description: '这是一条提醒,可能会有很多行。toast 会自动调整高度显示',
title: '提醒',
titleStyle: TextStyle(fontWeight: FontWeight.bold),
position: MOTION_TOAST_POSITION.bottom,
animationType: ANIMATION.fromBottom,
animationCurve: Curves.linear,
dismissable: true,
).show(context);

不过需要注意的是,一个是 dismissable 参数只对显示位置在底部的有用,当在底部且dismissable为 true 时,点击空白处可以让 toast 提前消失。另外就是显示位置 position 和 animationType 是存在某些互斥关系的。从源码可以看到底部显示的时候,animationType不能是 fromTop,顶部显示的时候 animationType 不能是 fromBottom

void _assertValidValues() {
assert(
(position == MOTION_TOAST_POSITION.bottom &&
animationType != ANIMATION.fromTop) ||
(position == MOTION_TOAST_POSITION.top &&
animationType != ANIMATION.fromBottom) ||
(position == MOTION_TOAST_POSITION.center),
);
}

自定义 toast

自定义其实就是使用 MotionToast 构建一个实例,其中,descriptionicon 和 primaryColor参数是必传的。自定义的参数很多,使用的时候建议看一下源码注释。

MotionToast(
description: '这是自定义 toast',
icon: Icons.flag,
primaryColor: Colors.blue,
secondaryColor: Colors.green[300],
descriptionStyle: TextStyle(
color: Colors.white,
),
position: MOTION_TOAST_POSITION.center,
animationType: ANIMATION.fromRight,
animationCurve: Curves.easeIn,
).show(context);

下面对自定义的一些参数做一下解释:

  • icon:图标,IconData 类,可以使用系统字体图标;
  • primaryColor:主颜色,也就是大的背景底色;
  • secondaryColor:辅助色,也就是图标和旁边的竖条的颜色;
  • descriptionStyle:toast 文字的字体样式;
  • title:标题文字;
  • titleStyle:标题文字样式;
  • toastDuration:显示时长;
  • backgroundType:背景类型,枚举值,共三个可选值,transparentsolid和 lighter,默认是 lighterlighter其实就是加了一层白色底色,然后再将原先的背景色(主色调)加上一定的透明度叠加到上面,所以看起来会泛白。
  • onClose:关闭时回调,可以用于出现多个错误时依次展示,或者是关闭后触发某些动作,如返回上一页。

总结

看完之后,是不是觉得以前的 toast 太丑了?用 motion_toast来一个更有趣的吧。另外,整个 motion_toast 的源码并不多,有兴趣的可以读读源码,了解一下toast 的实现也是不错的。


作者:岛上码农
链接:https://juejin.cn/post/7042301322376265742
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

这一次,解决Flutter Dialog的各种痛点!

前言 Q:你一生中闻过最臭的东西,是什么? A:我那早已腐烂的梦。 兄弟萌!!!我又来了! 这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包! 将之前的flutter_smart_dialog,在保持api稳定的基础...
继续阅读 »

前言



Q:你一生中闻过最臭的东西,是什么?


A:我那早已腐烂的梦。



兄弟萌!!!我又来了!


这次,我能自信的对大家说:我终于给大家带了一个,能真正帮助大家解决诸多坑比场景的pub包!


将之前的flutter_smart_dialog,在保持api稳定的基础上,进行了各种抓头重构,解决了一系列问题


现在,我终于可以说:它现在是一个简洁,强大,侵入性极低的pub包!


关于侵入性问题



  • 之前为了解决返回关闭弹窗,使用了一个很不优雅的解决方法,导致侵入性有点高

  • 这真是让我如坐针毡,如芒刺背,如鲠在喉,这个问题终于搞定了!


同时,我在pub包内部设计了一个弹窗栈,能自动移除栈顶弹窗,也可以定点移除栈内标记的弹窗。


问题


使用系统弹窗存在一系列坑,来和各位探讨探讨




  • 必须传BuildContext



    • 在一些场景必须多做一些传参工作,蛋痛但不难的问题




  • loading弹窗



    • 使用系统弹窗做loading弹窗,肯定遇到过这个坑比问题

      • loading封装在网络库里面:请求网络时加载loading,手贱按了返回按钮,关闭了loading

      • 然后请求结束后发现:特么我的页面怎么被关了!!!



    • 系统弹窗就是一个路由页面,关闭系统就是用pop方法,这很容易误关正常页面

      • 当然肯定有解决办法,路由监听的地方处理,此处就不细表了






  • 某页面弹出了多个系统Dialog,很难定点关闭某个非栈顶弹窗



    • 蛋蛋,这是路由入栈出栈机制导致的,理解的同时也一样要吐槽




  • 系统Dialog,点击事件无法穿透暗色背景



    • 这个坑比问题,我是真没辙




思考


上面列举了一些比较常见的问题,最严重的问题,应该就是loading的问题



  • loading是个超高频使用的弹窗,关闭loading弹窗的方法,同时也能关闭正常使用的页面,本身就是一个隐患

  • 本菜狗不具备大厂大佬们魔改flutter的能力,菜则思变,我只能从其它方向切入,寻求解决方案


系统的Page就是基于Overlay去实现的,咱们也要骚起来,从Overlay入手


这次,我要一次性帮各位解决:toast消息,loading弹窗,以及更强大的自定义dialog!


快速上手


初始化



dependencies:
flutter_smart_dialog: ^3.0.0


初始化方式一:强力推荐😃




  • 配置更加简洁


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
initialRoute: RouteConfig.main,
getPages: RouteConfig.getPages,
// here
navigatorObservers: [FlutterSmartDialog.observer],
// here
builder: FlutterSmartDialog.init(),
);
}
}


初始化方式二:兼容老版本😊




  • 老版本初始化方式仍然有效,区别是:需要在就加载MaterialApp之前,调用下FlutterSmartDialog.monitor()


void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// here
FlutterSmartDialog.monitor();
return MaterialApp(
home: SmartDialogPage(),
// here
navigatorObservers: [FlutterSmartDialog.observer],
/// here
builder: (BuildContext context, Widget? child) {
return FlutterSmartDialog(child: child);
},
);
}
}


大功告成🚀



上面俩种初始化方式,任选一种即可;然后,就可以完整使用本库的所有功能了


我非常推荐第一种初始化的方式,因为足够简洁;简洁明了的东西用起来,会让人心情愉悦🌞


极简使用



  • toast使用💬


SmartDialog.showToast('test toast');

toastDefault



  • loading使用


SmartDialog.showLoading();
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingDefault



  • dialog使用🎨


var custom = Container(
height: 80,
width: 180,
decoration: BoxDecoration(
color: Colors.black,
borderRadius: BorderRadius.circular(20),
),
alignment: Alignment.center,
child: Text('easy custom dialog', style: TextStyle(color: Colors.white)),
);
// here
SmartDialog.show(widget: custom, isLoadingTemp: false);

dialogEasy


OK,上面展示了,只需要极少的代码,就可以调用相应的功能


当然,内部还有不少地方做了特殊优化,接下来,我会详细的向大家描述下


你可能会有的疑问


初始化框架的时候,相比以前,居然让大家多写了一个参数,内心十分愧疚😩


关闭页面本质上是一个比较复杂的情况,涉及到



  • 物理返回按键

  • AppBar的back按钮

  • 手动pop


为了监控这些情况,不得已增加了一个路由监控参数



关于 FlutterSmartDialog.init()



本方法不会占用你的builder参数,init内部回调出来了builder,你可以大胆放心的继续套



  • 例如:继续套Bloc全局实例😄


class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return GetMaterialApp(
initialRoute: RouteConfig.main,
getPages: RouteConfig.getPages,
navigatorObservers: [FlutterSmartDialog.observer],
builder: FlutterSmartDialog.init(builder: _builder),
);
}
}

Widget _builder(BuildContext context, Widget? child) {
return MultiBlocProvider(
providers: [
BlocProvider.value(value: BlocSpanOneCubit()),
],
child: child!,
);
}


实体返回键



对返回按钮的监控,是非常重要的,基本能覆盖大多数情况


initBack



pop路由



虽然对返回按钮的监控能覆盖大多数场景,但是一些手动pop的场景就需要新增参数监控



  • 不加FlutterSmartDialog.observer

    • 如果打开了穿透参数(就可以和弹窗后的页面交互),然后手动关闭页面

    • 就会出现这种很尴尬的情况




initPopOne



  • 加了FlutterSmartDialog.observer,就能比较合理的处理了

    • 当然,这里的过渡动画,也提供了参数控制是否开启❤




initPopTwo



超实用的参数:backDismiss




  • 这个参数是默认设置为true,返回的时候会默认关闭弹窗;如果设置为false,将不会关闭页面

    • 这样就可以十分轻松的做一个紧急弹窗,禁止用户的下一步操作



  • 我们来看一个场景:假定某开源作者决定弃坑软件,不允许用户再使用该软件的弹窗


SmartDialog.show(
// here
backDismiss: false,
clickBgDismissTemp: false,
isLoadingTemp: false,
widget: Container(
height: 480,
width: 500,
padding: EdgeInsets.all(20),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
color: Colors.white,
),
alignment: Alignment.topCenter,
child: SingleChildScrollView(
child: Wrap(
direction: Axis.vertical,
crossAxisAlignment: WrapCrossAlignment.center,
spacing: 10,
children: [
// title
Text(
'特大公告',
style: TextStyle(fontSize: 30, fontWeight: FontWeight.bold),
),
// content
Text('鄙人日夜钻研下面秘籍,终于成功钓到富婆'),
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211102213746.jpeg',
height: 200,
width: 400,
),
Text('鄙人思考了三秒钟,怀着\'沉重\'的心情,决定弃坑本开源软件'),
Text('本人今后的生活是富婆和远方,已无\'精力\' 再维护本开源软件了'),
Text('各位叼毛,有缘江湖再见!'),
// button (only method of close the dialog)
ElevatedButton(
onPressed: () => SmartDialog.dismiss(),
child: Text('再会!'),
)
],
),
),
),
);

hardClose


从上面的效果图可以看出来



  • 点击遮罩,无法关闭弹窗

  • 点击返回按钮无法关闭弹窗

  • 只能点我们自己的按钮,才能关闭弹窗,点击按钮的逻辑可以直接写成关闭app之类


只需要俩个简单的参数设置,就能实现这样一个很棒的应急弹窗



设置全局参数



SmartDialog的全局参数都有着一个比较合理的默认值


为了应付多变的场景,你可以修改符合你自己要求的全局参数



  • 设置符合你的要求的数据,放在app入口就行初始化就行

    • 注:如果没有特殊要求,完全可以不用初始化全局参数




SmartDialog.config
..alignment = Alignment.center
..isPenetrate = false
..clickBgDismiss = true
..maskColor = Colors.black.withOpacity(0.3)
..maskWidget = null
..animationDuration = Duration(milliseconds: 260)
..isUseAnimation = true
..isLoading = true;


  • 代码的注释写的很完善,某个参数不明白的,点进去看看就行了


image-20211102223129866


Toast篇


toast的特殊性


严格来说,toast是一个非常特殊的弹窗,我觉得理应具备下述的特征



toast消息理应一个个展示,后续消息不应该顶掉前面的toast




  • 这是一个坑点,如果框架内部不做处理,很容易出现后面toast会直接顶掉前面toast的情况


toastOne



展示在页面最上层,不应该被一些弹窗之类遮挡




  • 可以发现loading和dialog的遮罩等布局,均未遮挡toast信息


toastTwo



对键盘遮挡情况做处理




  • 键盘这玩意有点坑,会直接遮挡所有布局,只能曲线救国

    • 在这里做了一个特殊处理,当唤起键盘的时候,toast自己会动态的调整自己和屏幕底部的距离

    • 这样就能起到一个,键盘不会遮挡toast的效果




toastSmart


自定义Toast



参数说明



toast暴露的参数其实并不多,只提供了四个参数



  • 例如:toast字体大小,字体颜色,toast的背景色等等之类,我都没提供参数

    • 一是觉得提供了这些参数,会让整个参数输入变的非常多,乱花渐入迷人眼

    • 二是觉得就算我提供了很多参数,也不一定会满足那些奇奇怪怪的审美和需求



  • 基于上述的考虑,我直接提供了底层参数,直接将widget参数提供出来

    • 你可以随心所欲的定制toast了

    • 注意:使用了widget参数,msgalignment参数会失效




image-20211031155838900



调整toast显示的位置



SmartDialog.showToast('the toast at the bottom');
SmartDialog.showToast('the toast at the center', alignment: Alignment.center);
SmartDialog.showToast('the toast at the top', alignment: Alignment.topCenter);

toastLocation



更强大的自定义toast




  • 首先,整一个自定义toast


class CustomToast extends StatelessWidget {
const CustomToast(this.msg, {Key? key}) : super(key: key);

final String msg;

@override
Widget build(BuildContext context) {
return Align(
alignment: Alignment.bottomCenter,
child: Container(
margin: EdgeInsets.only(bottom: 30),
padding: EdgeInsets.symmetric(horizontal: 20, vertical: 7),
decoration: BoxDecoration(
color: _randomColor(),
borderRadius: BorderRadius.circular(100),
),
child: Row(mainAxisSize: MainAxisSize.min, children: [
//icon
Container(
margin: EdgeInsets.only(right: 15),
child: Icon(Icons.add_moderator, color: _randomColor()),
),

//msg
Text('$msg', style: TextStyle(color: Colors.white)),
]),
),
);
}

Color _randomColor() {
return Color.fromRGBO(
Random().nextInt(256),
Random().nextInt(256),
Random().nextInt(256),
1,
);
}
}


  • 使用


SmartDialog.showToast('', widget: CustomToast('custom toast'));


  • 效果


toastCustom


Loading篇


避坑指南



  • 开启loading后,可以使用以下方式关闭

    • SmartDialog.dismiss():可以关闭loading和dialog

    • status设置为SmartStatus.loading:仅仅关闭loading




// easy close
SmartDialog.dismiss();
// exact close
SmartDialog.dismiss(status: SmartStatus.loading);


  • 一般来说,loading弹窗是封装在网络库里面的,随着请求状态的自动开启和关闭

    • 基于这种场景,我建议:使用dismiss时,加上status参数,将其设置为:SmartStatus.loading



  • 坑比场景

    • 网络请求加载的时候,loading也随之打开,这时很容易误触返回按钮,关闭loading

    • 当网络请求结束时,会自动调用dismiss方法

    • 因为loading已被关闭,假设此时页面又有SmartDialog的弹窗,未设置status的dismiss就会关闭SmartDialog的弹窗

    • 当然,这种情况很容易解决,封装进网络库的loading,使用:SmartDialog.dismiss(status: SmartStatus.loading); 关闭就行了



  • status参数,是为了精确关闭对应类型弹窗而设计的参数,在一些特殊场景能起到巨大的作用

    • 如果大家理解这个参数的含义,那对于何时添加status参数,必能胸有成竹




参数说明


参数在注释里面写的十分详细,就不赘述了,来看看效果


image-20211031215728656



  • maskWidgetTemp:强大的遮罩自定义功能😆,发挥你的脑洞吧。。。


var maskWidget = Container(
width: double.infinity,
height: double.infinity,
child: Opacity(
opacity: 0.6,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101103911.jpeg',
fit: BoxFit.fill,
),
),
);
SmartDialog.showLoading(maskWidgetTemp: maskWidget);

loadingOne



  • maskColorTemp:支持快捷自定义遮罩颜色


SmartDialog.showLoading(maskColorTemp: randomColor().withOpacity(0.3));

/// random color
Color randomColor() => Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingTwo



  • background:支持加载背景自定义


SmartDialog.showLoading(background: randomColor());

/// random color
Color randomColor() => Color.fromRGBO(
Random().nextInt(256), Random().nextInt(256), Random().nextInt(256), 1);

loadingThree



  • isLoadingTemp:动画效果切换


SmartDialog.showLoading(isLoadingTemp: false);

loadingFour



  • isPenetrateTemp:交互事件可以穿透遮罩,这是个十分有用的功能,对于一些特殊的需求场景十分关键


SmartDialog.showLoading(isPenetrateTemp: true);

loadingFive


自定义Loading


使用showLoading可以轻松的自定义出强大的loading弹窗;鄙人脑洞有限,就简单演示下



自定义一个loading布局



class CustomLoading extends StatefulWidget {
const CustomLoading({Key? key, this.type = 0}) : super(key: key);

final int type;

@override
_CustomLoadingState createState() => _CustomLoadingState();
}

class _CustomLoadingState extends State
with TickerProviderStateMixin {
late AnimationController _controller;

@override
void initState() {
_controller = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
_controller.forward();
_controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_controller.reset();
_controller.forward();
}
});
super.initState();
}

@override
Widget build(BuildContext context) {
return Stack(children: [
// smile
Visibility(visible: widget.type == 0, child: _buildLoadingOne()),

// icon
Visibility(visible: widget.type == 1, child: _buildLoadingTwo()),

// normal
Visibility(visible: widget.type == 2, child: _buildLoadingThree()),
]);
}

Widget _buildLoadingOne() {
return Stack(alignment: Alignment.center, children: [
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101174606.png',
height: 110,
width: 110,
),
),
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101181404.png',
height: 60,
width: 60,
),
]);
}

Widget _buildLoadingTwo() {
return Stack(alignment: Alignment.center, children: [
Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101162946.png',
height: 50,
width: 50,
),
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101173708.png',
height: 80,
width: 80,
),
),
]);
}

Widget _buildLoadingThree() {
return Center(
child: Container(
height: 120,
width: 180,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: Column(mainAxisSize: MainAxisSize.min, children: [
RotationTransition(
alignment: Alignment.center,
turns: _controller,
child: Image.network(
'https://cdn.jsdelivr.net/gh/xdd666t/MyData@master/pic/flutter/blog/20211101163010.png',
height: 50,
width: 50,
),
),
Container(
margin: EdgeInsets.only(top: 20),
child: Text('loading...'),
),
]),
),
);
}

@override
void dispose() {
_controller.dispose();
super.dispose();
}
}


来看看效果




  • 效果一


SmartDialog.showLoading(isLoadingTemp: false, widget: CustomLoading());
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingSmile



  • 效果二


SmartDialog.showLoading(
isLoadingTemp: false,
widget: CustomLoading(type: 1),
);
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingIcon



  • 效果三


SmartDialog.showLoading(widget: CustomLoading(type: 2));
await Future.delayed(Duration(seconds: 2));
SmartDialog.dismiss();

loadingNormal


Dialog篇


花里胡哨



弹窗从不同位置弹出,动画是有区别的



image-20211031221419600



  • alignmentTemp:该参数设置不同,动画效果会有所区别


var location = ({
double width = double.infinity,
double height = double.infinity,
}) {
return Container(width: width, height: height, color: randomColor());
};

//left
SmartDialog.show(
widget: location(width: 50),
alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
widget: location(height: 50),
alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
widget: location(width: 50),
alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
widget: location(height: 50),
alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//center
SmartDialog.show(
widget: location(height: 100, width: 100),
alignmentTemp: Alignment.center,
isLoadingTemp: false,
);

dialogLocation



  • isPenetrateTemp:交互事件穿透遮罩


SmartDialog.show(
alignmentTemp: Alignment.centerRight,
isPenetrateTemp: true,
clickBgDismissTemp: false,
widget: Container(
width: 80,
height: double.infinity,
color: randomColor(),
),
);

dialogPenetrate


dialog栈



  • 这是一个强大且实用的功能!

    • 可以很轻松的定点关闭某个弹窗




var stack = ({
double width = double.infinity,
double height = double.infinity,
String? msg,
}) {
return Container(
width: width,
height: height,
color: randomColor(),
alignment: Alignment.center,
child: Text('弹窗$msg', style: TextStyle(color: Colors.white)),
);
};

//left
SmartDialog.show(
tag: 'A',
widget: stack(msg: 'A', width: 60),
alignmentTemp: Alignment.centerLeft,
);
await Future.delayed(Duration(milliseconds: 500));
//top
SmartDialog.show(
tag: 'B',
widget: stack(msg: 'B', height: 60),
alignmentTemp: Alignment.topCenter,
);
await Future.delayed(Duration(milliseconds: 500));
//right
SmartDialog.show(
tag: 'C',
widget: stack(msg: 'C', width: 60),
alignmentTemp: Alignment.centerRight,
);
await Future.delayed(Duration(milliseconds: 500));
//bottom
SmartDialog.show(
tag: 'D',
widget: stack(msg: 'D', height: 60),
alignmentTemp: Alignment.bottomCenter,
);
await Future.delayed(Duration(milliseconds: 500));

//center:the stack handler
SmartDialog.show(
alignmentTemp: Alignment.center,
isLoadingTemp: false,
widget: Container(
decoration: BoxDecoration(
color: Colors.white, borderRadius: BorderRadius.circular(15)),
padding: EdgeInsets.symmetric(horizontal: 30, vertical: 20),
child: Wrap(spacing: 20, children: [
ElevatedButton(
child: Text('关闭弹窗A'),
onPressed: () => SmartDialog.dismiss(tag: 'A'),
),
ElevatedButton(
child: Text('关闭弹窗B'),
onPressed: () => SmartDialog.dismiss(tag: 'B'),
),
ElevatedButton(
child: Text('关闭弹窗C'),
onPressed: () => SmartDialog.dismiss(tag: 'C'),
),
ElevatedButton(
child: Text('关闭弹窗D'),
onPressed: () => SmartDialog.dismiss(tag: 'D'),
),
]),
),
);

dialogStack


骚气的小技巧


有一种场景比较蛋筒



  • 我们使用StatefulWidget封装了一个小组件

  • 在某个特殊的情况,我们需要在这个组件外部,去触发这个组件内部的一个方法

  • 对于这种场景,有不少实现方法,但是弄起来可能有点麻烦


这里提供一个简单的小思路,可以非常轻松的触发,组件内部的某个方法



  • 建立一个小组件


class OtherTrick extends StatefulWidget {
const OtherTrick({Key? key, this.onUpdate}) : super(key: key);

final Function(VoidCallback onInvoke)? onUpdate;

@override
_OtherTrickState createState() => _OtherTrickState();
}

class _OtherTrickState extends State {
int _count = 0;

@override
void initState() {
// here
widget.onUpdate?.call(() {
_count++;
setState(() {});
});

super.initState();
}

@override
Widget build(BuildContext context) {
return Center(
child: Container(
padding: EdgeInsets.symmetric(horizontal: 50, vertical: 20),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
child: Text('Counter: $_count ', style: TextStyle(fontSize: 30.0)),
),
);
}
}


  • 展示这个组件,然后外部触发它


VoidCallback? callback;

// display
SmartDialog.show(
alignmentTemp: Alignment.center,
widget: OtherTrick(
onUpdate: (VoidCallback onInvoke) => callback = onInvoke,
),
);

await Future.delayed(Duration(milliseconds: 500));

// handler
SmartDialog.show(
alignmentTemp: Alignment.centerRight,
maskColorTemp: Colors.transparent,
widget: Container(
height: double.infinity,
width: 150,
color: Colors.white,
alignment: Alignment.center,
child: ElevatedButton(
child: Text('add'),
onPressed: () => callback?.call(),
),
),
);


  • 来看下效果


trick


最后



相关地址




作者:小呆呆666
链接:https://juejin.cn/post/7026150456673959943
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

DiffUtil 让 RecyclerView 更好用

DiffUtil 让 RecyclerView 更好用前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。DiffUtil是什么DiffU...
继续阅读 »

DiffUtil 让 RecyclerView 更好用

前几天在写局部刷新RecyclerView时,评论区有掘友提到了DiffUtil,说实话,确实没有在项目中用到过,查了资料,DiffUtil帮我们做了很多刷新很多工作,真香。

DiffUtil是什么

DiffUtil 是来自recycleview-v7下的工具类,Diff 直接翻译过来是 差异、对比,所以这个工具类主要帮助我们对比两个数据集,寻找出最小的变化量。那么它和RecyclerView有什么关系呢,实际上我们只要把新旧数据集给到DiffUtil,那么它就会自动帮我们对比数据,并且刷新适配器,而不用我们判断,是增加了删除了等等,DiffUtil对比之后自动帮我们搞定,这就是它非常好用的地方了

常规的适配器

我们先用RecyclerView写一个常规的列表。
它拥有刷新item和item局部刷新的功能。
代码如下:

MainActivity: 主界面

public class MainActivity extends AppCompatActivity {

private RecyclerView recyclerView;

private List<PersonInfo> mDatas;
private PersonAdapter personAdapter;

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

recyclerView = findViewById(R.id.recyclerView);
initData();
recyclerView.setLayoutManager(new LinearLayoutManager(this));
personAdapter = new PersonAdapter(this, mDatas);
recyclerView.setAdapter(personAdapter);
}

private void initData() {
mDatas = new ArrayList<>();
mDatas.add(new PersonInfo(1, "姓名1"));
mDatas.add(new PersonInfo(2, "姓名2"));
mDatas.add(new PersonInfo(3, "姓名3"));
mDatas.add(new PersonInfo(4, "姓名4"));
mDatas.add(new PersonInfo(5, "姓名5"));
mDatas.add(new PersonInfo(6, "姓名6"));
mDatas.add(new PersonInfo(7, "姓名7"));
mDatas.add(new PersonInfo(8, "姓名8"));
mDatas.add(new PersonInfo(9, "姓名9"));
mDatas.add(new PersonInfo(10, "姓名10"));
mDatas.add(new PersonInfo(11, "姓名11"));
}

public void ADD(View view) {
int position = mDatas.size();
List<PersonInfo> tempData = new ArrayList<>();

tempData.add(new PersonInfo(12, "姓名12"));
tempData.add(new PersonInfo(13, "姓名13"));
tempData.add(new PersonInfo(14, "姓名114"));

mDatas.addAll(tempData);
personAdapter.notifyItemRangeInserted(position, tempData.size());
}

public void DELETE(View view) {
mDatas.remove(1);
personAdapter.notifyItemRemoved(1);
}

public void UPDATE(View view) {
mDatas.get(1).setName("姓名:我被更新了");
personAdapter.notifyItemChanged(1);
}

public void UPDATE2(View view) {
mDatas.get(1).setName("姓名:我被更新了");

Bundle payload = new Bundle();
payload.putString("KEY_NAME", mDatas.get(1).getName());
personAdapter.notifyItemChanged(1, payload);
}
}
复制代码

PersonAdapter: 适配器

public class PersonAdapter extends RecyclerView.Adapter<PersonAdapter.DiffVH> {
private List<PersonInfo> mDatas;
private LayoutInflater mInflater;

public PersonAdapter(Context context, List<PersonInfo> mDatas) {
this.mDatas = mDatas;
mInflater = LayoutInflater.from(context);
}

public void setDatas(List<PersonInfo> mDatas) {
this.mDatas = mDatas;
}

@Override
public DiffVH onCreateViewHolder(ViewGroup parent, int viewType) {
return new DiffVH(mInflater.inflate(R.layout.item_person, parent, false));
}

@Override
public void onBindViewHolder(final DiffVH holder, final int position) {
PersonInfo personInfo = mDatas.get(position);
holder.tv_index.setText(String.valueOf(personInfo.getIndex()));
holder.tv_name.setText(String.valueOf(personInfo.getName()));
}

@Override
public void onBindViewHolder(DiffVH holder, int position, List<Object> payloads) {
if (payloads.isEmpty()) {
onBindViewHolder(holder, position);
} else {
Bundle payload = (Bundle) payloads.get(0);
PersonInfo bean = mDatas.get(position);
for (String key : payload.keySet()) {
switch (key) {
case "KEY_INDEX":
holder.tv_index.setText(String.valueOf(bean.getIndex()));
break;
case "KEY_NAME":
holder.tv_name.setText(String.valueOf(bean.getName()));
break;
default:
break;
}
}
}
}

@Override
public int getItemCount() {
return mDatas != null ? mDatas.size() : 0;
}

class DiffVH extends RecyclerView.ViewHolder {
TextView tv_index;
TextView tv_name;

public DiffVH(View view) {
super(view);
tv_index = view.findViewById(R.id.tv_index);
tv_name = view.findViewById(R.id.tv_name);
}
}
}

复制代码

PersonInfo: 实体类

public class PersonInfo {
private int index;
private String name;

public PersonInfo(int index, String name) {
this.index = index;
this.name = name;
}

public int getIndex() {
return index;
}

public void setIndex(int index) {
this.index = index;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
复制代码

activity_main

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context=".MainActivity">

<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="增加"
android:onClick="ADD"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="删除"
android:onClick="DELETE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="修改"
android:onClick="UPDATE"/>

<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="局部更新"
android:onClick="UPDATE2"/>

</LinearLayout>


<android.support.v7.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="match_parent"
android:layout_height="match_parent" />


</LinearLayout>

复制代码

item_person

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
tools:context=".MainActivity">

<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:background="@color/purple_200"
android:orientation="horizontal"
android:padding="5dp">

<TextView
android:id="@+id/tv_index"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />

<TextView
android:id="@+id/tv_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_toEndOf="@+id/tv_index" />

</RelativeLayout>


</LinearLayout>
复制代码

引入DiffUtil

我们创建一个DiffUtil类,在需要更新的时候,用DiffUtil中的方法去代替原本的刷新方法。

用新增举例,这样就能达到更新的目的

public void ADD(View view) {
List<PersonInfo> newData = new ArrayList<>();
newData.addAll(mDatas);
newData.add(new PersonInfo(12, "姓名12"));
newData.add(new PersonInfo(13, "姓名13"));
newData.add(new PersonInfo(14, "姓名114"));

DiffUtil.calculateDiff(new DiffUtilCallBack(newData,mDatas), true).dispatchUpdatesTo(personAdapter);
mDatas = newData;
personAdapter.setDatas(mDatas);
}

复制代码

DiffUtilCallBack

public class DiffUtilCallBack extends DiffUtil.Callback {
private List<PersonInfo> newlist;
private List<PersonInfo> oldlist;

public DiffUtilCallBack(List<PersonInfo> newlist, List<PersonInfo> oldlist) {
this.newlist = newlist;
this.oldlist = oldlist;
}

@Override
public int getOldListSize() {
return oldlist.size();
}

@Override
public int getNewListSize() {
return newlist.size();
}

@Override
public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
//判断是否是同一个item,可以在这里处理 判断是否是相同item的逻辑,比如id之类的
return newlist.get(newItemPosition).getIndex() == oldlist.get(oldItemPosition).getIndex();
}

@Override
public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
//判断数据是否发生改变,这个 方法会在上面的方法返回true时调用, 因为虽然item是同一个,但有可能item的数据发生了改变
return newlist.get(newItemPosition).getName().equals(oldlist.get(oldItemPosition).getName());
}
}
复制代码

万能适配器中的DiffUtil

配合RecyclerView,我一直在使用万能适配器(BaseRecyclerViewAdapterHelper),如果你也习惯了使用万能适配器,在它的3.0方法中引入了对DiffUtil的支持。

直接看官方文档吧。

代码下载:https://github.com/CymChad/BaseRecyclerViewAdapterHelper/archive/refs/heads/master.zip

收起阅读 »

FastKV:一个真的很快的KV存储组件

一、前言 KV存储无论对于客户端还是服务端都是重要的构件。 对于Android客户端而言,最常见的莫过于SDK提供的SharePreferences(以下简称SP),但其低效率和ANR问题饱受诟病。 后来官方又推出了基于Kotlin的DataStore, 其中...
继续阅读 »

一、前言


KV存储无论对于客户端还是服务端都是重要的构件。

对于Android客户端而言,最常见的莫过于SDK提供的SharePreferences(以下简称SP),但其低效率和ANR问题饱受诟病。

后来官方又推出了基于Kotlin的DataStore, 其中的Preferences DataStore,换汤不换药,底层的存储策略还是一样的,目测该有的问题还是有。

18年年末微信开源了MMKV, 有较高热度。

我之前写过一个叫LightKV的Android客户端的KV存储组件,开源时间比MMKV要早一点,但关注量不多……不过话说回来,由于当时认知不足,LightKV的设计也不够成熟。


1.1 SP的不足


关于SP的缺点网上有不少讨论,这里主要提两个点:



  • 保存速度较慢


SP用内存层用HashMap保存,磁盘层则是用的XML文件保存。

每次更改,都需要将整个HashMap序列化为XML格式的报文然后整个写入文件。

归结其较慢的原因:

1、不能增量写入;

2、序列化比较耗时。



  • 可以能会导致ANR


public void apply() {
// ...省略无关代码...
QueuedWork.addFinisher(awaitCommit);
Runnable postWriteRunnable = new Runnable() {
@Override
public void run() {
awaitCommit.run();
QueuedWork.removeFinisher(awaitCommit);
}
};
SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
}

public void handleStopActivity(IBinder token, boolean show, int configChanges,
PendingTransactionActions pendingActions, boolean finalStateRequest, String reason) {
// ...省略无关代码...
// Make sure any pending writes are now committed.
if (!r.isPreHoneycomb()) {
QueuedWork.waitToFinish();
}
}

Activity stop时会等待SP的写入任务,如果SP的写入任务多且执行慢的话,可能会阻塞主线程较长时间,轻则卡顿,重则ANR。


1.2 MMKV的不足



  • 没有类型信息,不支持getAll

    MMKV的存储用类似于Protobuf的编码方式,只存储key和value本身,没有存类型信息(Protobuf用tag标记字段,信息更少)。

    由于没有记录类型信息,MMKV无法自动反序列化,也就无法实现getAll接口。

  • 读取相对较慢

    SP在加载的时候已经将value反序列化存在HashMap中了,读取的时候索引到之后就能直接引用了。

    而MMKV每次读取时都需要重新解码,除了时间上的消耗之外,还需要每次都创建新的对象。

    不过这不是大问题,相对SP没有差很多。

  • 需要引入so, 增加包体积

    引入MMKV需要增加的体积还是不少的,且不说jar包和aidl文件,光是一个arm64-v8a的so就有四百多K。



虽然说现在APP体积都不小,但毕竟增加体积对打包、分发和安装时间都多少有些影响。



  • 文件只增不减

    MMKV的扩容策略还是比较激进的,而且扩容之后不会主动trim size。

    比方说,假如有一个大value,让其扩容至1M,后面删除该value,哪怕有效内容只剩几K,文件大小还是保持在1M。

  • 可能会丢失数据

    前面的问题总的来说都不是什么“要紧”的问题,但是这个丢失数据确实是硬伤。

    MMKV官方有这么一段表述:

    通过 mmap 内存映射文件,提供一段可供随时写入的内存块,App 只管往里面写数据,由操作系统负责将内存回写到文件,不必担心 crash 导致数据丢失。





这个表述对一半不对一半。

如果数据完成写入到内存块,如果系统不崩溃,即使进程崩溃,系统也会将buffer刷入磁盘;

但是如果在刷入磁盘之前发生系统崩溃或者断电等,数据就丢失了,不过这种情况发生的概率不大;

另一种情况是数据写一半的时候进程崩溃或者被杀死,然后系统会将已写入的部分刷入磁盘,再次打开时文件可能就不完整了。

例如,MMKV在剩余空间不足时会回收无效的空间,如果这期间进程中断,数据可能会不完整。
MMKV官方的说明可以佐证:



CRC校验失败之后,MMKV有两种应对策略:直接丢弃所有数据,或者尝试读取数据(用户可以在初始化时设定)。

尝试读取数据不一定能恢复数据,甚至可能会读到一些错误的数据,得看运气。


这个过程是比较容易复现的,下面是其中一种复现路径:



  1. 新增和删除若干key-value
    得到数据如下:





  1. 插入一个大字符串,触发扩容,扩容前会触发垃圾回收




  2. 断点打在执行memmove的循环中,执行一部分memmove, 然后在手机上杀死进程






  1. 再次打开APP,数据丢失



相比之下,SP虽然低效,但至少不会丢失数据。


二、FastKV


在总结了之前的经验和感悟之后,笔者实现了一个高效且可靠的版本,且将其命名为: FastKV


2.1 特性


FastKV有以下特性:



  1. 读写速度快

    • FastKV采用二进制编码,编码后的体积相对XML等文本编码要小很多。

    • 增量编码:FastKV记录了各个key-value相对文件的偏移量,更新数据时,可以直接在对应的位置写入数据。

    • 默认用mmap的方式记录数据,更新数据时直接写入到内存即可,没有IO阻塞。



  2. 支持多种写入模式

    • 除了mmap这种非阻塞的写入方式,FastKV也支持常规的阻塞式写入方式,
      并且支持同步阻塞和异步阻塞(分别类似于SharePreferences的commit和apply)。



  3. 支持多种类型

    • 支持常用的boolean/int/float/long/double/String等基础类型。

    • 支持ByteArray (byte[])。

    • 支持存储自定义对象。

    • 内置StringSet编码器 (为了兼容SharePreferences)。



  4. 方便易用

    • FastKV提供了了丰富的API接口,开箱即用。

    • 提供的接口其中包括getAll()和putAll()方法,
      所以迁移SharePreferences等框架的数据到FastKV很方便,当然,迁移FastKV的数据到其他框架也很方便。



  5. 稳定可靠

    • 通过double-write等方法确保数据的完整性。

    • 在API抛IO异常时提供降级处理。



  6. 代码精简

    • FastKV由纯Java实现,编译成jar包后体积仅30多K。




2.2 实现原理


2.2.1 编码


文件的布局:



[data_len | checksum | key-value | key-value|....]




  • data_len: 占4字节, 记录所有key-value所占字节数。

  • checksum: 占8字节,记录key-value部分的checksum。


key-value的数据布局:


+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| delete_flag | external_flag | type | key_len | key_content | value |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| 1bit | 1bit | 6bits | 1 byte | | |


  • delete_flag :标记当前key-value是否删除。

  • external_flag: 标记value部分是否写到额外的文件。

    注:对于数据量比较大的value,放在主文件会影响其他key-value的访问性能,因此,单独用一个文件来保存该value, 并在主文件中记录其文件名。

  • type: value类型,目前支持boolean/int/float/long/double/String/ByteArray以及自定义对象。

  • key_len: 记录key的长度,key_len本身占1字节,所以支持key的最大长度为255。

  • key_content: key的内容本身,utf8编码。

  • value: 基础类型的value, 直接编码(little-end);

    其他类型,先记录长度(用varint编码),再记录内容。

    String采用UTF-8编码,ByteArray无需编码,自定义对象实现Encoder接口,分别在Encoder的encode/decode方法中序列化和反序列化。


2.2.2 存储




  • mmap

    为了提高写入性能,FastKV默认采用mmap的方式写入。




  • 降级

    当mmap API发生IO异常时,降级到常规的blocking I/O,同时为了不影响当前线程,会将写入放到异步线程中执行。




  • 数据完整性

    如果在写入一部分的过程中发生中断(进程或系统),则文件可能会不完整。

    故此,需要用一些方法确保数据的完整性。

    当用mmap的方式打开时,FastKV采用double-write的方式:数据依次写入A/B两个文件,确保任何时刻总有一个文件完整的; 加载数据时,通过checksum、标记、数据合法性检验等方法验证数据的正确性。

    double-write可以防止进程崩溃后数据不完整,但mmap是系统定时刷盘,若在刷盘系统崩溃或者断电,仍会丢失更新(之前的数据还在,仅丢失更新)。 可以通过调用force()强制刷盘,但这就不能发挥mmap的优点了。

    基于此,FastKV也支持用blocking I/O的方式写文件(比mmap慢,但是能确保数据真正落盘)。

    当用blocking I/O的写入时,先写临时文件,完整写入后再删除主文件,然后重命名临时文件为主文件。

    FastKV支持同步的和异步的blocking I/O,写入方式类似于SP的commit和apply,但是序列化key-value的部分是增量的,比SP的序列化整个HashMap的方式要快许多。




  • 更新策略(增/删/改)

    新增:写入到数据的尾部。

    删除:delete_flag设置为1。

    修改:如果value部分的长度和原来一样,则直接写入原来的位置;
    否则,先写入key-value到数据尾部,再标记原来位置的delete_flag为1(删除),最后再更新文件的data_len和checksum。




  • gc/truncate

    删除key-value时会收集信息(统计删除的个数,以及所在位置,占用空间等)。

    GC的触发点有两个:

    1、新增key-value时剩余空间不足,且已删除的空间达到阈值,且腾出删除空间后足够写入当前key-value, 则触发GC;

    2、删除key-value时,如果删除空间达到阈值,或者删除的key-value个数达到阈值,则触发GC。

    GC后如果不用的空间达到设定阈值,则触发truncate(缩小文件大小)。




2.3 使用方法


2.3.1 导入


dependencies {
implementation 'io.github.billywei01:fastkv:1.0.2'
}

2.3.2 初始化


    FastKVConfig.setLogger(FastKVLogger)
FastKVConfig.setExecutor(ChannelExecutorService(4))

初始化可以按需设置日志回调和Executor。

建议传入自己的线程池,以复用线程。


日志接口提供三个级别的回调,按需实现即可。


    public interface Logger {
void i(String name, String message);

void w(String name, Exception e);

void e(String name, Exception e);
}

2.3.3 数据读写



  • 基本用法


    FastKV kv = new FastKV.Builder(path, name).build();
if(!kv.getBoolean("flag")){
kv.putBoolean("flag" , true);
}


  • 保存自定义对象


    FastKV.Encoder<?>[] encoders = new FastKV.Encoder[]{LongListEncoder.INSTANCE};
FastKV kv = new FastKV.Builder(path, name).encoder(encoders).build();

String objectKey = "long_list";
List<Long> list = new ArrayList<>();
list.add(100L);
list.add(200L);
list.add(300L);
kv.putObject(objectKey, list, LongListEncoder.INSTANCE);

List<Long> list2 = kv.getObject("long_list");

FastKV支持保存自定义对象,为了加载文件时能自动反序列化,需在构建FastKV实例时传入对象的编码器。

编码器为实现FastKV.Encoder的对象。

比如上面的LongListEncoder的实现如下:


public class LongListEncoder implements FastKV.Encoder<List<Long>> {
public static final LongListEncoder INSTANCE = new LongListEncoder();

@Override
public String tag() {
return "LongList";
}

@Override
public byte[] encode(List<Long> obj) {
return new PackEncoder().putLongList(0, obj).getBytes();
}

@Override
public List<Long> decode(byte[] bytes, int offset, int length) {
PackDecoder decoder = PackDecoder.newInstance(bytes, offset, length);
List<Long> list = decoder.getLongList(0);
decoder.recycle();
return (list != null) ? list : new ArrayList<>();
}
}

编码对象涉及序列化/反序列化。

这里推荐笔者的另外一个框架:github.com/BillyWei01/…


2.3.4 For Android


Android平台上的用法和常规用法一致,不过Android平台多了SharePreferences API,以及支持Kotlin。

FastKV的API兼容SharePreferences, 可以很轻松地迁移SharePreferences的数据到FastKV。

相关用法可参考:github.com/BillyWei01/…


三、 性能测试



  • 测试数据:搜集APP中的SharePreferenses汇总的部份key-value数据(经过随机混淆)得到总共四百多个key-value。由于日常使用过程中部分key-value访问多,部分访问少,所以构造了一个正态分布的访问序列。

  • 比较对象: SharePreferences 和 MMKV

  • 测试机型:荣耀20S


测试结果:



























写入(ms)读取(ms)
SharePreferences14906
MMKV349
FastKV141


  • SharePreferences提交用的是apply, 耗时依然不少。

  • MMKV的读取比SharePreferences要慢一些,写入则比之快许多。

  • FastKV无论读取还是写入都比另外两种方式要快。


四、结语


本文探讨了当下Android平台的各类KV存储方式,提出并实现了一种新的存储组件,着重解决了KV存储的效率和数据可靠性问题。

目前代码已上传Github: github.com/BillyWei01/…


作者:呼啸长风
链接:https://juejin.cn/post/7018522454171582500
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

更高级的 Android 启动任务调度库

在之前的文章中,我介绍了自研的 Android 启动任务调度工具 AndroidStartup。近期,因为在组件化项目中运用该项目的需要,我对这个库做了一番升级。在最新的 2.2 版本中,我新增了一些特性。相比于目前市面上其他的启动任务调度库,使其具备了更多的...
继续阅读 »

在之前的文章中,我介绍了自研的 Android 启动任务调度工具 AndroidStartup。近期,因为在组件化项目中运用该项目的需要,我对这个库做了一番升级。在最新的 2.2 版本中,我新增了一些特性。相比于目前市面上其他的启动任务调度库,使其具备了更多的优势。这里我只介绍下经过新的版本迭代之后该项目与其他项目的不同点。对于其基础的实现原理,可以参考我之前的文章 《异步、非阻塞式 Android 启动任务调度库》


1、支持多种线程模型


这是相对于 Jetpack 的启动任务库的优势,在指定任务的时候,你可以通过 ISchedulerJobthreadMode() 方法指定该任务执行的线程,当前支持主线程(ThreadMode.MAIN)和非主线程(ThreadMode.BACKGROUND)两种情况。前者在主线程当中执行,后者在线程池当中执行,同时,该库还允许你自定义自己的线程池。关于这块的实现原理可以参考之前的文章或者项目源码。


2、非阻塞的任务调度方式


在之前的文章中也提到了,如果说采用 CountDownLatch 等阻塞的方式来实现任务调度,虽然不会占用主线程的 CPU,但是子线程会被阻塞,一样会导致 CPU 空转,影响程序执行的性能,尤其启动的时候大量任务执行时的情况。所以,在这个库的设计中,我们使用了通知唤醒的方式进行任务调度。也就是,


首先,它会将所有的需要执行的任务收集起来;然后,它会根据任务的依赖关系指定分发和调度任务的子任务;最后,当当前任务执行完毕,该任务会通知所有的子任务按照顺序执行。大致实现逻辑如下,


override fun execute() {
val realJob = {
// 1. Run the task if match given process.
if (matcher.match(job.targetProcesses())) {
job.run(context)
}

// 2. Then sort children task.
children.sortBy { child -> -child.order() }

// 3. No matter the task invoked in current process or not,
// its children will be notified after that.
children.forEach { it.notifyJobFinished(this) }
}

try {
if (job.threadMode() == ThreadMode.MAIN) {
// Cases for main thread.
if (Thread.currentThread() == Looper.getMainLooper().thread) {
realJob.invoke()
} else {
mainThreadHandler.post { realJob.invoke() }
}
} else {
// Cases for background thread.
executor.execute { realJob.invoke() }
}
} catch (e: Throwable) {
throw SchedulerException(e)
}
}

3、非 Class 的依赖方式


之前在本项目中,以及其他的项目中可能采用了基于 Class 的形式进行任务依赖。这种使用方式存在一些问题,即在组件化开发的时候,Class 之间需要直接进行引用。这导致各个组件之间的强耦合。这显然不是我们希望的。


所以,为了更好地支持组件化,在该库的新版本中,我们允许通过 name() 方法执行任务的名称,以及通过 dependencies() 方法指定该任务依赖的其他任务的名称。name() 默认使用任务 Class 的全限定名。这样,当多个组件之间进行相互依赖的时候,只需要通过字符串指定名称而无需引用具体的类。


比如,一个任务在一个组件中定义如下,


@StartupJob class BlockingBackgroundJob : ISchedulerJob {

override fun name(): String = "blocking"

override fun threadMode(): ThreadMode = ThreadMode.BACKGROUND

override fun dependencies(): List<String> = emptyList()

override fun run(context: Context) {
Thread.sleep(5_000L) // 5 seconds
L.d("BlockingBackgroundJob done! ${Thread.currentThread()}")
toast("BlockingBackgroundJob done!")
}
}

在另一个组件中的另一个任务需要依赖上述任务的时候,定义如下,


@StartupJob class SubModuleTask : ISchedulerJob {

override fun dependencies(): List<String> = listOf("blocking")

override fun run(context: Context) {
Log.d("SubModuleTask", "runed ")
}
}

这样我们就实现组件化场景中的依赖关系了。


4、支持任务的优先级


在实际开发中,我们可能会遇到需要为所有的根任务或者一个任务的所有的子任务指定执行的先后顺序的场景。或者在组件化中,存在依赖关系,但是我们希望某个根任务优先执行,但是不想为每个子任务都执行依赖关系的时候,我们可以通过指定这个任务的优先级为最高来使其最先被执行。你可以通过 priority() 方法传递一个 0 到 100 的整数来指定任务的优先级。


@StartupJob class TopPriorityJob : ISchedulerJob{

override fun priority(): Int = 100

override fun run(context: Context) {
L.d("Top level job done!")
}
}

优先级局限于依赖关系相同的任务,所以是依赖关系的补充,不会造成歧义。


5、支持指定任务执行的进程,可自定义进程匹配策略


如果我们的项目支持多进程,而我们希望某些启动任务只在某个进程中执行而其他进程不需要执行,以此避免没必要的任务来提升任务执行的性能的时候,我们可以通过指定任务执行的进程来进行优化。你可以通过 targetProcesses() 传递一个进程的列表来指定该任务执行的所有进程。默认列表为空,表示运行在所有的进程。


对于进程的匹配,我们提供了 IProcessMatcher 这个接口,


interface IProcessMatcher {
fun match(target: List<String>): Boolean
}

你可以通过指定这个接口来自定义线程的匹配策略。


6、支持注解形式的组件化调用


在之前的版本中,通过 ContentProvider 的形式我们一样可以实现所有组件内任务的收集和调用。但是使用 ContentProvider 存在一些不便之处,比如 ContentProvider 的初始化实际在 Application 的 attachBaseContext(),如果我们的任务中一些操作需要放到 Application 的 onCreate() 中执行的时候,通过 ContentProvider 默认装载任务的调度方式就存在问题。而通过基于注解 + APT的形式,我们可以随意指定任务收集、整理和执行的时机,灵活性更好。


为了支持组件化,我们在之前的项目上做了一些拓展。之前的项目虽然也是基于注解发现机制,但是在组件化的应用中存在问题。在新的版本中,我们只是处理了组件化应用场景中的问题,但是使用方式上面完全兼容,只不过你需要为每个组件在 gradle.build 中增加一个行信息来指定组件的名称(就像 ARouter 一样),


javaCompileOptions {
annotationProcessorOptions {
arguments = [STARTUP_MODULE_NAME: project.getName()]
}
}

也就是说你还是通过 @StartupJob 注解将任务标记为启动任务,然后通过


launchStartup(this) {
scanAnnotations()
}

这行代码启动扫描并执行任务。


在新的版本中,所有生产的代码会被统一放到包 me.shouheng.startup.hunter 下面,然后通过 JobHunter$$组件名 的形式为每个组件生成自己的类,然后在扫描任务的时候通过加载这个包名之下的所有的代码来找到所有要执行的任务。如果你对组件化感兴趣可以直接阅读这块的源码实现。


总结


启动任务调度库的设计不算复杂,但是我却在之前的面试中两次被问到如何设计。这种类型的问题能很好地考察代码设计能力。相信阅读这个库的代码之后,此类的问题再也难不倒你。如果你对 APT+注解 的组件化实现方式等感兴趣一样可以阅读这个库的代码。


以上介绍了这个库的一些特性和优势,没用过多地介绍其源码实现,感兴趣的同学可以直接阅读项目的源码,相信你能够从代码中学到一些东西。对于示例项目,除了阅读这个项目的示例,还可以参考 Android-VMLib 这个项目。该项目地址:github.com/Shouheng88/…


作者:shouheng
链接:https://juejin.cn/post/7017315629216579591
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 收起阅读 »

flutter 优秀日志库 ulog

ulog ulog的想法和代码风格,设计方式与 Android logger库几乎无差别,差别在于ulog第一个版本不支持文件打印,但支持动态json库配置 库源码:github.com/smartbackme… v0.0.1只有基础的console打印,后面...
继续阅读 »

ulog


ulog的想法和代码风格,设计方式与 Android logger库几乎无差别,差别在于ulog第一个版本不支持文件打印,但支持动态json库配置
库源码:github.com/smartbackme…


v0.0.1只有基础的console打印,后面将会增加文件打印


开始使用


添加库
dependencies:
flutter_ulog: ^0.0.1


//Initialization
//构建基础adapter isLoggable可以通过不同type来拦截打印,或者关闭打印
class ConsoleAdapter extends ULogConsoleAdapter{
@override
bool isLoggable(ULogType type, String? tag) => true;
}
//初始化配置json库
ULog.init((value){
return "";
});
//添加打印适配器
ULog.addLogAdapter(ConsoleAdapter());

输出基别


  verbose
debug
info
warning
error

如何输出
ULog.v("12321321\ndfafdasfdsa\ndafdasf");
ULog.d("12321321");
ULog.i("12321321");
ULog.w("12321321");
ULog.e("1321231",error: NullThrownError());
var map = [];
map.add("1232");
map.add("1232");
map.add("1232");
map.add("1232");
ULog.e(map,error: NullThrownError());
ULog.json('''
{
"a1": "value",
"a2": 42,
"bs": [
{
"b1": "any value",
"b2": 13
},
{
"b1": "another value",
"b2": 0
}
]
}
''');

ULog.e("1321231",error: NullThrownError(),tag: "12312");
ULog.e("1232132112321321x");


优点:



  1. 可打印json字符串

  2. 打印行数很多时候会自动折行

  3. 可以打印模型

  4. 颜色区分

  5. 可扩展性强


打印效果:
打印分级
在这里插入图片描述
json打印
在这里插入图片描述


折行打印
在这里插入图片描述



收起阅读 »

Android 10 启动分析之Init篇 (一)

按下电源键时,android做了啥?当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好...
继续阅读 »

按下电源键时,android做了啥?

当我们按下电源键时,手机开始上电,并从地址0x00000000处开始执行,而这个地址通常是Bootloader程序的首地址。

bootloader是一段裸机程序,是直接与硬件打交道的,其最终目的是“初始化并检测硬件设备,准备好软件环境,最后调用操作系统内核”。除此之外,bootloader还有保护功能,部分品牌的手机对bootloader做了加锁操作,防止boot分区和recovery分区被写入。

或许有人会问了,什么是boot分区,什么又是recovery分区?

我们先来认识一下Android系统的常见分区:

/boot

这个分区上有Android的引导程序,包括内核和内存操作程序。没有这个分区设备就不能被引导。恢复系统的时候会擦除这个分区,并且必须重新安装引导程序和ROM才能重启系统。

/recovery

recovery分区被认为是另一个启动分区,你可以启动设备进入recovery控制台去执行高级的系统恢复和管理操作。

/data

这个分区保存着用户数据。通讯录、短信、设置和你安装的apps都在这个分区上。擦除这个分区相当于恢复出厂设置,当你第一次启动设备的时候或者在安装了官方或者客户的ROM之后系统会自动重建这个分区。当你执行恢复出厂设置时,就是在擦除这个分区。

/cache

这个分区是Android系统存储频繁访问的数据和app的地方。擦除这个分区不影响你的个人数据,当你继续使用设备时,被擦除的数据就会自动被创建。

/apex

Android Q新增特性,将系统功能模块化,允许系统按模块来独立升级。此分区用于存放apex 相关的内容。

为什么需要bootloader去拉起linux内核,而不把bootloader这些功能直接内置在linux内核中呢?这个问题不在此做出回答,留给大家自行去思考。

bootloader完成初始化工作后,会载入 /boot 目录下面的 kernel,此时控制权转交给操作系统。操作系统将要完成的存储管理、设备管理、文件管理、进程管理、加载驱动等任务的初始化工作,以便进入用户态。

内核启动完成后,将会寻找init文件(init文件位于/system/bin/init),启动init进程,也就是android的第一个进程。

我们来关注一下内核的common/init/main.c中的kernel_init方法。

static int __ref kernel_init(void *unused)
{
...

if (execute_command) {
ret = run_init_process(execute_command);
if (!ret)
return 0;
}
if (CONFIG_DEFAULT_INIT[0] != '\0') {
ret = run_init_process(CONFIG_DEFAULT_INIT);
if (ret)
pr_err("Default init %s failed (error %d)\n",CONFIG_DEFAULT_INIT, ret);
else
return 0;
}

if (!try_to_run_init_process("/sbin/init") ||!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||!try_to_run_init_process("/bin/sh"))
return 0;
}

可以看到,在init_kernel的最后,会调用run_init_process方法来启动init进程。

static int run_init_process(const char *init_filename){
const char *const *p;
argv_init[0] = init_filename;
return kernel_execve(init_filename, argv_init, envp_init);
}

kernel_execve是内核空间调用用户空间的应用程序的函数。

接下来我们来重点分析init进程。

init进程解析

我们从system/core/init/main.cpp 这个文件开始看起。

int main(int argc, char** argv) {
#if __has_feature(address_sanitizer)
__asan_set_error_report_callback(AsanReportCallback);
#endif

if (!strcmp(basename(argv[0]), "ueventd")) {
return ueventd_main(argc, argv);
}

if (argc > 1) {
if (!strcmp(argv[1], "subcontext")) {
android::base::InitLogging(argv, &android::base::KernelLogger);
const BuiltinFunctionMap function_map;

return SubcontextMain(argc, argv, &function_map);
}

if (!strcmp(argv[1], "selinux_setup")) {
return SetupSelinux(argv);
}

if (!strcmp(argv[1], "second_stage")) {
return SecondStageMain(argc, argv);
}
}

return FirstStageMain(argc, argv);
}

第一个参数argc表示参数个数,第二个参数是参数列表,也就是具体的参数。

main函数有四个参数入口:

  • 一是参数中有ueventd,进入ueventd_main

  • 二是参数中有subcontext,进入InitLogging 和SubcontextMain

  • 三是参数中有selinux_setup,进入SetupSelinux

  • 四是参数中有second_stage,进入SecondStageMain

main的执行顺序如下:

  1.  FirstStageMain  启动第一阶段

  2. SetupSelinux    加载selinux规则,并设置selinux日志,完成SELinux相关工作

  3. SecondStageMain  启动第二阶段

  4.  ueventd_main    init进程创建子进程ueventd,并将创建设备节点文件的工作托付给ueventd。

FirstStageMain

我们来从FirstStageMain的源码看起,源码位于/system/core/init/first_stage_init.cpp

int FirstStageMain(int argc, char** argv) {

boot_clock::time_point start_time = boot_clock::now();

#define CHECKCALL(x) \
if (x != 0) errors.emplace_back(#x " failed", errno);

// Clear the umask.
umask(0);

//初始化系统环境变量
CHECKCALL(clearenv());
CHECKCALL(setenv("PATH", _PATH_DEFPATH, 1));
// 挂载及创建基本的文件系统,并设置合适的访问权限
CHECKCALL(mount("tmpfs", "/dev", "tmpfs", MS_NOSUID, "mode=0755"));
CHECKCALL(mkdir("/dev/pts", 0755));
CHECKCALL(mkdir("/dev/socket", 0755));
CHECKCALL(mount("devpts", "/dev/pts", "devpts", 0, NULL));
#define MAKE_STR(x) __STRING(x)
CHECKCALL(mount("proc", "/proc", "proc", 0, "hidepid=2,gid=" MAKE_STR(AID_READPROC)));
#undef MAKE_STR
// 不要将原始命令行公开给非特权进程
CHECKCALL(chmod("/proc/cmdline", 0440));
gid_t groups[] = {AID_READPROC};
CHECKCALL(setgroups(arraysize(groups), groups));
CHECKCALL(mount("sysfs", "/sys", "sysfs", 0, NULL));
CHECKCALL(mount("selinuxfs", "/sys/fs/selinux", "selinuxfs", 0, NULL));

CHECKCALL(mknod("/dev/kmsg", S_IFCHR | 0600, makedev(1, 11)));

if constexpr (WORLD_WRITABLE_KMSG) {
CHECKCALL(mknod("/dev/kmsg_debug", S_IFCHR | 0622, makedev(1, 11)));
}

//创建linux随机伪设备文件
CHECKCALL(mknod("/dev/random", S_IFCHR | 0666, makedev(1, 8)));
CHECKCALL(mknod("/dev/urandom", S_IFCHR | 0666, makedev(1, 9)));

//log wrapper所必须的,需要在ueventd运行之前被调用
CHECKCALL(mknod("/dev/ptmx", S_IFCHR | 0666, makedev(5, 2)));
CHECKCALL(mknod("/dev/null", S_IFCHR | 0666, makedev(1, 3)));

...

//将内核的stdin/stdout/stderr 全都重定向/dev/null,关闭默认控制台输出
SetStdioToDevNull(argv);
// tmpfs已经挂载到/dev上,同时我们也挂载了/dev/kmsg,我们能够与外界开始沟通了
//初始化内核log
InitKernelLogging(argv);

//检测上面的操作是否发生了错误
if (!errors.empty()) {
for (const auto& [error_string, error_errno] : errors) {
LOG(ERROR) << error_string << " " << strerror(error_errno);
}
LOG(FATAL) << "Init encountered errors starting first stage, aborting";
}

LOG(INFO) << "init first stage started!";

auto old_root_dir = std::unique_ptr<DIR, decltype(&closedir)>{opendir("/"), closedir};
if (!old_root_dir) {
PLOG(ERROR) << "Could not opendir("/"), not freeing ramdisk";
}

struct stat old_root_info;

...

//挂载 system、cache、data 等系统分区
if (!DoFirstStageMount()) {
LOG(FATAL) << "Failed to mount required partitions early ...";
}

...

//进入下一步,SetupSelinux
const char* path = "/system/bin/init";
const char* args[] = {path, "selinux_setup", nullptr};
execv(path, const_cast<char**>(args));

return 1;
}

我们来总结一下,FirstStageMain到底做了哪些重要的事情:

  1. 挂载及创建基本的文件系统,并设置合适的访问权限

  2. 关闭默认控制台输出,并初始化内核级log。

  3. 挂载 system、cache、data 等系统分区

SetupSelinux

这个模块主要的工作是设置SELinux安全策略,本章内容主要聚焦于android的启动流程,selinux的内容在此不做展开。

int SetupSelinux(char** argv) {

...

const char* path = "/system/bin/init";
const char* args[] = {path, "second_stage", nullptr};
execv(path, const_cast<char**>(args));

return 1;
}

SetupSelinux的最后,进入了init的第二阶段SecondStageMain。

SecondStageMain

不多说,先上代码。

int SecondStageMain(int argc, char** argv) {
// 禁止OOM killer 杀死该进程以及它的子进程
if (auto result = WriteFile("/proc/1/oom_score_adj", "-1000"); !result) {
LOG(ERROR) << "Unable to write -1000 to /proc/1/oom_score_adj: " << result.error();
}

// 启用全局Seccomp,Seccomp是什么请自行查阅资料
GlobalSeccomp();

// 设置所有进程都能访问的会话密钥
keyctl_get_keyring_ID(KEY_SPEC_SESSION_KEYRING, 1);

// 创建 /dev/.booting 文件,就是个标记,表示booting进行中
close(open("/dev/.booting", O_WRONLY | O_CREAT | O_CLOEXEC, 0000));

//初始化属性服务,并从指定文件读取属性
property_init();

...

// 进行SELinux第二阶段并恢复一些文件安全上下文
SelinuxSetupKernelLogging();
SelabelInitialize();
SelinuxRestoreContext();

//初始化Epoll,android这里对epoll做了一层封装
Epoll epoll;
if (auto result = epoll.Open(); !result) {
PLOG(FATAL) << result.error();
}

//epoll 中注册signalfd,主要是为了创建handler处理子进程终止信号
InstallSignalFdHandler(&epoll);

...

//epoll 中注册property_set_fd,设置其他系统属性并开启系统属性服务
StartPropertyService(&epoll);
MountHandler mount_handler(&epoll);

...

ActionManager& am = ActionManager::GetInstance();
ServiceList& sm = ServiceList::GetInstance();
//解析init.rc等文件,建立rc文件的action 、service,启动其他进程,十分关键的一步
LoadBootScripts(am, sm);

...

am.QueueBuiltinAction(SetupCgroupsAction, "SetupCgroups");

//执行rc文件中触发器为 on early-init 的语句
am.QueueEventTrigger("early-init");

// 等冷插拔设备初始化完成
am.QueueBuiltinAction(wait_for_coldboot_done_action, "wait_for_coldboot_done");

am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");
am.QueueBuiltinAction(SetMmapRndBitsAction, "SetMmapRndBits");
am.QueueBuiltinAction(SetKptrRestrictAction, "SetKptrRestrict");

// 设备组合键的初始化操作
Keychords keychords;
am.QueueBuiltinAction(
[&epoll, &keychords](const BuiltinArguments& args) -> Result<Success> {
for (const auto& svc : ServiceList::GetInstance()) {
keychords.Register(svc->keycodes());
}
keychords.Start(&epoll, HandleKeychord);
return Success();
},
"KeychordInit");
am.QueueBuiltinAction(console_init_action, "console_init");

// 执行rc文件中触发器为on init的语句
am.QueueEventTrigger("init");

// Starting the BoringSSL self test, for NIAP certification compliance.
am.QueueBuiltinAction(StartBoringSslSelfTest, "StartBoringSslSelfTest");

// Repeat mix_hwrng_into_linux_rng in case /dev/hw_random or /dev/random
// wasn't ready immediately after wait_for_coldboot_done
am.QueueBuiltinAction(MixHwrngIntoLinuxRngAction, "MixHwrngIntoLinuxRng");


am.QueueBuiltinAction(InitBinder, "InitBinder");

// 当设备处于充电模式时,不需要mount文件系统或者启动系统服务,充电模式下,将charger设为执行队列,否则把late-init设为执行队列
std::string bootmode = GetProperty("ro.bootmode", "");
if (bootmode == "charger") {
am.QueueEventTrigger("charger");
} else {
am.QueueEventTrigger("late-init");
}

// 基于属性当前状态 运行所有的属性触发器.
am.QueueBuiltinAction(queue_property_triggers_action, "queue_property_triggers");

while (true) {
//开始进入死循环状态
auto epoll_timeout = std::optional<std::chrono::milliseconds>{};

//执行关机重启流程
if (do_shutdown && !shutting_down) {
do_shutdown = false;
if (HandlePowerctlMessage(shutdown_command)) {
shutting_down = true;
}
}

if (!(waiting_for_prop || Service::is_exec_service_running())) {
am.ExecuteOneCommand();
}
if (!(waiting_for_prop || Service::is_exec_service_running())) {
if (!shutting_down) {
auto next_process_action_time = HandleProcessActions();

// If there's a process that needs restarting, wake up in time for that.
if (next_process_action_time) {
epoll_timeout = std::chrono::ceil<std::chrono::milliseconds>(
*next_process_action_time - boot_clock::now());
if (*epoll_timeout < 0ms) epoll_timeout = 0ms;
}
}

// If there's more work to do, wake up again immediately.
if (am.HasMoreCommands()) epoll_timeout = 0ms;
}

// 循环等待事件发生
if (auto result = epoll.Wait(epoll_timeout); !result) {
LOG(ERROR) << result.error();
}
}

return 0;
}

总结一下,第二阶段做了以下这些比较重要的事情:

  1. 初始化属性服务,并从指定文件读取属性
  2. 初始化epoll,并注册signalfd和property_set_fd,建立和init的子进程以及部分服务的通讯桥梁
  3. 初始化设备组合键,使系统能够对组合键信号做出响应
  4. 解析init.rc文件,并按rc里的定义去启动服务
  5. 开启死循环,用于接收epoll的事件

在第二阶段,我们需要重点关注以下问题:

init进程是如何通过init.rc配置文件去启动其他的进程的呢?

init.rc 解析

我们从 LoadBootScripts(am, sm)这个方法开始看起,一步一部来挖掘init.rc 的解析流程。

static void LoadBootScripts(ActionManager& action_manager, ServiceList& service_list) {
//初始化ServiceParse、ActionParser、ImportParser三个解析器
Parser parser = CreateParser(action_manager, service_list);

std::string bootscript = GetProperty("ro.boot.init_rc", "");
if (bootscript.empty()) {
//bootscript为空,进入此分支
parser.ParseConfig("/init.rc");
if (!parser.ParseConfig("/system/etc/init")) {
late_import_paths.emplace_back("/system/etc/init");
}
if (!parser.ParseConfig("/product/etc/init")) {
late_import_paths.emplace_back("/product/etc/init");
}
if (!parser.ParseConfig("/product_services/etc/init")) {
late_import_paths.emplace_back("/product_services/etc/init");
}
if (!parser.ParseConfig("/odm/etc/init")) {
late_import_paths.emplace_back("/odm/etc/init");
}
if (!parser.ParseConfig("/vendor/etc/init")) {
late_import_paths.emplace_back("/vendor/etc/init");
}
} else {
parser.ParseConfig(bootscript);
}
}

我们可以看到这句话,Parse开始解析init.rc文件,在深入下去之前,让我们先来认识一下init.rc。

 parser.ParseConfig("/init.rc")

init.rc是一个可配置的初始化文件,负责系统的初步建立。它的源文件的路径为 /system/core/rootdir/init.rc

init.rc文件有着固定的语法,由于内容过多,限制于篇幅的原因,在此另外单独开了一篇文章进行讲解:

Android 10 启动分析之init语法

了解了init.rc的语法后,我们来看看init.rc文件里的内容。

import /init.environ.rc  //导入全局环境变量
import /init.usb.rc //adb 服务、USB相关内容的定义
import /init.${ro.hardware}.rc //硬件相关的初始化,一般是厂商定制
import /vendor/etc/init/hw/init.${ro.hardware}.rc
import /init.usb.configfs.rc
import /init.${ro.zygote}.rc //定义Zygote服务

我们可以看到,在/system/core/init目录下,存在以下四个zygote相关的文件

image.png

怎样才能知道我们当前的手机用的是哪个配置文件呢?

答案是通过adb shell getprop | findstr ro.zygote命令,看看${ro.zygote}这个环境变量具体的值是什么,笔者所使用的华为手机的ro.zygote值如下所示:

image.png

什么是Zygote,Zygote的启动过程是怎样的,它的启动配置文件里又做了啥,在这里我们不再做进一步探讨, 只需要知道init在一开始在这个文件中对Zygote服务做了定义,而上述的这些问题将留到 启动分析之Zygote篇 再去说明。

on early-init
# Disable sysrq from keyboard
write /proc/sys/kernel/sysrq 0

# Set the security context of /adb_keys if present.
restorecon /adb_keys

# Set the security context of /postinstall if present.
restorecon /postinstall

mkdir /acct/uid

# memory.pressure_level used by lmkd
chown root system /dev/memcg/memory.pressure_level
chmod 0040 /dev/memcg/memory.pressure_level
# app mem cgroups, used by activity manager, lmkd and zygote
mkdir /dev/memcg/apps/ 0755 system system
# cgroup for system_server and surfaceflinger
mkdir /dev/memcg/system 0550 system system

start ueventd

# Run apexd-bootstrap so that APEXes that provide critical libraries
# become available. Note that this is executed as exec_start to ensure that
# the libraries are available to the processes started after this statement.
exec_start apexd-bootstrap

紧接着是一个Action,Action的Trigger 为early-init,在这个 Action中,我们需要关注最后两行,它启动了ueventd服务和apex相关服务。还记得什么是ueventd和apex吗?不记得的读者请往上翻越再自行回顾一下。

ueventd服务的定义也可以在init.rc文件的结尾找到,具体代码及含义如下:

service ueventd    //ueventd服务的可执行文件的路径为 /system/bin/ueventd
class core //ueventd 归属于 core class,同样归属于core class的还有adbd 、console等服务
critical //表明这个Service对设备至关重要,如果Service在四分钟内退出超过4次,则设备将重启进入恢复模式。
seclabel u:r:ueventd:s0 //selinux相关的配置
shutdown critical //ueventd服务关闭行为

然而,early-init 这个Trigger到底什么时候触发呢?

答案是通过init.cpp代码调用触发。

我们可以在init.cpp 代码中找到如下代码片段:

am.QueueEventTrigger("early-init");

QueueEventTrigger这个方法的实现机制我们稍后再进行探讨,目前我们只需要了解, ActionManager 这个类中的 QueueEventTrigger方法,负责触发init.rc中的Action。

我们继续往下看init.rc的内容。

on init

...

# Start logd before any other services run to ensure we capture all of their logs.
start logd

# Start essential services.
start servicemanager
...

在Trigger 为init的Action中,我们只需要关注以上的关键内容。在init的action中启动了一些核心的系统服务,这些服务具体的含义为 :

服务名含义
logdAndroid L加入的服务,用于保存Android运行期间的日志
servicemanagerandroid系统服务管理者,负责查询和注册服务

接下来是late-init Action:

on late-init
//启动vold服务(管理和控制Android平台外部存储设备,包括SD插拨、挂载、卸载、格式化等)
trigger early-fs
trigger fs
trigger post-fs
trigger late-fs

//挂载/data , 启动 apexd 服务
trigger post-fs-data

# 读取持久化属性或者从/data 中读取并覆盖属性
trigger load_persist_props_action

//启动zygote服务!!在启动zygote服务前会先启动netd服务(专门负责网络管理和控制的后台守护进程)
trigger zygote-start

//移除/dev/.booting 文件
trigger firmware_mounts_complete

trigger early-boot
trigger boot //初始化网络环境,设置系统环境和守护进程的权限

最后,我们用流程图来总结一下上述的启动过程:

first_stage
second_stage
设置SetupSelinux安全策略
挂载system、cache、data等系统分区
初始化内核log
初始化基本的文件系统
初始化系统环境变量
开启死循环,用于接收epoll的事件
启动zygote服务
启动netd服务
挂载/data,启动apexd服务
启动vold服务
启动servicemanager服务
启动logd服务
启动ueventd服务
解析init.rc文件
初始化设备组合键
初始化epoll
初始化属性服务
Click Power Button
bootloader
linux kernel
init进程
收起阅读 »

PermissionX 1.5发布,支持申请Android特殊权限啦

前言 Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。 不过之...
继续阅读 »

前言


Hello 大家早上好,说起 PermissionX,其实我已经有段时间没有更新这个框架了。一是因为现在工作确实比较忙,没有过去那么多的闲暇时间来写开源项目,二是因为,PermissionX 的主体功能已经相当稳定,并不需要频繁对其进行变更。


不过之前一直有朋友在反映,对于 Android 中的一些特殊权限申请,PermissionX 并不支持。是的,PermissionX 本质上只是对 Android 运行时权限 API 进行了一层封装,用于简化运行时权限申请的。而这些特殊权限并不属于 Android 运行时权限的一部分,所以 PermissionX 自然也是不支持的。


但是特殊权限却是我们这些开发者们可能经常要与之打交道的一部分,它们并不难写,但是每次去写都感觉很繁琐。因此经慎重考虑之后,我决定将几个比较常用的特殊权限纳入 PermissionX 的支持范围。那么本篇文章我们就来看一看,对于这几个常见的特殊权限,使用 PermissionX 和不使用 PermissionX 的写法有什么不同之处。


事实上,Android 的权限机制也是经历过长久的迭代的。在 6.0 系统之前,Google 将权限机制设计的比较简单,你的应用程序需要用到什么权限,只需要在 AndroidManifest.xml 文件中声明一下就可以了。


但是从 6.0 系统开始,Android 引入了运行时权限机制。Android 将常用的权限大致归成了几类,一类是普通权限,一类是危险权限,一类是特殊权限。


普通权限指的是那些不会直接威胁到用户的安全和隐私的权限,这种权限和过去一样,只需要在 AndroidManifest.xml 文件中声明一下就可以了,不需要做任何特殊处理。


危险权限则表示那些可能会触及用户隐私或者对设备安全性造成影响的权限,如获取设备联系人信息、定位设备的地理位置等。这部分权限需要通过代码进行申请,并要用户手动同意才可获得授权。PermissionX 库主要就是处理的这种权限的申请。


而特殊权限则更加少见,Google 认为这种权限比危险权限还要敏感,因此不能仅仅让用户手动同意就可以获得授权,而是需要让用户到专门的设置页面去手动对某一个应用程序授权,该程序才能使用这个权限。


不过相比于危险权限,特殊权限没有非常固定的申请方式,每个特殊权限可能都要使用不同的写法才行,这也导致申请特殊权限比申请危险权限还要繁琐。


从 1.5.0 版本开始,PermissionX 对最常用的几个特殊权限进行了支持。正如刚才所说,特殊权限没有固定的申请方式,因此 PermissionX 也是针对于这几个特殊权限一个一个去适配并支持的。如果你发现你需要申请的某个特殊权限还没有被 PermissionX 支持,也可以向我提出需求,我会考虑在接下来的版本中加入。


在过去,我们发布开源库通常都是发布到 jcenter 上的,但是相信大家现在都已经知道了,jcenter 即将停止服务,具体可以参考我的这篇文章 浅谈 JCenter 即将被停止服务的事件


目前的 jcenter 处在一个半废弃的边缘,虽然还可以正常从 jcenter 下载开源库,但是已经不能再向 jcenter 发布新的开源库了。而在明年 2 月 1 号之后,下载服务也会被关停。


所以,以后要想再发布开源库我们只能选择发布到其他仓库,比如现在 Google 推荐我们使用 Maven Central。


于是,从 1.5.0 版本开始,PermissionX 也会将库发布到 Maven Center 上,之前的老版本由于迁移价值并不大,所以我也不想再耗费经历做迁移了。1.5.0 之前的版本仍然保留在 jcenter 上,提供下载服务直到明年的 2 月 1 号。


而关于如何将库发布到 Maven Central,请参考 再见 JCenter,将你的开源库发布到 MavenCentral 上吧


Android的特殊权限


Android 里具体有哪些特殊权限呢?


说实话,这个我也不太清楚。我所了解的特殊权限基本都是因为需要用到了,然后发现这个权限即不属于普通权限,也不属于危险权限,要用一种更加特殊的方式去申请,才知道原来这是一个特殊权限。


因此,PermissionX 1.5.0 版本中对特殊权限的支持,也就仅限于我知道的,以及从网友反馈得来的几个最为常用的特殊权限。


一共是以下 3 个:



  1. 悬浮窗

  2. 修改设置

  3. 管理外部存储


接下来我就分别针对这 3 个特殊权限做一下更加详细的介绍。


悬浮窗


悬浮窗功能在不少应用程序中使用得非常频繁,因为你可能总有一些内容是要置顶于其他内容之上显示的,这个时候用悬浮窗来实现就会非常方便。


当然,如果你只是在自己的应用内部实现悬浮窗功能是不需要申请权限的,但如果你的悬浮窗希望也能置顶于其他应用程序的上方,这就必须得要申请权限了。


悬浮窗的权限名叫做 SYSTEM_ALERT_WINDOW,如果你去查一下这个权限的文档,会发现这个权限的申请方式比较特殊:



按照文档上的说法,从 Android 6.0 系统开始,我们在使用 SYSTEM_ALERT_WINDOW 权限前需要发出一个 action 为 Settings.ACTION_MANAGE_OVERLAY_PERMISSION 的 Intent,引导用户手动授权。另外我们还可以通过 Settings.canDrawOverlays() 这个 API 来判断用户是否已经授权。


因此,想要申请悬浮窗权限,自然而然就可以写出以下代码:


if (Build.VERSION.SDK_INT >= 23) {
if (Settings.canDrawOverlays(context)) {
showFloatView()
} else {
val intent = Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION)
startActivity(intent)
}
} else {
showFloatView()
}


看上去也不复杂嘛。


确实,但是它麻烦的点主要在于,它的请求方式是脱离于一般运行时权限的请求方式的,因此得要为它额外编写独立的权限请求逻辑才行。


而 PermissionX 的目标就是要弱化这种独立的权限请求逻辑,减少差异化代码编写,争取使用同一套 API 来实现对特殊权限的请求。


如果你已经比较熟悉 PermissionX 的用法了,那么以下代码你一定不会陌生:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW)
.onExplainRequestReason { scope, deniedList ->
val message = "PermissionX需要您同意以下权限才能正常使用"
scope.showRequestReasonDialog(deniedList, message, "Allow", "Deny")
}
.request { allGranted, grantedList, deniedList ->
if (allGranted) {
Toast.makeText(activity, "所有申请的权限都已通过", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(activity, "您拒绝了如下权限:$deniedList", Toast.LENGTH_SHORT).show()
}
}


可以看到,这就是最标准的 PermissionX 的正常用法,但是我们在这里却用来请求了悬浮窗权限。也就是说,即使是特殊权限,在 PermissionX 中也可以用普通的方式去处理。


另外不要忘记,所有申请的权限都必须在 AndroidManifest.xml 进行注册才行:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


那么运行效果是什么样的呢?我们来看看吧:



可以看到,PermissionX 还自带了一个权限提示框,友好地告知用户我们需要悬浮窗权限,引导用户去手动开启。


修改设置


了解了悬浮窗权限的请求方式之后,接下来我们就可以快速过一下修改设置权限的请求方式了,因为它们的用法是完全一样的。


修改设置的权限名叫 WRITE_SETTINGS,如果我们去查看一下它的文档,你会发现它和刚才悬浮窗权限的文档简直如出一辙:



同样是从 Android 6.0 系统开始,在使用 WRITE_SETTINGS 权限前需要先发出一个 action 为 Settings.ACTION_MANAGE_WRITE_SETTINGS 的 Intent,引导用户手动授权。然后我们还可以通过 Settings.System.canWrite() 这个 API 来判断用户是否已经授权。


所以,如果是自己手动申请这个权限,相信你已经知道要怎么写了。


那么用 PermissionX 申请的话应该要怎么写呢?这个当然就更简单了,只需要把要申请的权限替换一下即可,其他部分都不用作修改:


PermissionX.init(activity)
.permissions(Manifest.permission.WRITE_SETTINGS)
...


当然,不要忘记在 AndroidManifest.xml 中注册权限:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


运行一下,效果如下图所示:



管理外部存储


管理外部存储权限也是一种特殊权限,它可以允许你的 App 拥有对整个 SD 卡进行读写的权限。


有些朋友可能会问,SD 卡本来不就是可以全局读写的吗?为什么还要再申请这个权限?


那你一定是没有了解 Android 11 上的 Scoped Storage 功能。从 Android 11 开始,Android 系统强制启用了 Scoped Storage,所有 App 都不再拥有对 SD 卡进行全局读写的权限了。


关于 Scoped Storage 的更多内容,可以参考我的这篇文章 Android 11 新特性,Scoped Storage 又有了新花样


但是如果有的应用就是要对 SD 卡进行全局读写该怎么办呢(比如说文件浏览器)?


不用担心,Google 仍然还是给了我们一种解决方案,那就是请求管理外部存储权限。


这个权限是 Android 11 中新增的,为的就是应对这种特殊场景。


那么这个权限要怎么申请呢?我们还是先来看一看文档:



大致可以分为几步吧:


第一,在 AndroidManifest.xml 中声明 MANAGE_EXTERNAL_STORAGE 权限。


第二,发出一个 action 为 Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION 的 Intent,引导用户手动授权。


第三,调用 Environment.isExternalStorageManager() 来判断用户是否已授权。


传统请求权限的写法我就不再演示了,使用 PermissionX 来请求的写法仍然也还是差不多的。只不过要注意,因为 MANAGE_EXTERNAL_STORAGE 权限是 Android 11 系统新加入的,所以我们也只应该在 Android 11 以上系统去请求这个权限,代码如下所示:


if (Build.VERSION.SDK_INT >= 30) {
PermissionX.init(this)
.permissions(Manifest.permission.MANAGE_EXTERNAL_STORAGE)
...
}


AndroidManifest.xml 中的权限如下:


<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.permissionx.app">

<uses-permission android: />

</manifest>


运行一下程序,效果如下图所示:



这样我们就拥有全局读写 SD 卡的权限了。


另外 PermissionX 还有一个特别方便的地方,就是它可以一次性申请多个权限。假如我们想要同时申请悬浮窗权限和修改设置权限,只需要这样写就可以了:


PermissionX.init(activity)
.permissions(Manifest.permission.SYSTEM_ALERT_WINDOW, Manifest.permission.WRITE_SETTINGS)
...


运行效果如下图所示:



当然你也可以将特殊权限与普通运行时权限放在一起申请,PermissionX 对此也是支持的。只有当所有权限都请求结束时,PermissionX 才会将所有权限的请求结果一次性回调给开发者。


关于 PermissionX 新版本的内容变化就介绍到这里,升级的方式非常简单,修改一下 dependencies 当中的版本号即可:


repositories {
google()
mavenCentral()
}


dependencies {
implementation 'com.guolindev.permissionx:permissionx:1.5.0'
}


注意现在一定要使用 mavenCentral 仓库,而不能再使用 jcenter 了。



如果你对 PermissionX 的源码感兴趣,可以访问 PermissionX 的项目主页:


github.com/guolindev/P…

收起阅读 »

Android 图片转场和轮播特效,你想要的都在这了

使用 OpenGL 做图像的转场效果或者图片轮播器,可以实现很多令人惊艳的效果。 GLTransitions 熟悉的 OpenGL 开发的朋友已经非常了解 GLTransitions 项目,该项目主要用来收集各种 GL 转场特效及其 GLSL 实现...
继续阅读 »

使用 OpenGL 做图像的转场效果或者图片轮播器,可以实现很多令人惊艳的效果。


ogl.gif


GLTransitions


gallery.gif


熟悉的 OpenGL 开发的朋友已经非常了解 GLTransitions 项目,该项目主要用来收集各种 GL 转场特效及其 GLSL 实现代码,开发者可以很方便地移植到自己的项目中。


GLTransitions 项目网站地址: gl-transitions.com/gallery


config.gif


GLTransitions 项目已经有接近 100 种转场特效,能够非常方便地运用在视频处理中,**很多转场特效包含了混合、边缘检测、腐蚀膨胀等常见的图像处理方法,由易到难。 **


对于想学习 GLSL 的同学,既能快速上手,又能学习到一些高阶图像处理方法 GLSL 实现,强烈推荐。


edit.png


另外 GLTransitions 也支持 GLSL 脚本在线编辑、实时运行,非常方便学习和实践。


Android OpenGL 怎样移植转场特效


github.gif github2.gif github3.gif


由于 GLSL 脚本基本上是通用的,所以 GLTransitions 特效可以很方便地移植到各个平台,本文以 GLTransitions 的 HelloWorld 项目来介绍下特效移植需要注意的几个点。


GLTransitions 的 HelloWorld 项目是一个混合渐变的特效:


// transition of a simple fade.
vec4 transition (vec2 uv) {
return mix(
getFromColor(uv),
getToColor(uv),
progress
);
}


transition 是转场函数,功能类似于纹理采样函数,根据纹理坐标 uv 输出 rgba ,getFromColor(uv) 表示对源纹理进行采样,getToColor(uv) 表示对目标纹理进行采样,输出 rgba ,progress 是一个 0.0~1.0 数值之间的渐变量,mix 是 glsl 内置混合函数,根据第三个参数混合 2 个颜色。


根据以上信息,我们在 shader 中只需要准备 2 个纹理,一个取值在 0.0~1.0 的(uniform)渐变量,对应的 shader 脚本可以写成:


#version 300 es
precision mediump float;
in vec2 v_texCoord;
layout(location = 0) out vec4 outColor;
uniform sampler2D u_texture0;
uniform sampler2D u_texture1;
uniform float u_offset;//一个取值在 0.0~1.0 的(uniform)渐变量

vec4 transition(vec2 uv) {
return mix(
texture(u_texture0, uv);,
texture(u_texture1, uv);,
u_offset
);
}

void main()
{
outColor = transition(v_texCoord);
}


代码中设置纹理和变量:


glUseProgram (m_ProgramObj);

glBindVertexArray(m_VaoId);

glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[0]);
GLUtils::setInt(m_ProgramObj, "u_texture0", 0);

glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, m_TextureIds[1]);
GLUtils::setInt(m_ProgramObj, "u_texture1", 1);

GLUtils::setFloat(m_ProgramObj, "u_offset", offset);

glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, (const void *)0);

本文的 demo 实现的是一个图像轮播翻页效果,Android 实现代码见项目:


github.com/githubhaoha…


转场特效移植是不是很简单,动手试试吧。

收起阅读 »

ConstraintLayout 中的 Barrier 和 Chains

1. Barrier 是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier。 具体看图 “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二...
继续阅读 »



1. Barrier


是一个准则,可以说是对其的规则,这样说还不够名义,我们可以列表一些比较常见的场景; 官网 Barrier



  1. 具体看图


1883633-62653bd01cb70813.webp “第二行的label”和“第二行value”是一个整体,他们距离上面是 100dp ,但是有可能“第二行value”的值为空或者是空,也需要“第二行label”距离上面的距离是 100dp ,由于我们知道“第二行value”的高度高于第一个,所以采用的是“第二行label”跟“第二行value”对其,“第二行value”距离上边 100dp 的距离,但是由于“第二行value”有可能为空,所以当“第二行value”为空的时候就会出现下面的效果:


1883633-043d00e43ff22557.webp 我们发现达不到预期,现在能想到的办法有,首先在代码控制的时候随便把“第二行label”的 marginTop 也添加进去;还有就是换布局,将“第二行label”和“第二行value”放到一个布局中,比如 LinearLayout ,这样上边的 marginTopLinearLayout 控制;这样的话即便“第二行value”消失了也会保持上边的效果。


除了上边的方法还能使用其他的嘛,比如我们不使用代码控制,我们不使用其他的布局,因为我们知道布局嵌套太多性能也会相应的下降,所以在编写的时候能减少嵌套的情况下尽可能的减少,当然也不能为了减少嵌套让代码变得格外的复杂。


为了满足上面的需求, Barrier 出现了,它能做到隐藏的也能依靠它,并且与它的距离保持不变对于隐藏的“第二行value”来说,虽然消失了,但保留了 marginTop 的数值。下面看看布局:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">


<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="头部"
android:textSize="36sp"
app:layout_constraintBottom_toTopOf="@id/barrier3"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />



<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />


<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行label"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3"
app:layout_constraintVertical_bias="0.538" />


<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_marginStart="12dp"
android:layout_marginTop="100dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/textView2"
app:layout_constraintTop_toBottomOf="@+id/barrier3" />


</androidx.constraintlayout.widget.ConstraintLayout>

这样即便将“第二行value”消失,那么总体的布局仍然达到预期,并且也没有添加很多布局内容。在代码中:


<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="top"
android:layout_marginTop="100dp"
app:constraint_referenced_ids="textView2,textView3" />


这里主要有两个属性 app:barrierDirectionapp:constraint_referenced_ids



  • app:barrierDirection 是代表位置,也就是在包含内容的哪一个位置,我这里写的是 top ,是在顶部,还有其他的属性 top,bottom,left,right,startend 这几个属性,看意思就很明白了。

  • app:constraint_referenced_ids 上面说的内容就是包含在这里面的,这里面填写的是 id 的名称,如果有多个,那么使用逗号隔开;这里面的到 Barrier 的距离不会改变,即便隐藏了也不会变。


这里可能会有疑惑,为啥我写的 idtextView4 的也依赖于 Barrier ,这是因为本身 Barrier 只是规则不是实体,它的存在只能依附于实体,不能单独存在于具体的位置,如果我们只有“第二行value”依赖于它,但是本身“第二行value”没有上依赖,也相当于没有依赖,这样只会导致“第二行label”和“第二行value”都消失,如果 textView4 依赖于 Barrier ,由于 textView4 的位置是确定的,所以 Barrier 的位置也就确定了。



  1. 类似表格的效果。看布局效果:


1883633-c4b862a2df57fb96.webp 我要做成上面的样子。也就是右边永远与左边最长的保持距离。下面是我的代码:


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">



<TextView
android:id="@+id/textView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="头部"
android:textSize="36sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />


<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第二行"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView3"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView3" />


<TextView
android:id="@+id/textView3"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第二行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView4" />



<TextView
android:id="@+id/textView5"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="4dp"
android:text="第三次测试"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="@+id/textView6"
app:layout_constraintStart_toStartOf="@+id/textView4"
app:layout_constraintTop_toTopOf="@+id/textView6" />


<TextView
android:id="@+id/textView6"
android:layout_width="wrap_content"
android:layout_height="50dp"
android:layout_marginStart="20dp"
android:layout_marginTop="10dp"
android:text="第三行value"
android:textSize="36sp"
app:layout_constraintStart_toEndOf="@+id/barrier4"
app:layout_constraintTop_toBottomOf="@+id/textView3" />


<androidx.constraintlayout.widget.Barrier
android:id="@+id/barrier4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="end"
app:constraint_referenced_ids="textView2,textView5"
tools:layout_editor_absoluteX="411dp" />



</androidx.constraintlayout.widget.ConstraintLayout>

添加好了,记得让右边的约束指向 Barrier 。这里的 Barrier ,我们看到包含 textView2textView5 ,这个时候就能做到谁长听谁的,如果此时 textView2 变长了,那么就会将就 textView2


2. Chains


我们特别喜欢使用线性布局,因为我们发现 UI 图上的效果使用线性布局都可以使用,当然也有可能跟大部分人的思维方式有关系。比如我们非常喜欢的,水平居中,每部分的空间分布等等都非常的顺手。既然线性布局这么好用,那为啥还有约束布局呢,因为线性布局很容易写出嵌套很深的布局,但约束布局不会,甚至大部分情况都可以不需要嵌套就能实现,那是不是代表线性布局有的约束布局也有,答案是肯定的。


使用普通的约束关系就很容易实现水平居中等常用效果,其他的如水平方向平均分布空间,使用一般的约束是实现不了的,于是就要使用 Chains ,这个就很容易实现下面的效果:


1883633-78aa31c23dcb4c4f.webp 其实上一篇中我已经把官网的教程贴上去了,这里主要写双向约束怎么做,一旦双向约束形成,那么就自然进入到 Chains 模式。


1)在视图模式中操作


1883633-618f9b2eb563a637.webp


如果直接操作,那么只能单向约束,如果要形成这样的约束,需要选择相关的的节点,比如我这里就是同时选择 AB ,然后点击鼠标右键,就可以看到 ChainsCreate Horizontal Chain


对应的操作


选择图中的选项即可完成从 A 指向 B ,修改的示意图为:


1883633-cf3984e22df83c7c.webp


我们发现已经实现了水平方向的排列效果了。至于怎么实现上面的效果,主要是改变 layout_constraintVertical_chainStylelayout_constraintHorizontal_chainStyle 属性。至于权重则是属性 layout_constraintHorizontal_weight


layout_constraintHorizontal_chainStyle 属性说明:



  • spread 默认选项,效果就是上面的那种,也就是平均分配剩余空间;

  • spread_inside 两边的紧挨着非 Chains 的视图,中间的平均分配;


1883633-49c52026c6797e51.webp



  • packed 所有的都在中间


1883633-714e58d28eaab99c.webp 注意了, layout_constraintHorizontal_weight 这个属性只有在 A 身上设置才可以,也就是首节点上设置才可行,同时 layout_constraintHorizontal_weight 是代表水平方向,只能在水平方向才发生作用,如果水平的设置了垂直则不生效。


layout_constraintHorizontal_weight 这个属性只有在当前视图的宽或者高是 0dp 。至于这个的取值跟线性布局相同。


1883633-072f1f968528ef1a.webp


2)代码的方式 跟上面的差别就是在做双向绑定,用代码就很容易实现双向绑定,可平时添加约束相同。

收起阅读 »

JAVA面向对象简介

文章目录概念了解举例说明:创建一个Soldier类举例说明:创建一个FlashLight类举例说明:创建一个Car类类中包含的变量类中的方法概念了解Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。面向对象是一种符合人类...
继续阅读 »

文章目录

概念了解

Java是一种面向对象的程序设计语言,了解面向对象的编程思想对于学习Java开发相当重要。

面向对象是一种符合人类思维习惯的编程思想。现实生活中存在各种形态不同的事物,这些事物之间存在着各种各样的联系。在程序中使用对象来映射现实中的事物,使用对象的关系来描述事物之间的联系,这种思想就是面向对象。

面向对象和面向过程的区别
提到面向对象,自然会想到面向过程,面向过程就是分析解决问题所需要的步骤,然后用函数把这些步骤一一实现,使用的时候一个一个依次调用就可以了。面向对象则是把解决的问题按照一定规则划分为多个独立的对象,然后通过调用对象的方法来解决问题。当然,一个应用程序会包含多个对象,通过多个对象的相互配合来实现应用程序的功能,这样当应用程序功能发生变动时,只需要修改个别的对象就可以了,从而使代码更容易得到维护。

首先了解几个概念:


类是一个模板,它描述一类对象的行为和状态。

对象(实例)
对象是类的一个实例(对象不是找个女朋友),有状态和行为。例如,一条狗是一个对象,它的状态有:颜色、名字、品种;行为有:摇尾巴、叫、吃等。
每个对象占用独立的内存空间,保存各自的属性数据。
每个对象可以独立控制,让它执行指定的方法代码。

举例说明:创建一个Soldier类

我们定义一个士兵类,他的属性有 id(表示他的唯一编号)、blood(表示他的血量)。他的行为有 go(前进)、attack(攻击)

Soldier类

public class Soldier {
//成员变量
int id;//唯一编号,默认0
int blood = 100;//血量,默认满血


//成员方法
public void go(TextView tv) {
if (blood == 0) {
tv.setText(id + "已阵亡,无法前进" + "\n" + tv.getText());
return;
}
tv.setText(id + "前进" + "\n" + tv.getText());
}

public void attack(TextView tv) {
if (blood == 0) {
tv.setText(id + "已阵亡,无法攻击" + "\n" + tv.getText());
return;
}

tv.setText(+id + "号士兵发起进攻" + "]\n" + tv.getText());

int b = new Random().nextInt(30);
if (blood < b) {
blood = b;
}
blood -= b;

if (blood == 0) {
tv.setText("["+id + "号士兵阵亡" + "\n" + tv.getText());
} else {
tv.setText("[士兵" + id + "进攻完毕,血量" + blood + "\n" + tv.getText());
}
}
}

xml

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


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="新建士兵" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="前进" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="进攻" />


<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="#222222"
android:textSize="15sp"/>

</LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {
TextView textView;
Soldier s1;//默认null

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

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
}
}

public void f1() {
s1 = new Soldier();
s1.id = 9527;
//用s1找到士兵对象内存空间
//访问它的属性变量id
textView.setText("士兵9527已创建" + "\n");
}

public void f2() {
s1.go(textView);
}

public void f3() {
s1.attack(textView);
}
}

运行程序:
在这里插入图片描述

举例说明:创建一个FlashLight类

我们来创建一个手电筒的类。它的属性有颜色、开光状态。它的方法有开灯、关灯

FlashLight.java

public class FlashLight {
//属性变量,成员变量
int color = Color.BLACK;
boolean on = false;

public void turnOn() {
on = true;
}

public void turnOff() {
on = false;
}
}

xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/layout"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:background="@android:color/black"
android:orientation="vertical">


<ToggleButton
android:id="@+id/toggleButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:textOff="关"
android:textOn="开" />


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="红" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="白" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="蓝" />


</LinearLayout>

MainActivity.java

public class MainActivity extends AppCompatActivity {
FlashLight flashLight = new FlashLight();
LinearLayout linearLayout;
ToggleButton toggleButton;

Button red;
Button white;
Button blue;

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

linearLayout = findViewById(R.id.layout);
toggleButton = findViewById(R.id.toggleButton);
red = findViewById(R.id.button1);
white = findViewById(R.id.button2);
blue = findViewById(R.id.button3);
}

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
case R.id.toggleButton:
f4();
break;
}
}

public void f1() {
//用light变量找到手电筒对象的内存空间地址
//访问它的color变量
flashLight.color = Color.RED;
show();
}

public void f2() {
flashLight.color = Color.WHITE;
show();
}

public void f3() {
flashLight.color = Color.BLUE;
show();
}

public void f4() {
//判断开关指示按钮状态
if (toggleButton.isChecked()) {
flashLight.turnOn();
} else {
flashLight.turnOff();
}
show();
}

public void show() {
//根据flashlight属性控制界面
if (flashLight.on) {
linearLayout.setBackgroundColor(flashLight.color);
} else {
linearLayout.setBackgroundColor(Color.BLACK);
}
}
}

运行程序:
在这里插入图片描述

举例说明:创建一个Car类

我们来创建一个汽车类,它的属性有颜色color、品牌brand、速度speed。它的方法有前进、停止。
Car.java

public class Car {
public String color;
public String brand;
public int speed;

public void go(TextView tv) {
tv.append("\n" + color + brand + "汽车以时速" + speed + "前进");
}

public void stop(TextView tv) {
tv.append("\n" + color + brand + "汽车停止");
}
}

xml

<?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">


<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="创建汽车" />


<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="go" />


<Button
android:id="@+id/button3"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:onClick="doClick"
android:text="stop" />


<TextView
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="18sp"
android:textColor="#222222" />


</LinearLayout>

MainActivity

public class MainActivity extends AppCompatActivity {
Car car;

Button create;
Button go;
Button stop;
TextView textView;

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

create = findViewById(R.id.button1);
go = findViewById(R.id.button2);
stop = findViewById(R.id.button3);
textView = findViewById(R.id.textView);
}

public void doClick(View view) {
switch (view.getId()) {
case R.id.button1:
f1();
break;
case R.id.button2:
f2();
break;
case R.id.button3:
f3();
break;
}
}

private void f1() {
car = new Car();
//默认值如下
//color="";
//brand="";
//speed=0;
car.color = "红色";
car.brand = "BMW";
car.speed = 80;
textView.setText("汽车已创建");
}

private void f2() {
car.go(textView);
}

private void f3() {
car.stop(textView);
}
}

运行程序:
在这里插入图片描述

类中包含的变量

一个类可以包含以下类型变量:
1、局部变量
在方法、构造方法或者语句块中定义的变量被称为局部变量。变量声明和初始化都是在方法中,方法结束后,变量就会自动销毁。
局部变量有以下特点:
①必须手动初始化(分配内存空间),没有默认值
②局部变量作用域,到它定义的代码块为止
③作用域内不能重复定义

void f1(){
int a = 10;
if(){
int a = 9;//这样是不允许的,作用域范围内重复定义了
print(a);
}
}
void f1(){
int a = 10;
if(){
print(a);
int b = 100;
}//此时b的作用范围已结束,可以再定义一个b,不是同一个
int b = 1000;
}

2、成员变量
成员变量是定义在类中,方法体之外的变量。这种变量在创建对象的时候实例化。成员变量可以被类中方法、构造方法和特定类的语句块访问。
成员变量有以下特点:
①定义在类中,自动初始化,有默认值

Int a;默认0
boolean b;默认false
int[] c;默认null

例如我们第一个例子 Soldier 类中,唯一编号 id,默认值就是 0。
②访问范围:类中都可以访问,根据访问范围设置,类外也可以访问

3、类变量
类变量也声明在类中,方法体之外,但必须声明为 static 类型。
关于关键字 static 后边的文章中会讲到


收起阅读 »

Android 渲染系列-绘制流程总览

前言谈到Android的渲染,可能会想到测量、布局、绘制三大流程。但我们的view到底是如何一步一步显示到屏幕的?App的CPU/GPU渲染到底是什么?OpenGL/Vulkan/skia是什么? surfaceFlinger和HAL又是什么呢?带着这些问题,...
继续阅读 »

前言

谈到Android的渲染,可能会想到测量、布局、绘制三大流程。但我们的view到底是如何一步一步显示到屏幕的?App的CPU/GPU渲染到底是什么?OpenGL/Vulkan/skia是什么? surfaceFlinger和HAL又是什么呢?

带着这些问题,我们今天就深入的去学习Android绘制的整个流程吧。

参考分层思想,我们大概把整个渲染分为App层和SurfaceFlinger层,先讲各层都做什么工作,后面在把二者联系起来。

相关概念

  1. Vsync信号

由系统设备产生。假设在60HZ的屏幕上,屏幕就会每16ms进行一次扫描,在两次扫描中间会有一个间隔,此时系统就会发出Vsync信号,来通知APP(Vsync-app)进行渲染,SurfaceFlinger(Vsync-sf)来进行swap缓冲区进行展示。因此,只要App的渲染过程(CPU计算+GPU绘制)不超过16ms,画面就会显得很流畅。

说明:

  • 如果系统检测到硬件支持,则Vysnc信号由硬件产生,否则就由软件模拟产生。这个了解即可。
  • Vsync offset机制: Vsync-app、Vsync-sf并不是同时通知的,Vsync-sf会相对晚些,但对于我们App开发者来说,即可认为约等于同时发生。
  1. OpenGL、Vulkan、skia的关系
  • OpenGL: 是一种跨平台的3D图形绘制规范接口。OpenGL EL则是专门针对嵌入式设备,如手机做了优化。
  • Vulkan: 跟OpenGL相同功能,不过它同时支持3D、2D,比OpenGL更加的轻量、性能更高。
  • skia: skia是图像渲染库,2D图形绘制自己就能完成。3D效果(依赖硬件)由OpenGL、Vulkan、Metal支持。它不仅支持2D、3D,同时支持CPU软件绘制和GPU硬件加速。Android、flutter都是使用它来完成绘制。
  1. GPU和OpenGL什么关系

OpenGL是规范,GPU就是该规范具体的设备实现者。

  1. Surface 与 graphic buffer

插入一个问题: 一个Android程序有多少个window??

应用本身+系统(menu+statusBar)+dialog+toast+popupWindow。

Android的一个window对应一个surface(请注意: 一个surface不一定对应window。如surfaceView), 一个surface对应一个BufferQueue。 进程可以同时拥有多个surface,如使用surfaceView(封装了单独的surface,且在单独的子线程操作)。

canvas 是通过surface.lockCnavas得到(最终调用JNI的framework层的surface.lock方法获取graphic buffer)。

surface通过dequeue拿到graphic buffer,然后进行渲染绘制,渲染完成后回到BufferQueu队列,最后通知surfaceFlinger来消费。

  1. SurfaceFlinger 是什么?

可以认为它是协调缓冲区数据和设备显示的协调者。 Vsync信号、三倍缓冲、缓冲区的合成操作都是由它来控制。

1 Android渲染演变

了解Android系统对渲染的不断优化历史,对于理解渲染很有帮助。

  • Android 4.1

引入了project butter黄油计划:Vsync、三倍缓冲、choreography编舞者。

  • android 5.0

引入了RenderThread线程(该线程是系统在framework层维护),把之前CPU直接操作绘制指令(OpenGL/vulkan/skia)部分,交给了单独的渲染线程。减少主线程的工作。即使主线程卡住,渲染也不受影响。

  • Android7.0 引入了Vulkan支持。 OpenGL是3D渲染API,VulKan是用来替换OpenGL的。它不仅支持3D,也支持2D,同时更加轻量级。

2 App做了什么(重点)

从invalidate()(它会在onVsync信号来的时候(也就是下一帧)触发onDraw()方法)方法调用开始来分析整个过程。Activity的显示最终会调用requestLayout方法,关于Activity的启动过程可以自行查阅(后续有空在写篇文章~)。

invalidate()让 drawing cache(绘制缓存)无效,也就是所谓的标脏,所以才会要重新进行绘制。 requestLayout()方法跟invalidate()调用的是同一个方法:scheduleTraversals(),如下:

ViewRootImpl.java

void invalidate() {
mDirty.set(0, 0, mWidth, mHeight);
if (!mWillDrawSoon) {
scheduleTraversals();
}
}
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

viewRootImpl的invalidate()方法会postCalback到choreography。

choreography是在viewRootImpl创建的的时注册了监听系统的vsync信号。

当onVsync回调下一帧的时候,就会执行choreography的doFrame()方法,然后执行callback,调用 viewRootImpl的performTraversal()--doTraversal()方法,从而执行onMeasure()、onLayout()、onDraw()三大流程。

那UI线程如何与RenderThread交互呢? 什么时候把绘制好的数据交给SurfaceFlinger呢?

image.png

onMeasure()、onLayout()计算出view的大小和摆放的位置,这都是UI线程要做的事情。

  1. 在draw()方法中进行绘制,但此时是没有真正去绘制。而是把绘制的指令封装为displayList,进一步封装为RendNode,在同步给RenderThread。
  2. RenderThread通过 dequeue() 拿到graphic buffer(surfaceFlinger的缓冲区),根据绘制指令直接操作OpenGL的绘制接口,最终通过GPU设备把绘制指令渲染到了离屏缓冲区graphic buffer。
  3. 完成渲染后,把缓冲区交还给SurfaceFlinger的BufferQueue。SurfaceFlinger会通过硬件设备进行layer的合成,最终展示到屏幕。

image.png

以上流程也体现了生产者与消费者模式:

image.png

生产者: APP,再深入点就是canvas->surface。

消费者:SurfaceFlinger

BufferQueue 的大小一般是3。

  • 一块缓冲区用来被SurfaceFlinger交由设备展示
  • 一块用来App绘制缓冲数据
  • 还有一块,如果App绘制超过一帧时间16ms的时候,当下一帧vsync到来,其中两块都已经被占用,所以要用到第三块,避免此次vsync信号CPU和GPU处于空闲(因为如果空闲的话,下下帧就会出现jank)。

SurfaceFlinger 做了什么

SurfaceFlinger是显示合成系统。在应用程序请求创建surface的时候,SurfaceFlinger会创建一个Layer。Layer是SurfaceFlinger操作合成的基本单元。所以,一个surface对应一个Layer。

当应用程序把绘制好的GraphicBuffer数据放入BufferQueue后,接下来的工作就是SurfaceFlinger来完成了。

image.png

说明:

系统会有多个应用程序,一个程序有多个BufferQueue队列。SurfaceFlinger就是用来决定何时以及怎么去管理和显示这些队列的。

SurfaceFlinger请求HAL硬件层,来决定这些Buffer是硬件来合成还是自己通过OpenGL来合成。

最终把合成后的buffer数据,展示在屏幕上。

最后,附上官方完整渲染架构:

image.png

收起阅读 »

【开源库剖析】KOOM V1.0.5 源码解析

一、官方项目介绍1.1 描述:KOOM是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。其中Android Java内存部分在LeakCanary的基础上进行了大量优化,解决了线上内存监控的性能问题,在不影响用户体验的前提下线上采集内存...
继续阅读 »

一、官方项目介绍

1.1 描述:

KOOM是快手性能优化团队在处理移动端OOM问题的过程中沉淀出的一套完整解决方案。其中Android Java内存部分在LeakCanary的基础上进行了大量优化,解决了线上内存监控的性能问题,在不影响用户体验的前提下线上采集内存镜像并解析。从 2020 年春节后在快手主APP上线至今解决了大量OOM问题,其性能和稳定性经受住了海量用户与设备的考验,因此决定开源以回馈社区。

1.2 特点:

  • 比leakCanary更丰富的泄漏场景检测;
  • 比leakCanary更好的检测性能;
  • 功能全面的支持线上大规模部署的闭环监控系统;

1.3 KOOM框架

image.png

1.4 快手KOOM核心流程包括:

  • 配置下发决策;
  • 监控内存状态;
  • 采集内存镜像;
  • 解析镜像文件(以下简称hprof)生成报告并上传;
  • 问题聚合报警与分配跟进。

1.5 泄漏检测触发机制优化:

泄漏检测触发机制leakCanary做法是GC过后对象WeakReference一直不被加入 ReferenceQueue,它可能存在内存泄漏。这个过程会主动触发GC做确认,可能会造成用户可感知的卡顿,而KOOM采用内存阈值监控来触发镜像采集,将对象是否泄漏的判断延迟到了解析时,阈值监控只要在子线程定期获取关注的几个内存指标即可,性能损耗很低。

image.png

1.6 heap dump优化:

传统方案会冻结应用几秒,KOOM会fork新进程来执行dump操作,对父进程的正常执行没有影响。暂停虚拟机需要调用虚拟机的art::Dbg::SuspendVM函数,谷歌从Android 7.0开始对调用系统库做了限制,快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制。

image.png

随机采集线上真实用户的内存镜像,普通dump和fork子进程dump阻塞用户使用的耗时如下:

image.png

而从官方给出的测试数据来看,效果似乎是非常好的。

二、官方demo演示

这里就直接跑下官方提供的koom-demo

点击按钮,经过dump heap -> heap analysis -> report cache/koom/report/三个流程(heap analysis时间会比较长,但是完全不影响应用的正常操作),最终在应用的cache/koom/report里生成json报告:

cepheus:/data/data/com.kwai.koom.demo/cache/koom/report # ls
2020-12-08_15-23-32.json

模拟一个最简单的单例CommonUtils持有LeakActivity实例的内存泄漏,看下json最终上报的内容是个啥:

{
   "analysisDone":true,
   "classInfos":[
       {
           "className":"android.app.Activity",
           "instanceCount":4,
           "leakInstanceCount":3
       },
       {
           "className":"android.app.Fragment",
           "instanceCount":4,
           "leakInstanceCount":3
       },
       {
           "className":"android.graphics.Bitmap",
           "instanceCount":115,
           "leakInstanceCount":0
       },
       {
           "className":"libcore.util.NativeAllocationRegistry",
           "instanceCount":1513,
           "leakInstanceCount":0
       },
       {
           "className":"android.view.Window",
           "instanceCount":4,
           "leakInstanceCount":0
       }
   ],
   "gcPaths":[
       {
           "gcRoot":"Local variable in native code",
           "instanceCount":1,
           "leakReason":"Activity Leak",
           "path":[
               {
                   "declaredClass":"java.lang.Thread",
                   "reference":"android.os.HandlerThread.contextClassLoader",
                   "referenceType":"INSTANCE_FIELD"
               },
               {
                   "declaredClass":"java.lang.ClassLoader",
                   "reference":"dalvik.system.PathClassLoader.runtimeInternalObjects",
                   "referenceType":"INSTANCE_FIELD"
               },
               {
                   "declaredClass":"",
                   "reference":"java.lang.Object[]",
                   "referenceType":"ARRAY_ENTRY"
               },
               {
                   "declaredClass":"com.kwai.koom.demo.CommonUtils",
                   "reference":"com.kwai.koom.demo.CommonUtils.context",
                   "referenceType":"STATIC_FIELD"
               },
               {
                   "reference":"com.kwai.koom.demo.LeakActivity",
                   "referenceType":"instance"
               }
           ],
           "signature":"378fc01daea06b6bb679bd61725affd163d026a8"
       }
   ],
   "runningInfo":{
       "analysisReason":"RIGHT_NOW",
       "appVersion":"1.0",
       "buildModel":"MI 9 Transparent Edition",
       "currentPage":"LeakActivity",
       "dumpReason":"MANUAL_TRIGGER",
       "jvmMax":512,
       "jvmUsed":2,
       "koomVersion":1,
       "manufacture":"Xiaomi",
       "nowTime":"2020-12-08_16-07-34",
       "pss":32,
       "rss":123,
       "sdkInt":29,
       "threadCount":17,
       "usageSeconds":40,
       "vss":5674
   }
}

这里主要分三个部分:类信息、gc引用路径、运行基本信息。这里从gcPaths中能看出LeakActivity被CommonUtils持有了引用。

框架使用这里参考官方接入文档即可,这里不赘述: github.com/KwaiAppTeam…

三、框架解析

3.1 类图

image.png

3.2 时序图

KOOM初始化流程

image.png

KOOM执行初始化方法,10秒延迟之后会在threadHandler子线程中先通过check状态判断是否开始工作,工作内容是先检查是不是有未完成分析的文件,如果有就就触发解析,没有则监控内存。

heap dump流程

image.png

HeapDumpTrigger

  • startTrack:监控自动触发dump hprof操作。开启内存监控,子线程5s触发一次检测,看当前是否满足触发heap dump的条件。条件是由一系列阀值组织,这部分后面详细分析。满足阀值后会通过监听回调给HeapDumpTrigger去执行trigger。
  • trigger:主动触发dump hprof操作。这里是fork子进程来处理的,这部分也到后面详细分析。dump完成之后通过监听回调触发HeapAnalysisTrigger.startTrack触发heap分析流程。

heap analysis流程

image.png

HeapAnalysisTrigger

  • startTrack 根据策略触发hprof文件分析。
  • trigger 直接触发hprof文件分析。由单独起进程的service来处理,工作内容主要分内存泄漏检测(activity/fragment/bitmap/window)和泄漏数据整理缓存为json文件以供上报。

四、核心源码解析

经过前面的分析,基本上对框架的使用和结构有了一个宏观了解,这部分就打算对一些实现细节进行简单分析。

4.1 内存监控触发dump规则

这里主要是研究HeapMonitor中isTrigger规则,每隔5S都会循环判断该触发条件。

com/kwai/koom/javaoom/monitor/HeapMonitor.java
@Override
public boolean isTrigger() {
  if (!started) {
   return false;
  }
  HeapStatus heapStatus = currentHeapStatus();
  if (heapStatus.isOverThreshold) {
   if (heapThreshold.ascending()) {
     if (lastHeapStatus == null || heapStatus.used >= lastHeapStatus.used) {
       currentTimes++;
     } else {
       currentTimes = 0;
     }
   } else {
     currentTimes++;
   }
  } else {
   currentTimes = 0;
  }
  lastHeapStatus = heapStatus;
  return currentTimes >= heapThreshold.overTimes();
}
private HeapStatus lastHeapStatus;
private HeapStatus currentHeapStatus() {
  HeapStatus heapStatus = new HeapStatus();
  heapStatus.max = Runtime.getRuntime().maxMemory();
  heapStatus.used = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory();
  heapStatus.isOverThreshold = 100.0f * heapStatus.used / heapStatus.max > heapThreshold.value();
  return heapStatus;
}

com/kwai/koom/javaoom/common/KConstants.java

public static class HeapThreshold {
  public static int VM_512_DEVICE = 510;
  public static int VM_256_DEVICE = 250;
  public static int VM_128_DEVICE = 128;
  public static float PERCENT_RATIO_IN_512_DEVICE = 80;
  public static float PERCENT_RATIO_IN_256_DEVICE = 85;
  public static float PERCENT_RATIO_IN_128_DEVICE = 90;

  public static float getDefaultPercentRation() {
   int maxMem = (int) (Runtime.getRuntime().maxMemory() / MB);
   if (maxMem >= VM_512_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE;
   } else if (maxMem >= VM_256_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_256_DEVICE;
   } else if (maxMem >= VM_128_DEVICE) {
     return KConstants.HeapThreshold.PERCENT_RATIO_IN_128_DEVICE;
   }
   return KConstants.HeapThreshold.PERCENT_RATIO_IN_512_DEVICE;
  }

  public static int OVER_TIMES = 3;
  public static int POLL_INTERVAL = 5000;
}

这里就是针对不同内存大小做了不同的阀值比例:

  • 应用内存>512M 80%
  • 应用内存>256M 85%
  • 应用内存>128M 90%
  • 低于128M的默认按80%

应用已使用内存/最大内存超过该比例则会触发heapStatus.isOverThreshold。连续满足3次触发heap dump,但是这个过程会考虑内存增长性,3次范围内出现了使用内存下降或者使用内存/最大内存低于对应阀值了则清零。

因此规则总结为:3次满足>阀值条件且内存一直处于上升期才触发。这样能减少无效的dump。

4.2 fork进程执行dump操作实现

这里先对比下目前市面上三方框架的主流实现方案:

image.png

LeakCanaray、Matrix、Probe采用的方案:
直接执行Debug.dumpHprofData(),它执行过程会先挂起当前进程的所有线程,然后执行dump操作,生成完成hprof文件之后再唤醒所有线程。整个过程非常耗时,会带来比较明显的卡顿,因此这个痛点严重影响该功能带到线上环境。

KOOM采用的方案:
主进程fork子进程来处理hprof dump操作,主进程本身只有在fork 子进程过程会短暂的suspend VM, 之后耗时阻塞均发生在子进程内,对主进程完全不产生影响。suspend VM本身过程时间非常短,从测试结果来看完全可以忽略不计

接下来详细分析下fork dump方案的实现: 目前项目中默认使用ForkJvmHeapDumper来执行dump。

com/kwai/koom/javaoom/dump/ForkJvmHeapDumper.java
@Override
public boolean dump(String path) {
  boolean dumpRes = false;
  try {
   int pid = trySuspendVMThenFork();//暂停虚拟机,copy-on-write fork子进程
   if (pid == 0) {//子进程中
     Debug.dumpHprofData(path);//dump hprof
     exitProcess();//_exit(0) 退出进程
   } else {//父进程中
     resumeVM();//resume当前虚拟机
     dumpRes = waitDumping(pid);//waitpid异步等待pid进程结束
   }
  } catch (Exception e) {
   e.printStackTrace();
  }
  return dumpRes;
}

为什么需要先suspendVM然后再fork?
起初我理解主要是让fork前后的内存镜像保存一致性,但是对于内存泄漏来说这个造成的影响并不大,demo直接fork好像也没有什么问题,何况这块做了大量工作绕过Android N的限制去suspendVM肯定是有其必要性的。
最终才发现,单线程是没问题的,因为线程已经停了,demo加多线程dump会卡在suspendVM。因此需要先suspendVM,再fork,最后resumeVM。

好,确认工作流之后,来尝试实现:

native层:

这里fork、waitPid 、exitProcess都比较简单,直接忽略,重点看vm相关操作:

正常操作就应该是:

void *libHandle = dlopen("libart.so", RTLD_NOW);//打开libart.so, 拿到文件操作句柄
void *suspendVM = dlsym(libHandle, LIBART_DBG_SUSPEND);//获取suspendVM方法引用
void *resumeVM = dlsym(libHandle, LIBART_DBG_RESUME);//获取resumeVM方法引用
dlclose(libHandle);//关闭libart.so文件操作句柄

这在Android N以下的版本这么操作是OK了,但是谷歌从Android 7.0开始对调用系统库做了限制,基于此前提,快手自研了kwai-linker组件,通过caller address替换和dl_iterate_phdr解析绕过了这一限制,官方文档对限制说明,那么接下来就分析下KOOM是如何绕过此限制的。

源码参考:Android 9.0

/bionic/libdl/libdl.cpp
02__attribute__((__weak__))
103void* dlopen(const char* filename, int flag) {
104 const void* caller_addr = __builtin_return_address(0);//得到当前函数返回地址
105 return __loader_dlopen(filename, flag, caller_addr);
106}

/bionic/linker/dlfcn.cpp
152void* __loader_dlopen(const char* filename, int flags, const void* caller_addr) {
153 return dlopen_ext(filename, flags, nullptr, caller_addr);
154}

131static void* dlopen_ext(const char* filename,
132 int flags,
133 const android_dlextinfo* extinfo,
134 const void* caller_addr) {
135 ScopedPthreadMutexLocker locker(&g_dl_mutex);
136 g_linker_logger.ResetState();
137 void* result = do_dlopen(filename, flags, extinfo, caller_addr);//执行do_dlopen
138 if (result == nullptr) {
139 __bionic_format_dlerror("dlopen failed", linker_get_error_buffer());
140 return nullptr;
141 }
142 return result;
143}

/bionic/linker/linker.cpp
2049void* do_dlopen(const char* name, int flags,
2050 const android_dlextinfo* extinfo,
2051 const void* caller_addr) {
2052 std::string trace_prefix = std::string("dlopen: ") + (name == nullptr ? "(nullptr)" : name);
2053 ScopedTrace trace(trace_prefix.c_str());
2054 ScopedTrace loading_trace((trace_prefix + " - loading and linking").c_str());
2055 soinfo* const caller = find_containing_library(caller_addr);
2056 android_namespace_t* ns = get_caller_namespace(caller);
...
2141 return nullptr;
2142}

这里dlopen最终执行是通过__loader_dlopen,只不过默认会传入当前函数地址,这个地址其实就是做了caller address校验,如果检测出是三方地址则校验不通过,这里传入系统函数地址则能通过校验,例如dlerror。

那么KOOM的做法是:

大于N小于Q的Android版本

using __loader_dlopen_fn = void *(*)(const char *filename, int flag, void *address);
void *handle = ::dlopen("libdl.so", RTLD_NOW);//打开libel.so
//这里直接调用其__loader_dlopen方法,它与dlopen区别是可以传入caller address
auto __loader_dlopen = reinterpret_cast<__loader_dlopen_fn>(::dlsym(handle,"__loader_dlopen"));
__loader_dlopen(lib_name, flags, (void *) dlerror);//传入dlerror系统函数地址,保证caller address校验通过,绕过Android N限制。

Android Q及其以上的版本

因为Q引入了runtime namespace,因此__loader_dlopen返回的handle为nullptr

这里通过dl_iterate_phdr在当前进程中查询已加载的符合条件的动态库基对象地址。

int DlFcn::dl_iterate_callback(dl_phdr_info *info, size_t size, void *data) {
auto target = reinterpret_cast<dl_iterate_data *>(data);
if (info->dlpi_addr != 0 && strstr(info->dlpi_name, target->info_.dlpi_name)) {
target->info_.dlpi_addr = info->dlpi_addr;
target->info_.dlpi_phdr = info->dlpi_phdr;
target->info_.dlpi_phnum = info->dlpi_phnum;
// break iterate
return 1;
}

// continue iterate
return 0;
}

dl_iterate_data data{};
data.info_.dlpi_name = "libart.so";
dl_iterate_phdr(dl_iterate_callback, &data);
CHECKP(data.info_.dlpi_addr > 0)
handle = __loader_dlopen(lib_name, flags, (void *) data.info_.dlpi_addr);

这里dl_iterate_callback会回调当前进程所装载的每一个动态库,这里过滤出libart.so对应的地址:data.info_.dlpi_addr,再通过__loader_dlopen尝试打开libart.so。

附:

struct dl_phdr_info {
ElfW(Addr) dlpi_addr;//基对象地址
const ElfW(Phdr)* dlpi_phdr;//指针数组
ElfW(Half) dlpi_phnum;//
...
};

这便是快手自研的kwai-linker组件通过caller address替换和dl_iterate_phdr解析绕过Android 7.0对调用系统库做的限制的具体实现。也是fork dump方案的核心技术点。

4.3 内存泄漏检测实现

内存泄漏检测核心代码在于SuspicionLeaksFinder.find

public Pair<List<ApplicationLeak>, List<LibraryLeak>> find() {
  boolean indexed = buildIndex();
  if (!indexed) {
   return null;
  }
  initLeakDetectors();
  findLeaks();
  return findPath();
}

这里是针对Bitmap size做判断,超过768*1366这个size的认为泄漏。

另外,NativeAllocationRegistryLeakDetector和WindowLeakDetector两类还没做具体泄漏判断规则,不参与对象泄漏检测,只是做了统计。

总结: 整体看下来,KOOM有两个值得借鉴的点:

  • 1.触发内存泄漏检测,常规是watcher activity/fragment的onDestroy,而KOOM是定期轮询查看当前内存是否到达阀值;
  • 2.dump hprof,常规是对应进程dump,而KOOM是fork进程dump。

收起阅读 »

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。

Lobster一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!一、功能介绍1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例; 2.组件中...
继续阅读 »


Lobster

一个用于Android 应用组件化时各组件的Application进行解耦的轻便型框架。 三个注解即可搞定!

一、功能介绍

1.在组件中不使用BaseApplication实例,通过注解,直接使用组件自己创建的Application实例;
2.组件中自己创建的Application生命周期方法伴随壳子工程Application生命周期调用而调用;
3.组件中自己创建的Application可以配置优先级,用于优先或延后执行。

二、应用场景

组件化框架中,各组件有时需要持有Application的实例,但很多做法是在公共库中创建BaseApplication,  
让壳子工程的Application去继承BaseApplication,进而组件去持有BaseApplication的实例达到使用的目的,
然而这样会加剧组件对公共库的过分依赖,项目较大时,就会造成一定的耦合,可能会出现改一处而动全身的场景。
因此,在组件化当中,各组件应该像一个应用一样维护一个自己的Application,使用时拿的是自己Application的实例,
与其他组件隔离,也与公共库隔离,降低耦合!

三、使用方式

1.需要在壳子工程和其他module中添加如下依赖:

android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [LOBSTER_MODULE_NAME: project.getName()]
}
}
}
}

dependencies {
implementation project(path: ':lobster-annotation')
annotationProcessor project(path: ':lobster-compiler')
...
}

2.在壳子工程和其他Module中的Application中添加注解: ShellApp注解作用于壳子工程(主工程)Application,一般来说只有一个,ModuleApp注解作用于组件Application,可以设置优先级,AppInstance注解作用于组件Application的实例。

// 壳子工程的Application
@ShellApp
public class MyApplication extends Application {

@Override
public void onCreate() {
super.onCreate();
}
}
// 其他Module的Application
@ModuleApp(priority = 1)
public class Module1App extends Application {
private static final String TAG = "Lobster";

@AppInstance
public static Application mApplication1;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module1App->onCreate");
}
}
// 其他Module的Application
@ModuleApp(priority = 2)
public class Module2App extends Application {
private static final String TAG = "Lobster";
@AppInstance
public static Application mApplication2;

@Override
public void onCreate() {
super.onCreate();
Log.i(TAG , "Module2App->onCreate");
Toast.makeText(mApplication2, "I come from Module2App", Toast.LENGTH_SHORT).show();
}
}

3.没有了,可以开始耍了!

代码下载:Lobster.zip

收起阅读 »

Android自定义搜索控件 KSearchView

KSearchView自定义搜索控件布局示例代码 <com.kacent.widget.view.KingSearchView android:id="@+id/search_view" android:layout_wi...
继续阅读 »

KSearchView

自定义搜索控件


布局示例代码

 <com.kacent.widget.view.KingSearchView
android:id="@+id/search_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginEnd="5dp"
<!-提示文本->
app:hint_text="输入搜索内容"
<!-icon的padding->
app:icon_padding_bottom="5dp"
<!-searchView输入框的padding->
app:search_padding_bottom="10dp"
app:search_padding_start="30dp"
app:search_padding_top="10dp"
<!-searchView背景设置->
app:search_view_background="@drawable/my_search_shape"
app:text_size="8sp" />

设置搜索监听器

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val searchView = findViewById<KingSearchView>(R.id.search_view)
searchView.setQueryListener(object : KingSearchView.OnQueryListener {
override fun onQuery(value: String) {
if (TextUtils.isEmpty(value)) {
Toast.makeText(this@MainActivity, "没有输入相关搜索内容", Toast.LENGTH_SHORT).show()
}
Log.e("搜索内容", value)
}
})
}
}


代码下载:KingSearchView-master.zip

收起阅读 »

Android BaseDialog(开发必备)动画、加载进度、阴影

GitHubAPK使用方法将libray模块复制到项目中,或者直接在build.gradle中依赖:allprojects { repositories { maven { url 'https://jitpack.io' } } } ...
继续阅读 »

GitHub

APK

使用方法

将libray模块复制到项目中,或者直接在build.gradle中依赖:

allprojects {
repositories {

maven { url 'https://jitpack.io' }
}
}
dependencies {
compile 'com.github.AnJiaoDe:BaseDialog:V1.1.8'
}

1.Center


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:background="@drawable/white_shape"
android:orientation="vertical">
<TextView
android:layout_width="match_parent"
android:layout_height="80dp"
android:text="确定删除吗?"
android:textSize="16sp"
android:gravity="center"/>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:background="@color/line"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="48dp"

android:text="取消"
android:id="@+id/tv_cancel"
android:gravity="center"
android:textSize="16sp"/>
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:background="@color/line"/>
<TextView
android:layout_width="0dp"
android:layout_weight="1"
android:id="@+id/tv_confirm"
android:layout_height="48dp"
android:text="确定"
android:gravity="center"

android:textSize="16sp"/>
</LinearLayout>
</LinearLayout>

2.Left


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

3.Top


<?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:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

4.Right


<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="250dp"
android:layout_height="match_parent"
android:background="@color/white"

android:orientation="vertical">

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="Google Assistant: 一句 OK, Google,多少手指都用不上了
人工智能是今年的 Google I/O 的一大主题。在发布会一开始,Google CEO 桑达拉·皮蔡(Sundar Pichai)就强调机器学习在生活中扮演的重要角色。随后,一系列基于 Google 人工智能的产品纷至沓来。



OK, Google. 这句耳熟能详的命令,如今承载了 Google 全新的产品——Google Assistant.

之所以 Google Assistant 是发布会上首个亮相的产品,是因为后续登场的数个产品都基于这一技术。Google 用将近十年的时间,改善自己的语音识别技术,更强调自然语义和对话式搜索。"
android:textSize="16sp" />

</LinearLayout>

5.Bottom


<?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:background="@color/white"
android:orientation="vertical">

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">

<TextView
android:id="@+id/tv_photo"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="拍照"
android:textSize="16sp" />

<View
android:layout_width="match_parent"
android:layout_height="0.1dp"
android:background="@color/line" />

<TextView

android:id="@+id/tv_album"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="从相册选择"

android:textSize="16sp" />
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="10dp"
android:background="@color/bg"/>
<TextView
android:id="@+id/tv_photo_cancel"
android:layout_width="match_parent"
android:layout_height="56dp"
android:gravity="center"
android:text="取消"

android:textSize="16sp" />
</LinearLayout>

6.Progress


public class MainActivity extends BaseActivity {
private BaseDialog dialog;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.btn_center).setOnClickListener(this);
findViewById(R.id.btn_left).setOnClickListener(this);
findViewById(R.id.btn_top).setOnClickListener(this);
findViewById(R.id.btn_right).setOnClickListener(this);
findViewById(R.id.btn_bottom).setOnClickListener(this);
findViewById(R.id.btn_progress).setOnClickListener(this);

}

@Override
public void onClick(View v) {

switch (v.getId()) {
case R.id.btn_center:
dialog = new BaseDialog(this);
dialog.contentView(R.layout.dialog_center)
.canceledOnTouchOutside(true).show();
dialog.findViewById(R.id.tv_confirm).setOnClickListener(this);
dialog.findViewById(R.id.tv_cancel).setOnClickListener(this);

break;
case R.id.btn_left:
BaseDialog dialog_left = new BaseDialog(this);

dialog_left.contentView(R.layout.dialog_left)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))
.dimAmount(0.5f)
.gravity(Gravity.LEFT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.LEFT)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_top:
BaseDialog dialog_top = new BaseDialog(this);

dialog_top.contentView(R.layout.dialog_photo)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
.dimAmount(0.5f)
.gravity(Gravity.TOP)
.offset(0, ScreenUtils.dpInt2px(this, 48))
.animType(BaseDialog.AnimInType.TOP)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_right:
BaseDialog dialog_right = new BaseDialog(this);

dialog_right.contentView(R.layout.dialog_right)
.layoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.MATCH_PARENT))

.gravity(Gravity.RIGHT | Gravity.CENTER)
.animType(BaseDialog.AnimInType.RIGHT)
.offset(20, 0)
.canceledOnTouchOutside(true).show();

break;
case R.id.btn_bottom:
BaseDialog dialog_bottom = new BaseDialog(this);

dialog_bottom.contentView(R.layout.dialog_photo)
.gravity(Gravity.BOTTOM)
.animType(BaseDialog.AnimInType.BOTTOM)
.canceledOnTouchOutside(true).show();


break;
case R.id.btn_progress:

ProgressDialog progressDialog = new ProgressDialog(this);
progressDialog.color_iv(0xffffffff)
.color_bg_progress(0xffffffff)
.colors_progress(0xff2a5caa).show();
break;
case R.id.tv_confirm:
dialog.dismiss();
break;
case R.id.tv_cancel:
dialog.dismiss();
break;
}

}
}

源码:

BaseDialog

public class BaseDialog extends Dialog {

public BaseDialog(Context context) {
this(context, 0);

}


public BaseDialog(Context context, int themeResId) {
super(context, themeResId);

requestWindowFeature(Window.FEATURE_NO_TITLE);// 去除对话框的标题
GradientDrawable gradientDrawable = new GradientDrawable();
gradientDrawable.setColor(0x00000000);
getWindow().setBackgroundDrawable(gradientDrawable);//设置对话框边框背景,必须在代码中设置对话框背景,不然对话框背景是黑色的

dimAmount(0.2f);
}

public BaseDialog contentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
return this;
}


public BaseDialog contentView(@NonNull View view) {
getWindow().setContentView(view);
return this;
}

public BaseDialog contentView(@NonNull View view, @Nullable ViewGroup.LayoutParams params) {
getWindow().setContentView(view, params);
return this;
}
public BaseDialog layoutParams(@Nullable ViewGroup.LayoutParams params) {
getWindow().setLayout(params.width, params.height);
return this;
}


/**
* 点击外面是否能dissmiss
*
* @param canceledOnTouchOutside
* @return
*/
public BaseDialog canceledOnTouchOutside(boolean canceledOnTouchOutside) {
setCanceledOnTouchOutside(canceledOnTouchOutside);
return this;
}

/**
* 位置
*
* @param gravity
* @return
*/
public BaseDialog gravity(int gravity) {

getWindow().setGravity(gravity);

return this;

}

/**
* 偏移
*
* @param x
* @param y
* @return
*/
public BaseDialog offset(int x, int y) {
WindowManager.LayoutParams layoutParams = getWindow().getAttributes();
layoutParams.x = x;
layoutParams.y = y;

return this;
}

/*
设置背景阴影,必须setContentView之后调用才生效
*/
public BaseDialog dimAmount(float dimAmount) {

WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.dimAmount = dimAmount;
return this;
}


/*
动画类型
*/
public BaseDialog animType(BaseDialog.AnimInType animInType) {


switch (animInType.getIntType()) {
case 0:
getWindow().setWindowAnimations(R.style.dialog_zoom);

break;
case 1:
getWindow().setWindowAnimations(R.style.dialog_anim_left);

break;
case 2:
getWindow().setWindowAnimations(R.style.dialog_anim_top);

break;
case 3:
getWindow().setWindowAnimations(R.style.dialog_anim_right);

break;
case 4:
getWindow().setWindowAnimations(R.style.dialog_anim_bottom);

break;
}
return this;
}


/*
动画类型
*/
public enum AnimInType {
CENTER(0),
LEFT(1),
TOP(2),
RIGHT(3),
BOTTOM(4);

AnimInType(int n) {
intType = n;
}

final int intType;

public int getIntType() {
return intType;
}
}
}

ProgressDialog

public class ProgressDialog extends BaseDialog {

private MaterialProgressDrawable progress;

private ValueAnimator valueAnimator;
private CircleImageView imageView;

public ProgressDialog(Context context) {
super(context);
setCanceledOnTouchOutside(false);

FrameLayout frameLayout = new FrameLayout(context);

imageView = new CircleImageView(context);

progress = new MaterialProgressDrawable(getContext(), imageView);


//设置圈圈的各种大小
progress.updateSizes(MaterialProgressDrawable.DEFAULT);

progress.showArrow(false);
imageView.setImageDrawable(progress);

frameLayout.addView(imageView);


valueAnimator = valueAnimator.ofFloat(0f, 1f);

valueAnimator.setInterpolator(new DecelerateInterpolator());
valueAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
float n = (float) animation.getAnimatedValue();
//圈圈的旋转角度
progress.setProgressRotation(n * 0.5f);
//圈圈周长,0f-1F
progress.setStartEndTrim(0f, n * 0.8f);
//箭头大小,0f-1F
progress.setArrowScale(n);
//透明度,0-255
progress.setAlpha((int) (255 * n));
}
});

getWindow().setLayout(WindowManager.LayoutParams.WRAP_CONTENT, WindowManager.LayoutParams.WRAP_CONTENT);
setContentView(frameLayout);

setOnKeyListener(new OnKeyListener() {
@Override
public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {

if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_DOWN) {
hide();
return true;
}
return false;
}
});


}


public ProgressDialog duration(long duration) {
valueAnimator.setDuration(duration);

return this;
}


public ProgressDialog radius_iv(float radius_iv) {
imageView.radius(radius_iv);

return this;
}

public ProgressDialog color_iv(int color_iv) {
imageView.color(color_iv);

return this;
}

public ProgressDialog color_bg_progress(int color_bg_progress) {
progress.setBackgroundColor(color_bg_progress);

return this;
}

/**
* //圈圈颜色,可以是多种颜色
*
* @param colors_progress
* @return
*/
public ProgressDialog colors_progress(int... colors_progress) {
progress.setColorSchemeColors(colors_progress);

return this;
}

@Override
public void show() {
super.show();
if (progress == null) return;
progress.start();
if (valueAnimator == null) return;
valueAnimator.start();


}

@Override
public void hide() {
super.hide();
if (progress == null) return;
progress.stop();
if (valueAnimator == null) return;
valueAnimator.cancel();


}
}

参考:Android Dialog

GitHub

APK

收起阅读 »

一个Android文字展示动画框架:TextSurface

文字表面一个小动画框架,可以帮助您以漂亮的方式显示消息。用法创建TextSurface实例或将其添加到您的布局中。创建Text具有TextBuilder定义文本外观和位置的实例:Text textDaai = TextBuilder .create("Daa...
继续阅读 »

文字表面

一个小动画框架,可以帮助您以漂亮的方式显示消息。


用法

  1. 创建TextSurface实例或将其添加到您的布局中。
  2. 创建Text具有TextBuilder定义文本外观和位置的实例

Text textDaai = TextBuilder
.create("Daai")
.setSize(64)
.setAlpha(0)
.setColor(Color.WHITE)
.setPosition(Align.SURFACE_CENTER).build();

  1. 创建动画并将它们传递给TextSurface实例:

textSurface.play(
new Sequential(
Slide.showFrom(Side.TOP, textDaai, 500),
Delay.duration(500),
Alpha.hide(textDaai, 1500)
)
);


调整动画

  • 要按顺序播放动画,请使用 Sequential.java

  • 要同时播放动画,请使用 Parallel.java

  • 动画/效果可以这样组合:

    new Parallel(Alpha.show(textA, 500), ChangeColor.to(textA, 500, Color.RED))

    即文本的 alpha 和颜色将在 500 毫秒内同时更改

添加您自己的动画/效果

您可以扩展两个基本类来添加自定义动画:

Proguard 配置

该框架基于reflection广泛使用的标准 android 动画类为避免混淆,您需要排除框架的类:

-keep class su.levenetc.android.textsurface.** { *; }


下载

repositories {
maven { url "https://jitpack.io" }
}
//...
dependencies {
//...
compile 'com.github.elevenetc:textsurface:0.9.1'
}




github地址:https://github.com/elevenetc/TextSurface
下载地址:master.zip
收起阅读 »

单独维护图片选择开源库ImagePicker,便于根据个人业务需要进行二次开发的要求

演示1.用法使用前,对于Android Studio的用户,可以选择添加: compile 'com.lzy.widget:imagepicker:0.6.1' //指定版本2.功能和参数含义温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没...
继续阅读 »

演示

imageimageimageimage

1.用法

使用前,对于Android Studio的用户,可以选择添加:

	compile 'com.lzy.widget:imagepicker:0.6.1'  //指定版本

2.功能和参数含义

温馨提示:目前库中的预览界面有个原图的复选框,暂时只做了UI,还没有做压缩的逻辑

配置参数参数含义
multiMode图片选着模式,单选/多选
selectLimit多选限制数量,默认为9
showCamera选择照片时是否显示拍照按钮
crop是否允许裁剪(单选有效)
style有裁剪时,裁剪框是矩形还是圆形
focusWidth矩形裁剪框宽度(圆形自动取宽高最小值)
focusHeight矩形裁剪框高度(圆形自动取宽高最小值)
outPutX裁剪后需要保存的图片宽度
outPutY裁剪后需要保存的图片高度
isSaveRectangle裁剪后的图片是按矩形区域保存还是裁剪框的形状,例如圆形裁剪的时候,该参数给true,那么保存的图片是矩形区域,如果该参数给fale,保存的图片是圆形区域
imageLoader需要使用的图片加载器,自需要实现ImageLoader接口即可

3.代码参考

更多使用,请下载demo参看源代码

  1. 首先你需要继承 com.lzy.imagepicker.loader.ImageLoader 这个接口,实现其中的方法,比如以下代码是使用 Picasso 三方加载库实现的
public class PicassoImageLoader implements ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
Picasso.with(activity)//
                   .load(Uri.fromFile(new File(path)))//
.placeholder(R.mipmap.default_image)//
.error(R.mipmap.default_image)//
.resize(width, height)//
.centerInside()//
.memoryPolicy(MemoryPolicy.NO_CACHE, MemoryPolicy.NO_STORE)//
.into(imageView);
}

@Override
public void clearMemoryCache() {
//这里是清除缓存的方法,根据需要自己实现
}
}
  1. 然后配置图片选择器,一般在Application初始化配置一次就可以,这里就需要将上面的图片加载器设置进来,其余的配置根据需要设置
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_image_picker);

ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new PicassoImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
  1. 以上配置完成后,在适当的方法中开启相册,例如点击按钮时
public void onClick(View v) {
Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);
}
}
  1. 如果你想直接调用相机
Intent intent = new Intent(this, ImageGridActivity.class);
intent.putExtra(ImageGridActivity.EXTRAS_TAKE_PICKERS,true); // 是否是直接打开相机
startActivityForResult(intent, REQUEST_CODE_SELECT);
  1. 重写onActivityResult方法,回调结果
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {
if (data != null && requestCode == IMAGE_PICKER) {
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);
MyAdapter adapter = new MyAdapter(images);
gridView.setAdapter(adapter);
} else {
Toast.makeText(this, "没有数据", Toast.LENGTH_SHORT).show();
}
}
}

代码下载:ImagePicker-master.zip

收起阅读 »

用Activity实现的锁屏程序,可有效的屏蔽Home键,Recent键,通知栏

功能目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Act...
继续阅读 »

功能

目前市面上大部分锁屏应用都是用悬浮窗实现,而不用Activity。因为用Activity实现的锁屏应用,很大的问题就是Activity能被各种办法关闭或者绕过,所以本项目参考了一些前人的经验,也反编了一些现有锁屏应用的包,最后终于基本解决了所有能绕过Activity锁屏的场景,让Activity实现的锁屏也能安安全全的挡在屏幕前。

  1. 亮屏自动启动锁屏Activity

  2. 锁屏界面屏蔽Home键,back键,recent键,防止将Activity退到后台

  3. 锁屏界面禁用通知栏下拉,防止点击通知跳到第三方应用,锁屏被绕过

  4. 最近列表中排除锁屏Activity,防止锁屏Activity在不正常的场景出现

设置说明

  1. 请先设置"我的锁屏"为默认的Launcher程序(桌面应用),才可以正常使用所有功能
  2. 第三方应用无权限禁用系统的锁屏,所以如果设置了密码锁,会出现双重锁屏情况,测试时请先禁用系统锁屏
  3. 来电和闹铃等场景会自动解除锁屏,但是来电和闹铃亮屏后,过程中按电源键关闭屏幕,再打开,锁屏界面会出现在来电或者闹铃界面之上,造成覆盖,需要另做特殊处理
收起阅读 »

Android仿ButterKnife,实现自己的BindView

仿ButterKnife,实现自己的BindViewButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实...
继续阅读 »


仿ButterKnife,实现自己的BindView

ButterKnife插件的出现让Android程序员从繁琐的findViewById重复代码中解放出来,尤其搭配各种自动生成代码的Android Studio插件,更是如虎添翼。 ButterKnife的实现原理,大家应该都有所耳闻,利用AbstractProcess,在编译时候为BindView注解的控件自动生成findViewById代码,ButterKnife#bind(Activity)方法,实质就是去调用自动生成的这些findViewById代码。 然而,当我需要去了解这些实现细节的时候,我决定去看看ButterKnife的源码。ButterKnife整个项目涵盖的注解有很多,看起来可能会消耗不少的时间,笔者基于这些天的摸索的该项目的思路,实现了自己的一个BindView注解的使用,来帮助大家了解。

GitHub链接

笔者实现的项目已经上传到Github,欢迎大家star。点击查看MyButterKnife

项目结构

Annotation module

我们需要处理的BindView注解,就声明在这个module里,简单不多说。

@Target(ElementType.FIELD)
@Retention(RetentionPolicy.CLASS)
public @interface BindView {
int value() default -1;
}

Target为FIELD类型,表示这个注解用于类内属性的声明;Retention为CLASS,表示这个注解在项目编译时起作用,如果为RUNTIME则表示在运行时起作用,RUNTIME的注解都是结合反射使用的,所以执行效率上有所欠缺,应该尽量避免使用RUNTIME类注解。 BindView内的value为int类型,正是R.id对应的类型,方便我们直接对View声明其绑定的id:

@BindView(R.id.btn)
protected Button mBtn;

Compiler module

这个module是自动生成findViewById代码的重点,这里只有一个类,继承于AbstractProcessor。

@AutoService(Processor.class)
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class BindProcess extends AbstractProcessor{
private Elements mElementsUtil;

/**
* key: eclosed elemnt
* value: inner views with BindView annotation
*/
private Map<TypeElement,Set<Element>> mElems;

@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
mElementsUtil = processingEnv.getElementUtils();
mElems = new HashMap<>();
}

@Override
public Set<String> getSupportedAnnotationTypes() {
Set<String> types = new HashSet<>();
types.add(BindView.class.getCanonicalName());
return types;
}

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
System.out.println("Process start !");

initBindElems(roundEnv.getElementsAnnotatedWith(BindView.class));
generateJavaClass();

System.out.println("Process finish !");
return true;
}

private void generateJavaClass() {
for (TypeElement enclosedElem : mElems.keySet()) {
MethodSpec.Builder methodSpecBuilder = MethodSpec.methodBuilder("bindView")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addParameter(ClassName.get(enclosedElem.asType()),"activity")
.returns(TypeName.VOID);
for (Element bindElem : mElems.get(enclosedElem)) {
methodSpecBuilder.addStatement(String.format("activity.%s = (%s)activity.findViewById(%d)",bindElem.getSimpleName(),bindElem.asType(),bindElem.getAnnotation(BindView.class).value()));
}
TypeSpec typeSpec = TypeSpec.classBuilder("Bind"+enclosedElem.getSimpleName())
.superclass(TypeName.get(enclosedElem.asType()))
.addModifiers(Modifier.FINAL,Modifier.PUBLIC)
.addMethod(methodSpecBuilder.build())
.build();
JavaFile file = JavaFile.builder(getPackageName(enclosedElem),typeSpec).build();
try {
file.writeTo(processingEnv.getFiler());
} catch (IOException e) {
e.printStackTrace();
}
}
}

private void initBindElems(Set<? extends Element> bindElems) {
for (Element bindElem : bindElems) {
TypeElement enclosedElem = (TypeElement) bindElem.getEnclosingElement();
Set<Element> elems = mElems.get(enclosedElem);
if (elems == null){
elems= new HashSet<>();
mElems.put(enclosedElem,elems);
System.out.println("Add enclose elem "+enclosedElem.getSimpleName());
}
elems.add(bindElem);
System.out.println("Add bind elem "+bindElem.getSimpleName());
}
}

private String getPackageName(TypeElement type) {
return mElementsUtil.getPackageOf(type).getQualifiedName().toString();
}
}

类注解@AutoServic用于自动生成META-INF信息,对于AbstractProcessor的继承类,需要声明在META-INF里,才能在编译时生效。有了AutoService,可以自动把注解的类加入到META-INF里。使用AutoService需要引入如下包:

compile 'com.google.auto.service:auto-service:1.0-rc2'

然后编译时就会执行proces方法来生成代码,参数annotautions是一个集合,由于上面getSupportedAnnotationTypes返回的是@BindView注解,所以annotations参数里包含所有被@BindView注解的元素。把各元素按照所在类来分组,放入map中,然后generateJavaClass方法中用该map来生成代码,这里使用了javapoet包里的类,能很方便的生成各种java类,方法,修饰符等等。方法体类代码看似复杂,但稍微学一下javapoet包的使用,就可以很快熟练该方法的作用,以下是编译后生成出来的java类代码:

package top.lizhengxian.apt_sample;

public final class BindMainActivity extends MainActivity {
public static void bindView(MainActivity activity) {
activity.mBtn = (android.widget.Button)activity.findViewById(2131427422);
activity.mTextView = (android.widget.TextView)activity.findViewById(2131427423);
}
}

而被注解的原类如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

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

生成的java类位于如下位置: 

mybutterknife module

按理说,上面已经完成了整个findViewById的代码生成,在MainActivity的onCreat方法里,调用完setContentView后,就可以直接调用BindMainActivity.bindView(this)来完成各个View和id的绑定和实例化了。 但是我们观察ButterKnife中的实现,不管是哪个Activity类,都是调用ButterKnife.bindView(this)方法来注入的。而在本项目的代码中,不同的类,就会生成不同名字继承类,比如,如果另有一个HomeActivity类,那注入就要使用BindHomeActivity.bindView(this)来实现。 怎样实现ButterKnife那样统一方法来注入呢? 还是查看源码,可以发现,ButterKnife.bindView方法使用的还是反射来调用生成的类中的方法,也就是说,ButterKnife.bindView只是提供了统一入口。 对照于此,在mybutterknife module里,我们也可以用反射实现类似的方法路由,统一所有的注入方法入口:

public class MyButterKnife {
private static Map<Class,Method> classMethodMap = new HashMap<>();
public static void bindView(Activity target){
if (target != null){
Method method = classMethodMap.get(target.getClass());
try {
if (method == null) {
String bindClassName = target.getPackageName() + ".Bind" + target.getClass().getSimpleName();
Class bindClass = Class.forName(bindClassName);
method = bindClass.getMethod("bindView", target.getClass());
classMethodMap.put(target.getClass(), method);
}
method.invoke(null, target);
}catch (Exception e){
e.printStackTrace();
}
}
}
}

sample module

综上,轻轻松松实现了我们自己的BindView注解,使用方式如下:

public class MainActivity extends AppCompatActivity {

@BindView(R.id.btn)
protected Button mBtn;

@BindView(R.id.text)
protected TextView mTextView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
MyButterKnife.bindView(this);
mBtn.setText("changed");
mTextView.setText("changed too");
}
}

运行代码,完美!


代码下载:MyButterKnife-master.zip

收起阅读 »

Android 选择图片、上传图片之PictureSelector

效果图: 【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05) 之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。 后来业务需求...
继续阅读 »


效果图:
这里写图片描述这里写图片描述这里写图片描述这里写图片描述


【注意】Demo已更新到最新版本,并稍作调整。(2019-07-05)





之前出过一篇 Android 选择图片、上传图片之ImagePicker,这个是okgo作者出的,就一般需求来讲是够了,但是没有压缩,需要自己去搞。
后来业务需求提升,页面要美,体验要好,便不是那么满足需求了,所幸在github上找到PictureSelector(然后当时没多久Matisse就开源了…可以看这里Android 选择图片、上传图片之Matisse),也不用自己再撸一个了,下面来介绍介绍PictureSelector



github



https://github.com/LuckSiege/PictureSelector


目前是一直在维护的,支持从相册或拍照选择图片或视频、音频,支持动态权限获取、裁剪(单图or多图裁剪)、压缩、主题自定义配置等功能、适配android 6.0+系统,而且你能遇到的问题,README文档都有解决方案。



功能特点


功能齐全,且兼容性好,作者也做了兼容测试



1.适配android6.0+系统
2.解决部分机型裁剪闪退问题
3.解决图片过大oom闪退问题
4.动态获取系统权限,避免闪退
5.支持相片or视频的单选和多选
6.支持裁剪比例设置,如常用的 1:1、3:4、3:2、16:9 默认为图片大小
7.支持视频预览
8.支持gif图片
9.支持.webp格式图片
10.支持一些常用场景设置:如:是否裁剪、是否预览图片、是否显示相机等
11.新增自定义主题设置
12.新增图片勾选样式设置
13.新增图片裁剪宽高设置
14.新增图片压缩处理
15.新增录视频最大时间设置
16.新增视频清晰度设置
17.新增QQ选择风格,带数字效果
18.新增自定义 文字颜色 背景色让风格和项目更搭配
19.新增多图裁剪功能
20.新增LuBan多图压缩
21.新增单独拍照功能
22.新增压缩大小设置
23.新增Luban压缩档次设置
24.新增圆形头像裁剪
25.新增音频功能查询



主题配置


这个就想怎么改就怎么改了


<!--默认样式 注意* 样式只可修改,不能删除任何一项 否则报错-->
<style name="picture.default.style" parent="Theme.AppCompat.Light.DarkActionBar">
<!-- Customize your theme here. -->
<!--标题栏背景色-->
<item name="colorPrimary">@color/bar_grey</item>
<!--状态栏背景色-->
<item name="colorPrimaryDark">@color/bar_grey</item>
<!--是否改变图片列表界面状态栏字体颜色为黑色-->
<item name="picture.statusFontColor">false</item>
<!--返回键图标-->
<item name="picture.leftBack.icon">@drawable/picture_back</item>
<!--标题下拉箭头-->
<item name="picture.arrow_down.icon">@drawable/arrow_down</item>
<!--标题上拉箭头-->
<item name="picture.arrow_up.icon">@drawable/arrow_up</item>
<!--标题文字颜色-->
<item name="picture.title.textColor">@color/white</item>
<!--标题栏右边文字-->
<item name="picture.right.textColor">@color/white</item>
<!--图片列表勾选样式-->
<item name="picture.checked.style">@drawable/checkbox_selector</item>
<!--开启图片列表勾选数字模式-->
<item name="picture.style.checkNumMode">false</item>
<!--选择图片样式0/9-->
<item name="picture.style.numComplete">false</item>
<!--图片列表底部背景色-->
<item name="picture.bottom.bg">@color/color_fa</item>
<!--图片列表预览文字颜色-->
<item name="picture.preview.textColor">@color/tab_color_true</item>
<!--图片列表已完成文字颜色-->
<item name="picture.complete.textColor">@color/tab_color_true</item>
<!--图片已选数量圆点背景色-->
<item name="picture.num.style">@drawable/num_oval</item>
<!--预览界面标题文字颜色-->
<item name="picture.ac_preview.title.textColor">@color/white</item>
<!--预览界面已完成文字颜色-->
<item name="picture.ac_preview.complete.textColor">@color/tab_color_true</item>
<!--预览界面标题栏背景色-->
<item name="picture.ac_preview.title.bg">@color/bar_grey</item>
<!--预览界面底部背景色-->
<item name="picture.ac_preview.bottom.bg">@color/bar_grey_90</item>
<!--预览界面状态栏颜色-->
<item name="picture.status.color">@color/bar_grey_90</item>
<!--预览界面返回箭头-->
<item name="picture.preview.leftBack.icon">@drawable/picture_back</item>
<!--是否改变预览界面状态栏字体颜色为黑色-->
<item name="picture.preview.statusFontColor">false</item>
<!--裁剪页面标题背景色-->
<item name="picture.crop.toolbar.bg">@color/bar_grey</item>
<!--裁剪页面状态栏颜色-->
<item name="picture.crop.status.color">@color/bar_grey</item>
<!--裁剪页面标题文字颜色-->
<item name="picture.crop.title.color">@color/white</item>
<!--相册文件夹列表选中图标-->
<item name="picture.folder_checked_dot">@drawable/orange_oval</item>
</style>

功能配置


// 进入相册 以下是例子:用不到的api可以不写
PictureSelector.create(MainActivity.this)
.openGallery()//全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.theme()//主题样式(不设置为默认样式) 也可参考demo values/styles下 例如:R.style.picture.white.style
.maxSelectNum()// 最大图片选择数量 int
.minSelectNum()// 最小选择数量 int
.imageSpanCount(4)// 每行显示个数 int
.selectionMode()// 多选 or 单选 PictureConfig.MULTIPLE or PictureConfig.SINGLE
.previewImage()// 是否可预览图片 true or false
.previewVideo()// 是否可预览视频 true or false
.enablePreviewAudio() // 是否可播放音频 true or false
.isCamera()// 是否显示拍照按钮 true or false
.imageFormat(PictureMimeType.PNG)// 拍照保存图片格式后缀,默认jpeg
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径,可不填
.enableCrop()// 是否裁剪 true or false
.compress()// 是否压缩 true or false
.glideOverride()// int glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio()// int 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
.hideBottomControls()// 是否显示uCrop工具栏,默认不显示 true or false
.isGif()// 是否显示gif图片 true or false
.compressSavePath(getPath())//压缩图片保存地址
.freeStyleCropEnabled()// 裁剪框是否可拖拽 true or false
.circleDimmedLayer()// 是否圆形裁剪 true or false
.showCropFrame()// 是否显示裁剪矩形边框 圆形裁剪时建议设为false true or false
.showCropGrid()// 是否显示裁剪矩形网格 圆形裁剪时建议设为false true or false
.openClickSound()// 是否开启点击声音 true or false
.selectionMedia()// 是否传入已选图片 List<LocalMedia> list
.previewEggs()// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中) true or false
.cropCompressQuality()// 裁剪压缩质量 默认90 int
.minimumCompressSize(100)// 小于100kb的图片不压缩
.synOrAsy(true)//同步true或异步false 压缩 默认同步
.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效 int
.rotateEnabled() // 裁剪是否可旋转图片 true or false
.scaleEnabled()// 裁剪是否可放大缩小图片 true or false
.videoQuality()// 视频录制质量 0 or 1 int
.videoMaxSecond(15)// 显示多少秒以内的视频or音频也可适用 int
.videoMinSecond(10)// 显示多少秒以内的视频or音频也可适用 int
.recordVideoSecond()//视频秒数录制 默认60s int
.isDragFrame(false)// 是否可拖动裁剪框(固定)
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code

集成方式


compile引入


dependencies {
implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.2.3'
}

build.gradle加入


allprojects {
repositories {
jcenter()
maven { url 'https://jitpack.io' }
}
}

使用


使用非常简单,你想要的基本上都有



package com.yechaoa.pictureselectordemo;

import android.Manifest;
import android.annotation.SuppressLint;
import android.content.Intent;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.View;
import android.view.WindowManager;
import android.widget.PopupWindow;
import android.widget.TextView;
import android.widget.Toast;

import com.luck.picture.lib.PictureSelector;
import com.luck.picture.lib.config.PictureConfig;
import com.luck.picture.lib.config.PictureMimeType;
import com.luck.picture.lib.entity.LocalMedia;
import com.luck.picture.lib.permissions.Permission;
import com.luck.picture.lib.permissions.RxPermissions;

import java.util.ArrayList;
import java.util.List;

import io.reactivex.functions.Consumer;

public class MainActivity extends AppCompatActivity {

private int maxSelectNum = 9;
private List<LocalMedia> selectList = new ArrayList<>();
private GridImageAdapter adapter;
private RecyclerView mRecyclerView;
private PopupWindow pop;

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

mRecyclerView = findViewById(R.id.recycler);

initWidget();
}

private void initWidget() {
FullyGridLayoutManager manager = new FullyGridLayoutManager(this, 3, GridLayoutManager.VERTICAL, false);
mRecyclerView.setLayoutManager(manager);
adapter = new GridImageAdapter(this, onAddPicClickListener);
adapter.setList(selectList);
adapter.setSelectMax(maxSelectNum);
mRecyclerView.setAdapter(adapter);
adapter.setOnItemClickListener(new GridImageAdapter.OnItemClickListener() {
@Override
public void onItemClick(int position, View v) {
if (selectList.size() > 0) {
LocalMedia media = selectList.get(position);
String pictureType = media.getPictureType();
int mediaType = PictureMimeType.pictureToVideo(pictureType);
switch (mediaType) {
case 1:
// 预览图片 可自定长按保存路径
//PictureSelector.create(MainActivity.this).externalPicturePreview(position, "/custom_file", selectList);
PictureSelector.create(MainActivity.this).externalPicturePreview(position, selectList);
break;
case 2:
// 预览视频
PictureSelector.create(MainActivity.this).externalPictureVideo(media.getPath());
break;
case 3:
// 预览音频
PictureSelector.create(MainActivity.this).externalPictureAudio(media.getPath());
break;
}
}
}
});
}

private GridImageAdapter.onAddPicClickListener onAddPicClickListener = new GridImageAdapter.onAddPicClickListener() {

@SuppressLint("CheckResult")
@Override
public void onAddPicClick() {
//获取写的权限
RxPermissions rxPermission = new RxPermissions(MainActivity.this);
rxPermission.requestEach(Manifest.permission.WRITE_EXTERNAL_STORAGE)
.subscribe(new Consumer<Permission>() {
@Override
public void accept(Permission permission) {
if (permission.granted) {// 用户已经同意该权限
//第一种方式,弹出选择和拍照的dialog
showPop();

//第二种方式,直接进入相册,但是 是有拍照得按钮的
// showAlbum();
} else {
Toast.makeText(MainActivity.this, "拒绝", Toast.LENGTH_SHORT).show();
}
}
});
}
};

private void showAlbum() {
//参数很多,根据需要添加
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())// 全部.PictureMimeType.ofAll()、图片.ofImage()、视频.ofVideo()、音频.ofAudio()
.maxSelectNum(maxSelectNum)// 最大图片选择数量
.minSelectNum(1)// 最小选择数量
.imageSpanCount(4)// 每行显示个数
.selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选PictureConfig.MULTIPLE : PictureConfig.SINGLE
.previewImage(true)// 是否可预览图片
.isCamera(true)// 是否显示拍照按钮
.isZoomAnim(true)// 图片列表点击 缩放效果 默认true
//.setOutputCameraPath("/CustomPath")// 自定义拍照保存路径
.enableCrop(true)// 是否裁剪
.compress(true)// 是否压缩
//.sizeMultiplier(0.5f)// glide 加载图片大小 0~1之间 如设置 .glideOverride()无效
.glideOverride(160, 160)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度
.withAspectRatio(1, 1)// 裁剪比例 如16:9 3:2 3:4 1:1 可自定义
//.selectionMedia(selectList)// 是否传入已选图片
//.previewEggs(false)// 预览图片时 是否增强左右滑动图片体验(图片滑动一半即可看到上一张是否选中)
//.cropCompressQuality(90)// 裁剪压缩质量 默认100
//.compressMaxKB()//压缩最大值kb compressGrade()为Luban.CUSTOM_GEAR有效
//.compressWH() // 压缩宽高比 compressGrade()为Luban.CUSTOM_GEAR有效
//.cropWH()// 裁剪宽高比,设置如果大于图片本身宽高则无效
.rotateEnabled(false) // 裁剪是否可旋转图片
//.scaleEnabled()// 裁剪是否可放大缩小图片
//.recordVideoSecond()//录制视频秒数 默认60s
.forResult(PictureConfig.CHOOSE_REQUEST);//结果回调onActivityResult code
}

private void showPop() {
View bottomView = View.inflate(MainActivity.this, R.layout.layout_bottom_dialog, null);
TextView mAlbum = bottomView.findViewById(R.id.tv_album);
TextView mCamera = bottomView.findViewById(R.id.tv_camera);
TextView mCancel = bottomView.findViewById(R.id.tv_cancel);

pop = new PopupWindow(bottomView, -1, -2);
pop.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
pop.setOutsideTouchable(true);
pop.setFocusable(true);
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 0.5f;
getWindow().setAttributes(lp);
pop.setOnDismissListener(new PopupWindow.OnDismissListener() {

@Override
public void onDismiss() {
WindowManager.LayoutParams lp = getWindow().getAttributes();
lp.alpha = 1f;
getWindow().setAttributes(lp);
}
});
pop.setAnimationStyle(R.style.main_menu_photo_anim);
pop.showAtLocation(getWindow().getDecorView(), Gravity.BOTTOM, 0, 0);

View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View view) {
switch (view.getId()) {
case R.id.tv_album:
//相册
PictureSelector.create(MainActivity.this)
.openGallery(PictureMimeType.ofImage())
.maxSelectNum(maxSelectNum)
.minSelectNum(1)
.imageSpanCount(4)
.selectionMode(PictureConfig.MULTIPLE)
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_camera:
//拍照
PictureSelector.create(MainActivity.this)
.openCamera(PictureMimeType.ofImage())
.forResult(PictureConfig.CHOOSE_REQUEST);
break;
case R.id.tv_cancel:
//取消
//closePopupWindow();
break;
}
closePopupWindow();
}
};

mAlbum.setOnClickListener(clickListener);
mCamera.setOnClickListener(clickListener);
mCancel.setOnClickListener(clickListener);
}

public void closePopupWindow() {
if (pop != null && pop.isShowing()) {
pop.dismiss();
pop = null;
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
List<LocalMedia> images;
if (resultCode == RESULT_OK) {
if (requestCode == PictureConfig.CHOOSE_REQUEST) {// 图片选择结果回调

images = PictureSelector.obtainMultipleResult(data);
selectList.addAll(images);

//selectList = PictureSelector.obtainMultipleResult(data);

// 例如 LocalMedia 里面返回三种path
// 1.media.getPath(); 为原图path
// 2.media.getCutPath();为裁剪后path,需判断media.isCut();是否为true
// 3.media.getCompressPath();为压缩后path,需判断media.isCompressed();是否为true
// 如果裁剪并压缩了,以取压缩路径为准,因为是先裁剪后压缩的
adapter.setList(selectList);
adapter.notifyDataSetChanged();
}
}
}

}





Demo:https://github.com/yechaoa/PictureSelectorDemo



收起阅读 »

Android7.0拍照以及使用uCrop裁剪

一、引入 Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切...
继续阅读 »

一、引入



  1. Android在7.0中修改了文件权限,所以从Android7.0开始要使用FileProvider来处理uri,从网上找了好多文章,解决了在7.0下拍照及相册选图的问题,但是参照网上的解决方案前切图片一直搞不定,最终使用了UCrop进行剪切图片并返回文件地址,便于与服务器交互。

  2. 本文主要介绍在Android7.0上进行拍照,相册选图以及相应的图片剪切,当然也会向下兼容,同时我也在Android4.3的手机上进行了测试,在文章最后我会附上源码,会有我自认为详细的注释哈哈。








二、拍照及相册



  1. FileProvider

    想必FileProvider大家都很熟悉了,但是想了一下感觉还是写一下比较好。



    1. 在manifest中配置

       <application

      ... ...

      <provider
      android:name="android.support.v4.content.FileProvider"
      android:authorities="com.sdwfqin.sample.fileprovider"
      android:exported="false"
      android:grantUriPermissions="true">
      <meta-data
      android:name="android.support.FILE_PROVIDER_PATHS"
      android:resource="@xml/file_paths_public"/>
      </provider>
      </application>

    2. 在 res 目录下新建文件夹 xml 然后创建资源文件 file_paths_public(名字随意,但是要和manifest中的名字匹配)

       <?xml version="1.0" encoding="utf-8"?>
      <paths>
      <!--照片-->
      <external-path
      name="my_images"
      path="Pictures"/>

      <!--下载-->
      <paths>
      <external-path
      name="download"
      path=""/>

      </paths>
      </paths>


  2. 调用相机拍照

     // 全局变量
    public static final int RESULT_CODE_1 = 201;
    // 7.0 以上的uri
    private Uri mProviderUri;
    // 7.0 以下的uri
    private Uri mUri;
    // 图片路径
    private String mFilepath = SDCardUtils.getSDCardPath() + "AndroidSamples";
    -----------
    /**
    * 拍照
    */

    private void camera() {
    File file = new File(mFilepath, System.currentTimeMillis() + ".jpg");
    if (!file.getParentFile().exists()) {
    file.getParentFile().mkdirs();
    }
    Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
    // Android7.0以上URI
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    //通过FileProvider创建一个content类型的Uri
    mProviderUri = FileProvider.getUriForFile(this, "com.sdwfqin.sample.fileprovider", file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mProviderUri);
    //添加这一句表示对目标应用临时授权该Uri所代表的文件
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    } else {
    mUri = Uri.fromFile(file);
    intent.putExtra(MediaStore.EXTRA_OUTPUT, mUri);
    }
    try {
    startActivityForResult(intent, RESULT_CODE_1);
    } catch (ActivityNotFoundException anf) {
    ToastUtils.showShort("摄像头未准备好!");
    }
    }

  3. 相册选图

     // 全局变量
    public static final int RESULT_CODE_2 = 202;
    ----------
    private void selectImg() {
    Intent pickIntent = new Intent(Intent.ACTION_PICK,
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI);
    pickIntent.setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*");
    startActivityForResult(pickIntent, RESULT_CODE_2);
    }

  4. onActivityResult

    需要注意的是拍照没有返回数据,用之前的uri就可以,从相册查找图片会返回uri

     case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    // 调用裁剪方法
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;


三、图片剪裁(重点)



  1. 因为用原生的一直是各种报错,所以我这里用的是UCrop,大家可能都见过官方的展示图,界面可能在有些需求下显得过于复杂,但是真正使用起来感觉有很多都是可以修改的哈哈哈!推荐大家看一下官方的例子。项目地址:github.com/Yalantis/uC…


  2. 简单说一下引入方法但是并不能保证是最新的



    1. 依赖

       compile 'com.github.yalantis:ucrop:2.2.1'

    2. 在AndroidManifest中添加Activity

       <activity
      android:name="com.yalantis.ucrop.UCropActivity"
      android:screenOrientation="portrait"
      android:theme="@style/Theme.AppCompat.Light.NoActionBar"/>


  3. 剪切图片

     public void cropRawPhoto(Uri uri) {

    // 修改配置参数(我这里只是列出了部分配置,并不是全部)
    UCrop.Options options = new UCrop.Options();
    // 修改标题栏颜色
    options.setToolbarColor(getResources().getColor(R.color.colorPrimary));
    // 修改状态栏颜色
    options.setStatusBarColor(getResources().getColor(R.color.colorPrimaryDark));
    // 隐藏底部工具
    options.setHideBottomControls(true);
    // 图片格式
    options.setCompressionFormat(Bitmap.CompressFormat.JPEG);
    // 设置图片压缩质量
    options.setCompressionQuality(100);
    // 是否让用户调整范围(默认false),如果开启,可能会造成剪切的图片的长宽比不是设定的
    // 如果不开启,用户不能拖动选框,只能缩放图片
    options.setFreeStyleCropEnabled(true);

    // 设置源uri及目标uri
    UCrop.of(uri, Uri.fromFile(new File(mFilepath, System.currentTimeMillis() + ".jpg")))
    // 长宽比
    .withAspectRatio(1, 1)
    // 图片大小
    .withMaxResultSize(200, 200)
    // 配置参数
    .withOptions(options)
    .start(this);
    }

  4. 剪切完图片的回掉

     if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case UCrop.REQUEST_CROP:
    // 成功(返回的是文件地址)
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    // 使用Glide显示图片
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }

  5. 完整的onActivityResult,包含拍照的回掉

     @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == UCrop.RESULT_ERROR){
    mCameraTv.setText(UCrop.getError(data) + "");
    showMsg("图片剪裁失败");
    return;
    }
    if (resultCode == RESULT_OK) {
    switch (requestCode) {
    case RESULT_CODE_1:
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
    cropRawPhoto(mProviderUri);
    } else {
    cropRawPhoto(mUri);
    }
    break;
    case RESULT_CODE_2:
    Log.i(TAG, "onActivityResult: " + data.getData());
    cropRawPhoto(data.getData());
    break;
    case UCrop.REQUEST_CROP:
    Log.i(TAG, "onActivityResult: " + UCrop.getOutput(data));
    mCameraTv.setText(UCrop.getOutput(data) + "");
    Glide.with(this)
    .load(UCrop.getOutput(data))
    .crossFade()
    .into(mCameraImg);
    break;
    }
    }
    }

    ```



四、源码


源码地址:github.com/sdwfqin/And…   

收起阅读 »

巨大图片显示 Subsampling Scale Image View

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)。包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。该视图可选地使用二次采样和图...
继续阅读 »

适用于 Android 的自定义图像视图,专为照片画廊而设计,无需OutOfMemoryErrors即可显示巨大的图像(例如地图和建筑计划)包括捏缩放、平移、旋转和动画支持,并允许轻松扩展,因此您可以添加自己的覆盖和触摸事件检测。

该视图可选地使用二次采样和图块来支持非常大的图像 - 加载低分辨率基础层,当您放大时,它会与可见区域的较小高分辨率图块重叠。这避免了在内存中保存过多数据。它非常适合显示大图像,同时允许您放大高分辨率细节。您可以禁用较小图像的平铺以及显示位图对象时。禁用平铺有一些优点和缺点,以便决定哪个最好,请参阅wiki

演示


特征

图像显示

  • 显示来自资产、资源、文件系统或位图的图像
  • 根据 EXIF 自动旋转文件系统(例如相机或图库)中的图像
  • 以 90° 为增量手动旋转图像
  • 显示源图像的一个区域
  • 在加载大图像时使用预览图像
  • 在运行时交换图像
  • 使用自定义位图解码器

启用平铺:

  • 显示巨大的图像,大于可以加载到内存中
  • 在放大时显示高分辨率细节
  • 测试高达 20,000x20,000 像素,但较大的图像速度较慢

手势检测

  • 一指平底锅
  • 两指捏合放大
  • 快速缩放(一指缩放)
  • 缩放时平移
  • 在平移和缩放之间无缝切换
  • 平移后抛出动量
  • 双击可放大和缩小
  • 禁用平移和/或缩放手势的选项

动画片

  • 为比例和中心设置动画的公共方法
  • 可定制的持续时间和缓动
  • 可选的不间断动画

可覆盖的事件检测

  • 支持OnClickListenerOnLongClickListener
  • 支持使用GestureDetector拦截事件OnTouchListener
  • 扩展以添加您自己的手势

轻松集成

  • 在 a 内使用ViewPager以创建照片库
  • 屏幕旋转后轻松恢复比例、中心和方向
  • 可以扩展以添加随图像移动和缩放的叠加图形
  • 处理视图调整大小和wrap_content布局

快速开始

1)将此库添加为应用程序的 build.gradle 文件中的依赖项。

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view:3.10.0'
}

如果您的项目使用 AndroidX,请按如下方式更改工件名称:

dependencies {
implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0'
}

2)将视图添加到您的布局 XML。

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

<com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
android:id="@+id/imageView"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

3a)现在,在您的片段或活动中,设置图像资源、资产名称或文件路径。

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.resource(R.drawable.monkey));
// ... or ...
imageView.setImage(ImageSource.asset("map.png"))
// ... or ...
imageView.setImage(ImageSource.uri("/sdcard/DCIM/DSCM00123.JPG"));

3b)或者,如果您Bitmap在内存中有一个对象,请将其加载到视图中。这不适合大图像,因为它绕过了子采样 - 您可能会得到一个OutOfMemoryError.

SubsamplingScaleImageView imageView = (SubsamplingScaleImageView)findViewById(id.imageView);
imageView.setImage(ImageSource.bitmap(bitmap));


github地址:https://github.com/davemorrissey/subsampling-scale-image-view

下载地址:master.zip

收起阅读 »

PhotoView 图片展示

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。依赖将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:allprojects { repositories { ...
继续阅读 »

PhotoView 旨在帮助生成一个易于使用的缩放 Android ImageView 实现。


依赖

将此添加到您的根build.gradle文件(不是您的模块build.gradle文件)中:

allprojects {
repositories {
maven { url "https://www.jitpack.io" }
}
}

buildscript {
repositories {
maven { url "https://www.jitpack.io" }
}
}

然后,将库添加到您的模块中 build.gradle

dependencies {
implementation 'com.github.chrisbanes:PhotoView:latest.release.here'
}

特征

  • 开箱即用的缩放,使用多点触控和双击。
  • 滚动,平滑滚动。
  • 在滚动父级(例如 ViewPager)中使用时效果很好。
  • 允许在显示的矩阵更改时通知应用程序。当您需要根据当前缩放/滚动位置更新 UI 时很有用。
  • 允许在用户点击照片时通知应用程序。

用法

提供示例展示了如何以更高级的方式使用库,但为了完整起见,以下是让 PhotoView 工作所需的全部内容:

<com.github.chrisbanes.photoview.PhotoView
android:id="@+id/photo_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

PhotoView photoView = (PhotoView) findViewById(R.id.photo_view);
photoView.setImageResource(R.drawable.image);

就是这样!

视图组的问题

有一些 ViewGroups(使用 onInterceptTouchEvent 的那些)在放置 PhotoView 时抛出异常,最显着的是ViewPagerDrawerLayout这是一个尚未解决的框架问题。为了防止此异常(通常在缩小时发生),请查看HackyDrawerLayout,您可以看到解决方案是简单地捕获异常。任何使用 onInterceptTouchEvent 的 ViewGroup 也需要扩展并捕获异常。使用HackyDrawerLayout作为如何执行此操作的模板。基本实现是:

public class HackyProblematicViewGroup extends ProblematicViewGroup {

public HackyProblematicViewGroup(Context context) {
super(context);
}

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
//uncomment if you really want to see these errors
//e.printStackTrace();
return false;
}
}
}

与 Fresco 一起使用

由于 Fresco 的复杂性,该库目前不支持 Fresco。这个项目作为一种替代解决方案。


github地址:https://github.com/Baseflow/PhotoView

下载地址:master.zip

收起阅读 »

一个酷炫的 android 粒子动画库

一、灵感做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。二、使用项目地址:github.com/...
继续阅读 »


一、灵感

做这个粒子动画库的灵感来自于 MIUI 卸载应用时的动画:

这个爆炸的粒子效果看起来很酷炫,而且粒子颜色是从 icon 中拿到的。

最开始我简单实现了类似爆炸的效果,后来想到可以直接扩展一下,写一个通用的粒子动画库。

二、使用

项目地址:github.com/ultimateHan…

Particle 是一个使用 kotlin 编写的粒子动画库,可以用几行代码轻松搞定一个粒子动画。同时也支持高度自定义的粒子动画轨迹,可以打造出非常炫酷的自定义动画。这个项目发布了 0.1 版本在 JitPack 上,按如下操作引入:

在根目录的 build.gradle 中的 allprojects 中添加(注意不是 buildScript):

allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}

然后在你的项目中引入依赖即可。

implementation 'com.github.ultimateHandsomeBoy666:Particle:0.1'

在引入了 Particle 之后,只需要下面几行简单的代码,就可以实现上面的粒子爆炸效果:

Particles.with(context, container) // container 是粒子动画的宿主父 ViewGroup
.colorFromView(button)// 从 button 中采样颜色
.particleNum(200)// 一共 200 个粒子
.anchor(button)// 把 button 作为动画的锚点
.shape(Shape.CIRCLE)// 粒子形状是圆形
.radius(2, 6)// 粒子随机半径 2~6
.anim(ParticleAnimation.EXPLOSION)// 使用爆炸动画
.start()

三、粒子形状

粒子的形状支持圆形、三角形、矩形、五角星以及矢量图形及位图,并且支持多种图形粒子混合

下面详细说明。

Shape.CIRCLE 和 Shape.HOLLOWCIRCLE

  • 圆形和空心圆

  • 使用 radius 定义圆的大小。空心圆使用 strokeWidth 定义粗细。

Shape.TRIANGLE 和 Shape.HOLLOWTRIANGLE

  • 实心三角形和空心三角形

  • 使用 width 和 height 定义三角形的大小。空心三角形使用 strokeWidth 定义粗细。

Shape.RECTANGLE 和 Shape.HOLLOWRECTANGLE

  • 实心矩形和空心矩形。

  • 使用 width 和 height 定义矩形的大小。空心矩形使用 strokeWidth 定义粗细。

Shape.PENTACLE 和 Shape.HOLLOWPENTACLE

  • 实心五角星和空心五角星

  • 使用 radius 定义五角星外接圆的大小。空心五角星使用 strokeWidth 定义粗细。

Shape.BITMAP

  • 支持位图。

  • 支持矢量图,只需要把矢量图 xml 的资源 id 传入即可。

  • 图片粒子不受 color 设置的影响。

除了上述单种图形以外,还支持多种图形的混合粒子,如下:

四、粒子动画

动画控制

粒子的动画使用 ValueAnimator 来控制,可以自行定义 animator 来控制动画的行为,包括动画时长、Interpolater、重复、开始结束的监听等等。

粒子特效

目前仅支持粒子在运动过程中的旋转,如下。后续会增加更多效果

粒子轨迹

粒子轨迹的控制使用 IPathGenerator 接口的派生类来完成。库中自带四种轨迹动画,分别是:

  • ParticleAnimation.EXPLOSION 爆炸💥效果
  • ParticleAnimation.RISE 粒子上升
  • ParticleAnimation.FALL 粒子下降
  • ParticleAnimation.FIREWORK 烟花🎇效果

如果想要自定义粒子运动轨迹的话,可以继承 IPathGenerator 接口,复写生成粒子坐标的方法:

private fun createPathGenerator(): IPathGenerator {
// LinearPathGenerator 库中自带
return object : LinearPathGenerator() {
val cos = Random.nextDouble(-1.0, 1.0)
val sin = Random.nextDouble(-1.0, 1.0)

override fun getCurrentCoord(progress: Float, duration: Long): Pair<Int, Int> {
// 在这里写你想要的粒子轨迹
val originalX = distance * progress
val originalY = 100 * sin(originalX / 50)
val x = originalX * cos - originalY * sin
val y = originalX * sin + originalY * cos
return Pair((0.01 * x * originalY).toInt(), (0.008 * y * originalX).toInt())
}
}
}

然后把这个返回 IPathGenerator 的方法通过高阶函数的形式传入即可:

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.strokeWidth(10f)
.size(20, 20)
.rotation(Rotation(600))
.bitmap(R.drawable.ic_thumbs_up)
.anim(ParticleAnimation.with({
// 控制动画的animator
createAnimator()
}, {
// 粒子运动的轨迹
createPathGenerator()
})).start()

上述代码中的 ParticleAnimation.with 方法接受两个高阶函数分别生成动画控制和粒子轨迹。

fun with(animator: () -> ValueAnimator = DEFAULT_ANIMATOR_LAMBDA,
generator: () -> IPathGenerator)
: ParticleAnimation {
return ParticleAnimation(generator, animator)
}

终于,经过上面的折腾,可以得到下面的酷炫动画:

当然,只要你想要,可以构造出无限多的粒子动画轨迹,不过这可能要求一点数学功底🐶。

在 github.com/ultimateHan… 目录下有一份我之前试验的比较酷炫的轨迹公式合集,可以参考。

五、注意事项

  • 粒子动画比较消耗内存和 CPU,所以粒子数目太多,比如超过 1000 的话,可能会有卡顿。
  • 默认在动画结束的时候,粒子是不会消失的。如果要让粒子在动画结束时消失,可以自定义 ValueAnimator 监听动画结束,在结束时调用 ParticleManager.hide() 方法来隐藏粒子。
  • 如果需要反复触发粒子动画,比如按一次按钮触发一次,可以使用一个全局的 particleManager 变量来启动和取消粒子动画,可以避免内存消耗和内存抖动。比如:
particleManager = Particles.with(this, container)
button.setOnClickListener {

particleManager!!.colorFromView(button)
.particleNum(300)
.anchor(it)
.shape(Shape.CIRCLE, Shape.BITMAP)
.radius(8, 12)
.rotation(Rotation(600))
.anim(ParticleAnimation.EXPLOSION)

particleManager!!.start()
}

代码下载:ChipsLayoutManager-master.zip

收起阅读 »

Android 可扩展视图设计

前言问题飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。当时面临一个问题:如何优雅地扩展一个...
继续阅读 »

前言

问题

飞书团队在去年对Chat页面进行了布局优化,在优化的时候发现了一个现象:很多布局(特别是RootView)往往会被附加非常多的功能(输入法监控、渲染耗时统计 、侧边栏滑出抽屉等),而且这些功能在很多场景下都会被用到。

当时面临一个问题:如何优雅地扩展一个View的功能?

常用方案

对于View的功能扩展,一般有三条路可走:

  1. 一个自定义View的无限膨胀
  2. 多层自定义View
  3. 多重继承自定义View

但是,这三个方案都有问题:

  1. 一个自定义View,会完全没有可复用性,可维护性差
  2. 多层自定义View,会有过度绘制问题(增加了视图层级)
  3. 多重继承自定义View,会有耦合性问题,因为如果有N个功能自由组合,使用继承的方式来实现,最终自定义View的个数会是:C(N,1)+C(N,2)+...+C(N,N)

一个想法

我们知道,在软件设计中有一对非常重要的概念:is-a 和 has-a  简单理解,is-a表示继承关系,has-a是组合关系,而has-a要比is-a拥有更好的可扩展性。

那么在扩展视图功能的时候,是不是也可以用has-a(组合)代替常用的is-a(继承)?

答案是可以的,而且我们可以使用委托模式来实现它,委托模式天然适合这个工作:设计的出发点就是为has-a替代is-a提供解决方案的, 而Kotlin在语言层面对委托模式提供了非常优雅的支持,在这种场景下可以使用它的by接口委托 

探索

概念定义

  • Widget: 系统View / ViewGroup、自定义View / ViewGroup。
  • WidgetPlus: 委托者。继承自Widget,并可通过register()的方式has some items。
  • DelegateItem: 被委托者。接受来自WidgetPlus的委托,负责业务逻辑的具体实现。
  • IDelegate: 被委托者接口。

不支持在 Docs 外粘贴 block

流程设计

无法复制加载中的内容

角色转换

在被委托接口IDelagate的“润滑”下,Widget、WidgetPlus和Item相互之间是可以做到无缝转换的

  • Widget -> WidgetPlus

    • 简单描述:一个视图可以改造为功能可扩展的视图(可双向
    • 转换方法:实现IDelegate接口、支持item注册
  • Widget -> DelegateItem

    • 简单描述:自定义视图可以被改造为一个功能项,供其它可扩展视图动态配置(可双向
    • 转换方法:自定义Widget移除对Widget的继承,实现IDelegate接口
  • WidgetPlus -> DelegateItem

    • 简单描述:一个可扩展视图(本身带有一部分功能),可被改造为功能项(可双向
    • 转换方法:移除对Widget的继承,保留IDelegate接口的实现

无法复制加载中的内容

通信和调用

  • 可扩展视图和扩展项应该支持双向通信:

    • WidgetPlus -> DelegateItem

      • 这个比较简单,WidgetPlus会用组合的方式持有Item,在收到业务或系统的请求时,委托Item去执行具体的实现逻辑。
    • DelegateItem -> WidgetPlus

      • 在Item初始化的时候,需要传入WidgetPlus的相关信息(widgetPlus、context、attrs、defStyleAttr、defStyleRes)
  • WidgetPlus跟Items拥有相同的API,需要设置调用原则:

    • 所有公共方法,一律使用WidgetPlus对象来触发(无论是在外部代码还是Item内部)
    • Item私有方法,使用Item对象来触发

竞争机制

一个WidgetPlus同时持有多个Item的时候,如果这些Item被委托实现了相同的方法,那么就会出现Item的内部竞争问题。这里,可以根据方法类别来分别处理:

  1. 无返回值方法

    1. 比如onMeasure(),按照Item注册列表顺序执行
  2. 有返回值方法

  • 比如onTouchEvent():Boolean,这里出现了功能冲突,因为不可能同时返回多个值,只能取第一个返回值作为WidgetPlus的返回值。
  • 对于这种情形,可以打印日志以便Develop时就被发现,解决方法有两种:
  1. 合而为一,即把两个Item合并,在一个Item中处理冲突;
  2. 分而治之,即把其中一个Item转换为WidgetPlus,创建两级视图。

关键点

1:1

  • 一个WidgetPlus可以无限扩展Item功能项,但是对一种Item功能项只能持有一个对象。
  • 但是,由于外部调用具有不可控性,所以register()的入参应该是Item的Class对象,在WidgetPlus内部反射调用Item的构造来生成对象。

Center

WidgetPlus中还是有一部分代码量的,为了减少Widget的转换成本、增加后续的可维护性,可以在WidgetPlus和Item直接再加一层DelegateCenter,由它来统一管理。

无法复制加载中的内容

Super

  • 问题:在重写Widget的系统方法时,是需要执行superMethod的,而Item在进行业务实现时,无法直接触发到这个superMethod的。
  • 有两个解决方案:
  1. 把Widget的method拆分为methodBefore()、methodAfter()、isHasSuper(),分别委托Item实现
  2. 把superMethod作为委托参数,这里可以使用Kotlin的方法类型参数

很显然,第二种方案要更好。

示意代码

 /**

* Widget

*/

package android.widget;

public class LinearLayout extends ViewGroup {

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {}

}



/**

* WidgetPlus

*/

class LinearLayoutPlus() : LinearLayout(), IDelegate by DelegateCenter() {

override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {

onDelegateMeasure(widthMeasureSpec, heightMeasureSpec) { _, _ ->

super.onMeasure(widthMeasureSpec, heightMeasureSpec)}

}

}



/**

* Center

*/

class DelegateCenter() : IDelegate {



private val itemList = mutableListOf<IItem>()



fun register(item: Class<IDelegate>) {

plusList.add(item.newInstance())

}



fun unRegister(item: Class<IDelegate>) {

plusList.remove(item)

}



override fun onDelegateMeasure(

widthMeasureSpec: Int,

heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit) {

for (item in itemList) {

item.onDelegateMeasure(widthMeasureSpec, heightMeasureSpec,superMethod)

}

}

}



/**

* delegate interface

*/

interface IDelegate : IItem {



fun register(item: Class<IDelegate>)



fun unRegister(item: Class<IDelegate>)

}



/**

* Item interface

*/

interface IItem{

fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int,

superMethod: (Int, Int) -> Unit)

}



/**

* Item1

*/

class Item1() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: I nt, superMethod: (Int, Int) -> Unit) {}

}



/**

* Item2

*/

class Item2() : IItem() {

override fun onDelegateMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int, superMethod: (Int, Int) -> Unit) {}

}



/**

* main

*/

fun main() {

val plus = LinearLayoutPlus(context, attrs)

plus.register(Item1::class.java)

plus.register(Item2::class.java)

}
复制代码

背景知识

类与类之间的关系

  • 类与类之间有六种关系:
关系描述耦合度语义代码层面
继承继承指的是一个类(称为子类、子接口)继承另外的一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力☆☆☆☆☆☆is-a在Java中继承关系通过关键字extends明确标识
实现实现指的是一个类实现接口(可以是多个)的功能☆☆☆☆☆is-a在Java中实现关系通过关键字implements明确标识
组合它体现整体与部分间的关系,而且具有不可分割性,生命周期是一致的☆☆☆☆contains-a类B作为类A的成员变量,只能从语义上来区别聚合和关联
聚合它体现整体与部分间的关系,它们是可分离的,各有自己的生命周期☆☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和关联
关联这种使用关系具有长期性,而且双方的关系一般是平等的☆☆has-a类B作为类A的成员变量,只能从语义上来区别组合和聚合
依赖这种使用关系具有临时性,非常的脆弱use-a类B作为入参,在类A的某个方法中被使用
  • 继承和实现体现的一种纵向关系,一般是明确无异议的。而组合、聚合、关联和依赖体现的是横向关系,它们之间就比较难区分了,这几种关系都是语义级别的,从代码层面并不能完全区分。

委托模式

  • 定义:有两个对象参与处理同一个请求,接受请求的对象将请求委托给另一个对象来处理。
  • 能力: 是一种基础模式,状态模式、策略模式、访问者模式等在本质上就是在特殊场合采用了委托模式,委托模式使得我们可以用组合、聚合、关联来替代继承。
  • 委托模式不能等价于代理模式: 虽然它们都是把业务需要实现的逻辑交给一个目标实现类来完成,但是使用代理模式的目的在于提供一种代理以控制对这个对象的访问,但是委托模式的出发点是将某个对象的请求拜托给另一个对象。
  • 委托模式是可以自由切换被委托者,委托者甚至可以自实现业务逻辑,例如Java ClassLoader的双亲委派模型中,在委托父加载器加载失败的情况下,可以切换为自己去加载。

收起阅读 »

Android顶部悬浮条控件HoveringScroll

上滑停靠顶端悬浮框,下滑恢复原有位置滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addView和removeView来实现。###具体实现步骤:1.让ScrollView实现...
继续阅读 »


上滑停靠顶端悬浮框,下滑恢复原有位置

滑动时,监听ScrollView的滚动Y值和悬浮区域以上的高度进行比较计算,对两个控件(布局)的显示隐藏来实现控件的顶部悬浮,通过addViewremoveView来实现。

###具体实现步骤:

1.让ScrollView实现滚动监听

具体参见HoveringScrollview

2.布局实现

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin" >

<!-- zing定义view: HoveringScrollview -->
<com.steve.hovering.samples.HoveringScrollview
android:id="@+id/hoveringScrollview"
android:layout_width="match_parent"
android:layout_height="match_parent" >

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical" >

<RelativeLayout
android:id="@+id/rlayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal" >

<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:text="TOP信息\nTOP信息\nTOP信息\nTOP信息"
android:textColor="#d19275"
android:textSize="30sp" />
</RelativeLayout>

<!-- 这个悬浮条必须是固定高度:如70dp -->
<LinearLayout
android:id="@+id/search02"
android:layout_width="match_parent"
android:layout_height="70dp" >

<LinearLayout
android:id="@+id/hoveringLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#A8A8A8"
android:gravity="center_vertical"
android:orientation="horizontal"
android:padding="10dp" >

<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:padding="10dp"
android:text="¥188\r\n原价:¥399"
android:textColor="#FF7F00" />

<Button
android:id="@+id/btnQiaBuy"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:background="#FF7F00"
android:padding="10dp"
android:onClick="clickListenerMe"
android:text="立即抢购"
android:textColor="#FFFFFF" />
</LinearLayout>
</LinearLayout>

<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:text="1测试内容\n2测试内容\n3测试内容\n4测试内容\n5测试内容\n6测试内容\n7测试内容\n8测试内容\n9测试内容\n10测试内容\n11测试内容\n12测试内容\n13测试内容\n14测试内容\n15测试内容\n16测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n测试内容\n25测试内容"
android:textSize="40sp" />
</LinearLayout>
</com.steve.hovering.samples.HoveringScrollview>

<LinearLayout
android:id="@+id/search01"
android:layout_width="match_parent"
android:layout_height="70dp"
android:orientation="vertical" >
</LinearLayout>

</RelativeLayout>

3.监听变化,实现悬停效果

通过search01和search02的addViewremoveView来实现


代码下载:ijustyce-HoveringScroll-master.zip

收起阅读 »

Android仿微信图片选择器-LQRImagePicker

LQRImagePicker完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数##一、简述:本项目是基于ImagePicker完善及界面修改。 主要工作:原项目中UI方面与微信有明显差别,如:文件...
继续阅读 »

LQRImagePicker

完全仿微信的图片选择,并且提供了多种图片加载接口,选择图片后可以旋转,可以裁剪成矩形或圆形,可以配置各种其他的参数

##一、简述:

本项目是基于ImagePicker完善及界面修改。 主要工作:

  1. 原项目中UI方面与微信有明显差别,如:文件夹选择菜单的样式就不是很美观,高度比例与微信的明显不同,故对其进行美化;

  2. 原项目在功能方面有一个致命的BUG,在一开始打开菜单后,随便点击一张图片就会直接崩溃(亲测4.4可用,但6.0直接崩溃),本项目已对此进行了解决;

  3. 编码方面,原项目中获取本地文件uri路径时,使用Uri.fromFile(),这种方式不好,控制台会一直报错(such file or directory no found),故使用Uri.parse()进行代替。

##二、使用:

不得不说,原项目是一个非常不错的项目,有很多地方值得我们学习,其中图片的加载方案让我受益匪浅,通过定义一个接口,由第三方开发者自己在自己项目中实现,避免了在库中强制使用指定图片加载工具的问题,使得本项目的扩展性增强。当然也有其他值得学习的地方,在 ImagePicker中有详细的配置方式,如有更多需求请前往原项目查看学习。这里我只记录下我自己项目中的使用配置:

###1、在自己项目中添加本项目依赖:

compile 'com.lqr.imagepicker:library:1.0.0'

###2、实现ImageLoader接口(注意不是com.nostra13.universalimageloader.core.ImageLoader),实现图片加载策略:

/**
* @创建者 CSDN_LQR
* @描述 仿微信图片选择控件需要用到的图片加载类
*/
public class UILImageLoader implements com.lqr.imagepicker.loader.ImageLoader {

@Override
public void displayImage(Activity activity, String path, ImageView imageView, int width, int height) {
ImageSize size = new ImageSize(width, height);
com.nostra13.universalimageloader.core.ImageLoader.getInstance().displayImage(Uri.parse("file://" + path).toString(), imageView, size);
}

@Override
public void clearMemoryCache() {
}
}

###3、在自定义Application中初始化(别忘了在AndroidManifest.xml中使用该自定义Application):

/**
* @创建者 CSDN_LQR
* @描述 自定义Application类
*/
public class App extends Application {

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

private void initUniversalImageLoader() {
//初始化ImageLoader
ImageLoader.getInstance().init(
ImageLoaderConfiguration.createDefault(getApplicationContext()));
}

/**
* 初始化仿微信控件ImagePicker
*/
private void initImagePicker() {
ImagePicker imagePicker = ImagePicker.getInstance();
imagePicker.setImageLoader(new UILImageLoader()); //设置图片加载器
imagePicker.setShowCamera(true); //显示拍照按钮
imagePicker.setCrop(true); //允许裁剪(单选才有效)
imagePicker.setSaveRectangle(true); //是否按矩形区域保存
imagePicker.setSelectLimit(9); //选中数量限制
imagePicker.setStyle(CropImageView.Style.RECTANGLE); //裁剪框的形状
imagePicker.setFocusWidth(800); //裁剪框的宽度。单位像素(圆形自动取宽高最小值)
imagePicker.setFocusHeight(800); //裁剪框的高度。单位像素(圆形自动取宽高最小值)
imagePicker.setOutPutX(1000);//保存文件的宽度。单位像素
imagePicker.setOutPutY(1000);//保存文件的高度。单位像素
}
}

###4、打开图片选择界面代码:

public static final int IMAGE_PICKER = 100;

Intent intent = new Intent(this, ImageGridActivity.class);
startActivityForResult(intent, IMAGE_PICKER);

###5、获取所选图片信息:

@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (resultCode == ImagePicker.RESULT_CODE_ITEMS) {//返回多张照片
if (data != null) {
//是否发送原图
boolean isOrig = data.getBooleanExtra(ImagePreviewActivity.ISORIGIN, false);
ArrayList<ImageItem> images = (ArrayList<ImageItem>) data.getSerializableExtra(ImagePicker.EXTRA_RESULT_ITEMS);

Log.e("CSDN_LQR", isOrig ? "发原图" : "不发原图");//若不发原图的话,需要在自己在项目中做好压缩图片算法
for (ImageItem imageItem : images) {
Log.e("CSDN_LQR", imageItem.path);
}
}
} }

代码下载:ImagePicker-master.zip

收起阅读 »

Android高度自定义日历控件-CalenderView

CalenderViewAndroid上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低Gradlecompile 'com.haibin:calendarview:1.0.4'<depende...
继续阅读 »

CalenderView

Android上一个优雅、高度自定义、性能高效的日历控件,支持标记、自定义颜色、农历等。Canvas绘制,速度快、占用内存低

Gradle

compile 'com.haibin:calendarview:1.0.4'
<dependency>
<groupId>com.haibin</groupId>
<artifactId>calendarview</artifactId>
<version>1.0.4</version>
<type>pom</type>
</dependency>

使用方法

 <com.haibin.calendarview.CalendarView
android:id="@+id/calendarView"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:min_year="2004"
app:scheme_text="假"
app:scheme_theme_color="@color/colorPrimary"
app:selected_color="#30cfcfcf"
app:selected_text_color="#333333"
app:week_background="#fff"
app:week_text_color="#111" />

attrs

<declare-styleable name="CalendarView">
       <attr name="week_background" format="color" /> <!--星期栏的背景-->
       <attr name="week_text_color" format="color" /> <!--星期栏文本颜色-->
       <attr name="scheme_theme_color" format="color" /> <!--标记的颜色-->
       <attr name="current_day_color" format="color" /> <!--今天的文本颜色-->
<attr name="scheme_text" format="string" /> <!--标记文本-->
<attr name="selected_color" format="color" /> <!--选中颜色-->
<attr name="selected_text_color" format="color" /> <!--选中文本颜色-->
       <attr name="min_year" format="integer" />  <!--最小年份1900-->
       <attr name="max_year" format="integer" /> <!--最大年份2099-->
</declare-styleable>

api

public int getCurDay(); //今天
public int getCurMonth(); //当前的月份
public int getCurYear(); //今年
public void showSelectLayout(final int year); //快速弹出年份选择月份
public void closeSelectLayout(final int position); //关闭选择年份并跳转日期
public void setOnDateChangeListener(OnDateChangeListener listener);//添加事件
public void setOnDateSelectedListener(OnDateSelectedListener listener);//日期选择事件
public void setSchemeDate(List<Calendar> mSchemeDate);//标记日期
public void setStyle(int schemeThemeColor, int selectLayoutBackground, int lineBg);
public void update();//动态更新

代码下载:CalendarView.zip


收起阅读 »

TurboDex: 在Android瞬间加载Dex

众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.Qu...
继续阅读 »

TurboDex: 在Android瞬间加载Dex

众所周知,Android中在Runtime加载一个 未优化的Dex文件 (尤其在 ART 模式)需要花费 很长的时间. 当你在App中使用 插件化框架 的时候, 首次加载插件就需要耗费很长的时间.

TurboDex 就是为了解决这一问题而生, 就像是给AndroidVM开启了上帝模式, 在引入TurboDex后, 无论你加载了多大的Dex文件,都可以在毫秒级别内完成.

Quick Start Guide

Building TurboDex

TurboDex的 pre-compiled 版本在 /Prebuilt 目录下, 如果你想要构建自己的TurboDex, 你需要安装 Android-NDK.

 lody@MacBook-Pro  ~/TurboDex/TurboDex/jni> ndk-build                  
SharedLibrary : libturbo-dex.so
Install : libturbo-dex.so => libs/armeabi/libturbo-dex.so
SharedLibrary : libturbo-dex.so
Install : libturbo-dex.so => libs/x86/libturbo-dex.so

Config

Maven


com.github.asLody
turbodex
1.1.0
pom

Gradle

compile 'com.github.asLody:turbodex:1.1.0'

Usage

使用TurboDex, 你需要将library 添加到你的项目中, 在 Application 中写入以下代码:


@Override
protected void attachBaseContext(Context base) {
TurboDex.enableTurboDex();
super.attachBaseContext(base);
}

开启 TurboDex后, 下列调用都不再成为拖慢你App运行的元凶:

new DexClassLoader(...):

DexFile.loadDex(...);

##其它的分析和评论 http://note.youdao.com/share/?id=28e62692d218a1f1faef98e4e7724f22&type=note#/

然而,不知道这篇笔记的作者为什么会认为Hook模块是我实现的, 我并没有给Substrate那部分的模块自己命名,而是采用了原名:MSHook, 而且, 所有的Cydia源码我也保留了头部的协议申明,你知道源码的出处,却没有意识到这一点?

代码下载:lody-WelikeAndroid-master.zip

收起阅读 »

WelikeAndroid 是一款引入即用的便捷开发框架,一行代码完成http请求,bitmap异步加载,数据库增删查改,同时拥有最超前的异常隔离机制!

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.##Welike带来了哪些特征?WelikeAndroid...
继续阅读 »

##WelikeAndroid 是什么? WelikeAndroid 是一款引入即用的便捷开发框架,致力于为程序员打造最佳的编程体验,
使用WelikeAndroid, 你会觉得写代码是一件很轻松的事情.

##Welike带来了哪些特征?

WelikeAndroid目前包含五个大模块:

  • 异常安全隔离模块(实验阶段):当任何线程抛出任何异常,我们的异常隔离机制都会让UI线程继续运行下去.
  • Http模块: 一行代码完成POST、GET请求和Download,支持上传, 高度优化Disk的缓存加载机制,
    自由设置缓存大小、缓存时间(也支持永久缓存和不缓存).
  • Bitmap模块: 一行代码完成异步显示图片,无需考虑OOM问题,支持加载前对图片做自定义处理.
  • Database模块: 支持NotNull,Table,ID,Ignore等注解,Bean无需Getter和Setter,一键式部署数据库.
  • ui操纵模块: 我们为Activity基类做了完善的封装,继承基类可以让代码更加优雅.
  • :请不要认为功能相似,框架就不是原创,源码摆在眼前,何不看一看?

使用WelikeAndroid需要以下权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />

##下文将教你如何圆润的使用WelikeAndroid:
###通过WelikeContext在任意处取得上下文:

  • WelikeContext.getApplication(); 就可以取得当前App的上下文
  • WelikeToast.toast("你好!"); 简单一步弹出Toast.

##WelikeGuard(异常安全隔离机制用法):

  • 第一步,开启异常隔离机制:
WelikeGuard.enableGuard();
  • 第二步,注册一个全局异常监听器:

WelikeGuard.registerUnCaughtHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread thread, Throwable ex) {

WelikeGuard.newThreadToast("出现异常了: " + ex.getMessage() );

}
});
  • 你也可以自定义异常:

/**
*
* 自定义的异常,当异常被抛出后,会自动回调onCatchThrowable函数.
*/
@Catch(process = "onCatchThrowable")
public class CustomException extends IllegalAccessError {

public static void onCatchThrowable(Thread t){
WeLog.e(t.getName() + " 抛出了一个异常...");
}
}
  • 另外,继承自UncaughtThrowable的异常我们不会对其进行拦截.

使用Welike做屏幕适配:

Welike的ViewPorter类提供了屏幕适配的Fluent-API,我们可以通过一组流畅的API轻松做好屏幕适配.

        ViewPorter.from(button).ofScreen().divWidth(2).commit();//宽度变为屏幕的二分之一
ViewPorter.from(button).of(viewGroup).divHeight(2).commit();//高度变为viewGroup的二分之一
ViewPorter.from(button).div(2).commit();//宽度和高度变为屏幕的四分之一
ViewPorter.from(button).of(this).fillWidth().fillHeight().commit();//宽度和高度铺满Activity
ViewPorter.from(button).sameAs(imageView).commit();//button的宽度和高度和imageView一样

WelikeHttp入门:

首先来看看框架的调试信息,是不是一目了然. DEBUG DEBUG2

  • 第一步,取得WelikeHttp默认实例.
WelikeHttp welikeHttp = WelikeHttp.getDefault();
  • 第二步,发送一个Get请求.
HttpParams params = new HttpParams();
params.putParams("app","qr.get",
"data","Test");//一次性放入两对 参数 和 值

//发送Get请求
HttpRequest request = welikeHttp.get("http://api.k780.com:88", params, new HttpResultCallback() {
@Override
public void onSuccess(String content) {
super.onSuccess(content);
WelikeToast.toast("返回的JSON为:" + content);
}

@Override
public void onFailure(HttpResponse response) {
super.onFailure(response);
WelikeToast.toast("JSON请求发送失败.");
}

@Override
public void onCancel(HttpRequest request) {
super.onCancel(request);
WelikeToast.toast("请求被取消.");
}
});

//取消请求,会回调onCancel()
request.cancel();

当然,我们为满足需求提供了多种扩展的Callback,目前我们提供以下Callback供您选择:

  • HttpCallback(响应为byte[]数组)
  • FileUploadCallback(仅在上传文件时使用)
  • HttpBitmapCallback(建议使用Bitmap模块)
  • HttpResultCallback(响应为String)
  • DownloadCallback(仅在download时使用)

如需自定义Http模块的配置(如缓存时间),请查看HttpConfig.

WelikeBitmap入门:

  • 第一步,取得默认的WelikeBitmap实例:

//取得默认的WelikeBitmap实例
WelikeBitmap welikeBitmap = WelikeBitmap.getDefault();
  • 第二步,异步加载一张图片:
BitmapRequest request = welikeBitmap.loadBitmap(imageView,
"http://img0.imgtn.bdimg.com/it/u=937075122,1381619862&fm=21&gp=0.jpg",
android.R.drawable.btn_star,//加载中显示的图片
android.R.drawable.ic_delete,//加载失败时显示的图片
new BitmapCallback() {

@Override
public Bitmap onProcessBitmap(byte[] data) {
//如果需要在加载时处理图片,可以在这里处理,
//如果不需要处理,就返回null或者不复写这个方法.
return null;
}

@Override
public void onPreStart(String url) {
super.onPreStart(url);
//加载前回调
WeLog.d("===========> onPreStart()");
}

@Override
public void onCancel(String url) {
super.onCancel(url);
//请求取消时回调
WeLog.d("===========> onCancel()");
}

@Override
public void onLoadSuccess(String url, Bitmap bitmap) {
super.onLoadSuccess(url, bitmap);
//图片加载成功后回调
WeLog.d("===========> onLoadSuccess()");
}

@Override
public void onRequestHttp(HttpRequest request) {
super.onRequestHttp(request);
//图片需要请求http时回调
WeLog.d("===========> onRequestHttp()");
}

@Override
public void onLoadFailed(HttpResponse response, String url) {
super.onLoadFailed(response, url);
//请求失败时回调
WeLog.d("===========> onLoadFailed()");
}
});
  • 如果需要自定义Config,请看BitmapConfig这个类.

##WelikeDAO入门:

  • 首先写一个Bean.

/*表名,可有可无,默认为类名.*/
@Table(name="USER",afterTableCreate="afterTableCreate")
public class User{
@ID
public int id;//id可有可无,根据自己是否需要来加.

/*这个注解表示name字段不能为null*/
@NotNull
public String name;

public static void afterTableCreate(WelikeDao dao){
//在当前的表被创建时回调,可以在这里做一些表的初始化工作
}
}
  • 然后将它写入到数据库
WelikeDao db = WelikeDao.instance("Welike.db");
User user = new User();
user.name = "Lody";
db.save(user);
  • 从数据库取出Bean

User savedUser = db.findBeanByID(1);
  • SQL复杂条件查询
List<User> users = db.findBeans().where("name = Lody").or("id = 1").find();
  • 更新指定ID的Bean
User wantoUpdateUser = new User();
wantoUpdateUser.name = "NiHao";
db.updateDbByID(1,wantoUpdateUser);
  • 删除指ID定的Bean
db.deleteBeanByID(1);
  • 更多实例请看DEMO和API文档.

##十秒钟学会WelikeActivity

  • 我们将Activity的生命周期划分如下:

=>@initData(所有标有InitData注解的方法都最早在子线程被调用)
=>initRootView(bundle)
=>@JoinView(将标有此注解的View自动findViewByID和setOnClickListener)
=>onDataLoaded(数据加载完成时回调)
=>点击事件会回调onWidgetClick(View Widget)

###关于@JoinView的细节:

  • 有以下三种写法:
@JoinView(name = "welike_btn")
Button welikeBtn;
@JoinView(id = R.id.welike_btn)
Button welikeBtn;
@JoinView(name = "welike_btn",click = false)
Button welikeBtn;
  • clicktrue时会自动调用view的setOnClickListener方法,并在onWidgetClick回调.
  • 当需要绑定的是一个Button的时候, click属性默认为true,其它的View则默认为false.
收起阅读 »

Java原生的Http网络框架,底层基于HttpNet,动态代理+构建的!

#Elegant项目结构如下 Elegant采用Retrofit动态代理+构建的思想,本身并不做网络请求,网络部分基于HttpNet实现,本着简洁清晰的思想,保持了和Retrofit相似的API##gradlecompile 'com.haibin:...
继续阅读 »


#Elegant项目结构如下 输入图片说明

Elegant采用Retrofit动态代理+构建的思想,本身并不做网络请求,网络部分基于HttpNet实现,本着简洁清晰的思想,保持了和Retrofit相似的API

##gradle

compile 'com.haibin:elegant:1.1.9'

##创建API接口

public interface LoginService {

//普通POST
@Headers({"Cookie:cid=adcdefg;"})
@POST("api/users/login")
Call<BaseModel<User>> login(@Form("email") String email,
@Form("pwd") String pwd,
@Form("versionNum") int versionNum,
@Form("dataFrom") int dataFrom);

// 上传文件
@POST("action/apiv2/user_edit_portrait")
@Headers("Cookie:xxx=hbbb;")
Call<String> postAvatar(@File("portrait") String file);


//JSON POST
@POST("action/apiv2/user_edit_portrait")
@Headers("Cookie:xxx=hbbb;")
Call<String> postJson(@Json String file);

//PATCH
@PATCH("mobile/user/{uid}/online")
Call<ResultBean<String>> handUp(@Path("uid") long uid);
}

##执行请求

public static final String API = "http://www.oschina.net/";
public static Elegant elegant = new Elegant();

static {
elegant.registerApi(API);
}

LoginService service = elegant.from(LoginService.class)
.login("xxx@qq.com", "123456", 2, 2);
.withHeaders(Headers...)
.execute(new CallBack<BaseModel<User>>() {
@Override
public void onResponse(Response<BaseModel<User>> response) {

}

@Override
public void onFailure(Exception e) {

}                               });

代码下载:dev-Elegant-master.zip

收起阅读 »

XVideo 一个能自动进行压缩的小视频录制库

XVideo一个能自动进行压缩的小视频录制库特征支持自定义小视频录制时的视频质量。支持自定义视频录制的界面。支持自定义最大录制时长和最小录制时长。支持自定义属性的视频压缩。演示(请star支持)Demo下载添加Gradle依赖1.在项目根目录的 build.g...
继续阅读 »

XVideo

一个能自动进行压缩的小视频录制库

特征

  • 支持自定义小视频录制时的视频质量。

  • 支持自定义视频录制的界面。

  • 支持自定义最大录制时长和最小录制时长。

  • 支持自定义属性的视频压缩。

演示(请star支持)

Demo下载

Github

添加Gradle依赖

1.在项目根目录的 build.gradle 的 repositories 添加:

allprojects {
repositories {
...
maven { url "https://jitpack.io" }
}
}

2.在主项目的 build.gradle 中增加依赖。

dependencies {
···
implementation 'com.github.xuexiangjys:XVideo:1.0.2'
}

3.进行视频录制存储目录地址的设置。

/**
* 初始化xvideo的存放路径
*/
public static void initVideo() {
XVideo.setVideoCachePath(PathUtils.getExtDcimPath() + "/xvideo/");
// 初始化拍摄
XVideo.initialize(false, null);
}

视频录制

1.视频录制需要CAMERA权限和STORAGE权限。在Android6.0机器上需要动态获取权限,推荐使用XAOP进行权限申请。

2.调用MediaRecorderActivity.startVideoRecorder开始视频录制。

/**
* 开始录制视频
* @param requestCode 请求码
*/
@Permission({PermissionConsts.CAMERA, PermissionConsts.STORAGE})
public void startVideoRecorder(int requestCode) {
MediaRecorderConfig mediaRecorderConfig = MediaRecorderConfig.newInstance();
XVideo.startVideoRecorder(this, mediaRecorderConfig, requestCode);
}

3.MediaRecorderConfig是视频录制的配置对象,可自定义视频的宽、高、时长以及质量等。

MediaRecorderConfig config = new MediaRecorderConfig.Builder()
.fullScreen(needFull) //是否全屏
.videoWidth(needFull ? 0 : Integer.valueOf(width)) //视频的宽
.videoHeight(Integer.valueOf(height)) //视频的高
.recordTimeMax(Integer.valueOf(maxTime)) //最大录制时间
.recordTimeMin(Integer.valueOf(minTime)) //最小录制时间
.maxFrameRate(Integer.valueOf(maxFrameRate)) //最大帧率
.videoBitrate(Integer.valueOf(bitrate)) //视频码率
.captureThumbnailsTime(1)
.build();

视频压缩

使用libx264进行视频压缩。由于手机本身CPU处理能力有限的问题,在手机上进行视频压缩的效率并不是很高,大约压缩的时间需要比视频拍摄本身的时长还要长一些。

LocalMediaConfig.Builder builder = new LocalMediaConfig.Builder();
final LocalMediaConfig config = builder
.setVideoPath(path) //设置需要进行视频压缩的视频路径
.captureThumbnailsTime(1)
.doH264Compress(compressMode) //设置视频压缩的模式
.setFramerate(iRate) //帧率
.setScale(fScale) //压缩比例
.build();
CompressResult compressResult = XVideo.startCompressVideo(config);

混淆配置

-keep class com.xuexiang.xvideo.jniinterface.** { *; }

代码下载:XVideo.zip

收起阅读 »

模版空壳Android工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate和XHttp2)

TemplateAppProjectAndroid空壳模板工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate、XHttp2、友盟统计和walle多渠道打包)效果使用方式视频教程-如何使用模板工程1.克隆项目git clone htt...
继续阅读 »

TemplateAppProject

Android空壳模板工程,快速搭建(集成了XUI、XUtil、XAOP、XPage、XUpdate、XHttp2、友盟统计和walle多渠道打包)

效果

templateproject_demo.gif


使用方式

视频教程-如何使用模板工程

1.克隆项目

2.修改项目名(文件夹名),并删除目录下的.git文件夹(隐藏文件)

3.使用AS打开项目,然后修改包名applicationIdapp_name

  • 修改包名

templateproject_1.png

templateproject_2.png

  • 修改applicationId

templateproject_3.png

  • 修改app_name

templateproject_5.png

项目打包

1.修改工程根目录的gradle.properties中的isNeedPackage=true

2.添加并配置keystore,在versions.gradle中修改app_release相关参数。

3.如果考虑使用友盟统计的话,在local.properties中设置应用的友盟ID:APP_ID_UMENG

4.使用./gradlew clean assembleReleaseChannels进行多渠道打包。

代码下载:TemplateAppProject-master.zip

收起阅读 »

android侧滑菜单SuperSlidingPaneLayout

SuperSlidingPaneLayout     SuperSlidingPaneLayout是在SlidingPaneLayout的基础之上扩展修改,新增几种不同的侧滑效果,基本用法与SlidingPan...
继续阅读 »


SuperSlidingPaneLayout

Download Jitpack API License Blog QQGroup

SuperSlidingPaneLayout是在SlidingPaneLayout的基础之上扩展修改,新增几种不同的侧滑效果,基本用法与SlidingPaneLayout一致。

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>superslidingpanelayout</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:superslidingpanelayout:1.1.0'

Lvy:

<dependency org='com.king.view' name='superslidingpanelayout' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

使用布局示例:

<?xml version="1.0" encoding="utf-8"?>
<com.king.view.superslidingpanelayout.SuperSlidingPaneLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/superSlidingPaneLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/menu_bg1"
app:mode="default_"
app:compat_sliding="false">
<include layout="@layout/menu_layout"/>
<LinearLayout
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<include layout="@layout/top_title_bar"/>
<TextView
android:id="@+id/tvMode"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/white"
android:gravity="center"
android:text="Default"
android:textSize="24sp"/>
</LinearLayout>

</com.king.view.superslidingpanelayout.SuperSlidingPaneLayout>

代码设置侧滑模式效果:

        superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.DEFAULT);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.TRANSLATION);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_MENU);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_PANEL);

superSlidingPaneLayout.setMode(SuperSlidingPaneLayout.Mode.SCALE_BOTH);

更多使用详情请查看demo示例。

相关博文:http://blog.csdn.net/jenly121/article/details/52757409

代码下载:SuperSlidingPaneLayout.zip

收起阅读 »

CounterView for Android 一个数字变化效果的计数器视图控件。

CounterViewCounterView for Android 一个数字变化效果的计数器视图控件。Gif 展示引入Maven:<dependency> <groupId>com.king.view</groupId>...
继续阅读 »


CounterView

CounterView for Android 一个数字变化效果的计数器视图控件。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>counterview</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:counterview:1.1.0'

Lvy:

<dependency org='com.king.view' name='counterview' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

核心代码

counterView.showAnimation(10000);

代码下载:CounterView.zip

收起阅读 »

NeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。

NeverCrashNeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。Gif 展示引入Maven: com.king.thread nevercrash 1.0.0 pom Gra...
继续阅读 »

NeverCrash

NeverCrash for Android 一个全局捕获Crash的库。信NeverCrash,永不Crash。

Gif 展示

Image

引入

Maven:


com.king.thread
nevercrash
1.0.0
pom

Gradle:

compile 'com.king.thread:nevercrash:1.0.0'

Lvy:



如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

核心代码(大道至简)

NeverCrash.init(CrashHandler);

代码示例

public class App extends Application {

@Override
public void onCreate() {
super.onCreate();
NeverCrash.init(new NeverCrash.CrashHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Log.d("Jenly", Log.getStackTraceString(e));
// e.printStackTrace();
showToast(e.getMessage());


}
});
}

private void showToast(final String text){

new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(),text,Toast.LENGTH_SHORT).show();
}
});
}

}

代码下载:NeverCrash.zip

收起阅读 »

SlideBar for Android 一个很好用的联系人快速索引。

SlideBarSlideBar for Android 一个很好用的联系人快速索引。Gif 展示引入Maven:<dependency> <groupId>com.king.view</groupId> <a...
继续阅读 »

SlideBar

SlideBar for Android 一个很好用的联系人快速索引。

Gif 展示


引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>slidebar</artifactId>
<version>1.1.0</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:slidebar:1.1.0'

Lvy:

<dependency org='com.king.view' name='slidebar' rev='1.1.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

具体实现详情请戳传送门

代码下载:SlideBar.zip

收起阅读 »

Android码表变化的旋转计数器动画控件

SpinCounterViewSpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。Gif 展示引入Maven:<dependency> <groupId>com.king.view</...
继续阅读 »

SpinCounterView

SpinCounterView for Android 一个类似码表变化的旋转计数器动画控件。

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>spincounterview</artifactId>
<version>1.1.1</version>
<type>pom</type>
</dependency>

Gradle:

compile 'com.king.view:spincounterview:1.1.1'

Lvy:

<dependency org='com.king.view' name='spincounterview' rev='1.1.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)

allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局

    <com.king.view.spincounterview.SpinCounterView
android:id="@+id/scv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:max="100"
app:maxValue="1000"/>

核心动画代码

spinCounterView.showAnimation(80);

代码下载:SpinCounterView.zip

收起阅读 »

一行代码实现欢迎引导页-GuidePage

GuidePageGuidePage for Android 是一个App欢迎引导页。一般用于首次打开App时场景,通过引导页指南,概述App特色等相关信息功能介绍 链式调用,简单易用 自定义配置,满足各种需求引入Maven:<dep...
继续阅读 »

GuidePage

GuidePage for Android 是一个App欢迎引导页。一般用于首次打开App时场景,通过引导页指南,概述App特色等相关信息

功能介绍

  •  链式调用,简单易用
  •  自定义配置,满足各种需求


引入

Maven:

<dependency>
<groupId>com.king.guide</groupId>
<artifactId>guidepage</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>

Gradle:

//AndroidX
implementation 'com.king.guide:guidepage:1.0.0'

Lvy:

<dependency org='com.king.guide' name='guidepage' rev='1.0.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

代码示例

    //简单调用示例
GuidePage.load(intArrayOf(R.drawable.guide_page_1,R.drawable.guide_page_2,R.drawable.guide_page_3,R.drawable.guide_page_4))
.pageDoneDrawableResource(R.drawable.btn_done)
.start(this)//Activity or Fragment
      //Demo中的调用示例
GuidePage.load(intArrayOf(R.drawable.guide_page_1,R.drawable.guide_page_2,R.drawable.guide_page_3,R.drawable.guide_page_4))
.pageDoneDrawableResource(R.drawable.btn_done)
// .indicatorDrawableResource(R.drawable.indicator_radius)
// .indicatorSize(this,6f)//默认5dp
.showSkip(v.id == R.id.btn1)//是否显示“跳过”
.lastPageHideSkip(true)//最后一页是否隐藏“跳过”
.onGuidePageChangeCallback(object : GuidePage.OnGuidePageChangeCallback{//引导页改变回调接口

override fun onPageDone(skip: Boolean) {
//TODO 当点击完成(立即体验)或者右上角的跳过时,触发此回调方法
//这里可以执行您的逻辑,比如跳转到APP首页或者登陆页
if(skip){
Toast.makeText(this@MainActivity,"跳过",Toast.LENGTH_SHORT).show()
}else{
Toast.makeText(this@MainActivity,"立即体验",Toast.LENGTH_SHORT).show()
}
}

})
.start(this)//Activity or Fragment

相关说明

  • 通过GuidePage链式调用,可以满足一些基本需求场景。
  • GuidePage中提供的配置无法满足需求时,可通过资源命名相同方式去自定义配置,即:资源覆盖方式。如dimensstyles等对应的资源。

更多使用详情,请查看app中的源码使用示例

代码下载:GuidePage.zip

收起阅读 »

Android通用的Adapter、Activity、Fragment、Dialog等-base

BaseBase是针对于Android开发封装好一些常用的基类,主要包括通用的Adapter、Activity、Fragment、Dialog等、和一些常用的Util类,只为更简单。Base 3.x 在Base 2.x 的基础上进行了重构,最大的变化...
继续阅读 »

Base

Base是针对于Android开发封装好一些常用的基类,主要包括通用的Adapter、Activity、Fragment、Dialog等、和一些常用的Util类,只为更简单。

Base 3.x 在Base 2.x 的基础上进行了重构,最大的变化是将base-adapter和base-util提取了出来。

单独提取library主要是为了模块化,使其更加独立。在使用时需要用哪个库就引入库,这样就能尽可能的减少引入库的体积。

  • base 主要是封装了常用的Activity、Fragment、DialogFragment、Dialog等作为基类,方便使用。
  • base-adapter 主要是封装了各种Adapter、简化自定义Adapter步骤,让写自定义适配器从此更简单。
  • base-util 主要是封装了一些常用的工具类。

AndroidX version

引入

Maven:

//base
<dependency>
<groupId>com.king.base</groupId>
<artifactId>base</artifactId>
<version>3.2.1</version>
<type>pom</type>
</dependency>

//base-adapter
<dependency>
<groupId>com.king.base</groupId>
<artifactId>adapter</artifactId>
<version>3.2.1</version>
<type>pom</type>
</dependency>

//base-util
<dependency>
<groupId>com.king.base</groupId>
<artifactId>util</artifactId>
<version>3.2.1</version>
<type>pom</type>
</dependency>

Gradle:

//---------- AndroidX 版本
//base
implementation 'com.king.base:base:3.2.1-androidx'

//base-adapter
implementation 'com.king.base:adapter:3.2.1-androidx'

//base-util
implementation 'com.king.base:util:3.2.1-androidx'


//---------- Android 版本
//base
implementation 'com.king.base:base:3.2.1'

//base-adapter
implementation 'com.king.base:adapter:3.2.1'

//base-util
implementation 'com.king.base:util:3.2.1'

Lvy:

//base
<dependency org='com.king.base' name='base' rev='3.2.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

//base-adapter
<dependency org='com.king.base' name='adapter' rev='3.2.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>

//base-util
<dependency org='com.king.base' name='util' rev='3.2.1'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

引入的库:

//---------- AndroidX 版本
//base
compileOnly 'androidx.appcompat:appcompat:1.0.0+'
compileOnly 'com.king.base:util:3.2.1-androidx'

//base-adapter
compileOnly 'androidx.appcompat:appcompat:1.0.0+'
compileOnly 'androidx.recyclerview:recyclerview:1.0.0+'

//base-util
compileOnly 'androidx.appcompat:appcompat:1.0.0+'
//---------- Android 版本
//base
compileOnly 'com.android.support:appcompat-v7:28.0.0'
compileOnly 'com.king.base:util:3.2.1'

//base-adapter
compileOnly 'com.android.support:appcompat-v7:28.0.0'
compileOnly 'com.android.support:recyclerview-v7:28.0.0'

//base-util
compileOnly 'com.android.support:appcompat-v7:28.0.0'

简要说明:

Base主要实用地方体现在:出统一的代码风格,实用的各种基类,BaseActivity和BaseFragment里面还有许多实用的代码封装,只要用了Base,使用Fragment就感觉跟使用Activtiy基本是一样的。

代码示例:

通用的Adapter

/**
*
* 只需继承通用的适配器(ViewHolderAdapter或ViewHolderRecyclerAdapter),简单的几句代码,妈妈再也不同担心我写自定义适配器了。
*/
public class TestAdapter extends ViewHolderAdapter<String> {


public TestAdapter(Context context, List<String> listData) {
super(context, listData);
}

@Override
public View buildConvertView(LayoutInflater layoutInflater,T t,int position, ViewGroup parent) {
return inflate(R.layout.list_item,parent,false);
}

@Override
public void bindViewDatas(ViewHolder holder, String s, int position) {
holder.setText(R.id.tv,s);
}
}

基类BaseActivity

public class TestActivity extends BaseActivity {

private TextView tv;
private Button btn;

@Override
public void initUI() {
//TODO:初始化UI
setContentView(R.layout.activity_test);
tv = findView(R.id.tv);
btn = findView(R.id.btn);
}

@Override
public void initData() {
//TODO:初始化数据(绑定数据)
tv.setText("text");
}

}

GestureActivity

public class TestGestureActivity extends GestureActivity {

private TextView tv;
private Button btn;

@Override
public void initUI() {
//TODO:初始化UI
setContentView(R.layout.activity_test);
tv = findView(R.id.tv);
btn = findView(R.id.btn);
}

@Override
public void initData() {
//TODO:初始化数据(绑定数据)
tv.setText("text");
}

@Override
public void onLeftFling() {
//TODO:向左滑动
}

@Override
public boolean onRightFling() {
//TODO:向右滑动,默认执行finish,返回为true表示拦截事件。
return false;
}
}

SplashActivity

public class TestSplashActivity extends SplashActivity {
@Override
public int getContentViewId() {
return R.layout.activity_splash;
}

@Override
public Animation.AnimationListener getAnimationListener() {
return new Animation.AnimationListener() {
@Override
public void onAnimationStart(Animation animation) {

}

@Override
public void onAnimationEnd(Animation animation) {
//TODO: 启动动画结束,可执行跳转逻辑
}

@Override
public void onAnimationRepeat(Animation animation) {

}
};
}
}

BaseFragment

public class TestFragment extends BaseFragment {
@Override
public int inflaterRootView() {
return R.layout.fragment_test;
}

@Override
public void initUI() {
//TODO:初始化UI
}

@Override
public void initData() {
//TODO:初始化数据(绑定数据)
}

}

BaseDialogFragment

public class TestDialogFragment extends BaseDialogFragment {
@Override
public int inflaterRootView() {
return R.layout.fragment_test_dialog;
}

@Override
public void initUI() {
//TODO:初始化UI
}

@Override
public void initData() {
//TODO:初始化数据(绑定数据)
}


}

WebFragment

    WebFragment实现基本webView功能

其他小功能

使用Log: 统一控制管理Log

 LogUtils.v();

LogUtils.d();

LogUtils.i();

LogUtils.w();

LogUtils.e();

LogUtils.twf();

LogUtils.println();

使用Toast

 showToast(CharSequence text);

showToast(@StringRes int resId);

使用Dialog

 showDialog(View v);
 showProgressDialog();

showProgressDialog(@LayoutRes int resId);

showProgressDialog(View v);

App中的源码使用示例或直接查看API帮助文档。更多实用黑科技,请速速使用Base体会吧。

代码下载:Base.zip

收起阅读 »

Android 路线规划和导航的地图帮助类库-MapHelper

MapHelperMapHelper for Android 是一个整合了高德地图、百度地图、腾讯地图、谷歌地图等相关路线规划和导航的地图帮助类库。功能介绍 简单易用,一句代码实现 地图路线规划/导航 GCJ-02 /&...
继续阅读 »


MapHelper

Image


MapHelper for Android 是一个整合了高德地图、百度地图、腾讯地图、谷歌地图等相关路线规划和导航的地图帮助类库。

功能介绍

  •  简单易用,一句代码实现
  •  地图路线规划/导航
  •  GCJ-02 / WGS-84 / BD09LL 等相关坐标系互转

Gif 展示

Image

引入

Maven:

<dependency>
<groupId>com.king.map</groupId>
<artifactId>maphelper</artifactId>
<version>1.0.0</version>
<type>pom</type>
</dependency>

Gradle:

implementation 'com.king.map:maphelper:1.0.0'

Lvy:

<dependency org='com.king.map' name='maphelper' rev='1.0.0'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

代码示例

Kotlin 示例
    //调用相关地图线路/导航示例(params表示一些具体参数)

//跳转到地图(高德、百度、腾讯、谷歌地图等)
MapHelper.gotoMap(params)
//跳转到高德地图
MapHelper.gotoAMap(params)
//跳转到百度地图
MapHelper.gotoBaiduMap(params)
//跳转腾讯地图
MapHelper.gotoTencentMap(params)
//跳转到谷歌地图
MapHelper.gotoGoogleMap(params)
//坐标系转换:WGS-84转GCJ-02(火星坐标系)
MapHelper.wgs84ToGCJ02(lat,lng)
//...更多示例详情请查看MapHelper
Java 示例
    //调用相关地图线路/导航示例(params表示一些具体参数)

//跳转到地图(高德、百度、腾讯、谷歌地图等)
MapHelper.INSTANCE.gotoMap(params);
//跳转到高德地图
MapHelper.INSTANCE.gotoAMap(params);
//跳转到百度地图
MapHelper.INSTANCE.gotoBaiduMap(params);
//跳转腾讯地图
MapHelper.INSTANCE.gotoTencentMap(params);
//跳转到谷歌地图
MapHelper.INSTANCE.gotoGoogleMap(params);
//坐标系转换:WGS-84转GCJ-02(火星坐标系)
MapHelper.INSTANCE.wgs84ToGCJ02(lat,lng);
//...更多示例详情请查看MapHelper

更多使用详情,请查看app中的源码使用示例或直接查看API帮助文

代码下载:MapHelper.zip

收起阅读 »

ArcSeekBar for Android 是一个弧形的拖动条进度控件

ArcSeekBarArcSeekBar for Android 是一个弧形的拖动条进度控件,配置参数完全可定制化。ArcSeekBar 是基于 CircleProgressView 修改而来的库。 但青出于蓝而胜于蓝,所以&nb...
继续阅读 »


ArcSeekBar

ArcSeekBar for Android 是一个弧形的拖动条进度控件,配置参数完全可定制化。

ArcSeekBar 是基于 CircleProgressView 修改而来的库。 但青出于蓝而胜于蓝,所以 CircleProgressView 的大部分用法,ArcSeekBar基本都支持,而且可配置的参数更细致。

之所以新造一个ArcSeekBar库,而不直接在CircleProgressView上面直接改,原因是CircleProgressView里面的部分动画效果对于SeekBar并不适用,所以ArcSeekBar是在CircleProgressView的基础上有所删减后,而再进行扩展增强的。 实际还需根据具体的需求而选择适合的。

Gif 展示

Image

ArcSeekBar自定义属性说明(进度默认渐变色)

属性值类型默认值说明
arcStrokeWidthdimension12dp画笔描边的宽度
arcStrokeCapenumROUND画笔的线冒样式
arcNormalColorcolor#FFC8C8C8弧形正常颜色
arcProgressColorcolor#FF4FEAAC弧形进度颜色
arcStartAngleinteger270开始角度,默认十二点钟方向
arcSweepAngleinteger360扫描角度范围
arcMaxinteger100进度最大值
arcProgressinteger0当前进度
arcDurationinteger500动画时长
arcLabelTextstring中间的标签文本,默认自动显示百分比
arcLabelTextColorcolor#FF333333文本字体颜色
arcLabelTextSizedimension30sp文本字体大小
arcLabelPaddingTopdimension0dp文本居顶边内间距
arcLabelPaddingBottomdimension0dp文本居底边内间距
arcLabelPaddingLeftdimension0dp文本居左边内间距
arcLabelPaddingRightdimension0dp文本居右边内间距
arcShowLabelbooleantrue是否显示文本
arcShowTickbooleantrue是否显示环刻度
arcTickStrokeWidthdimension10dp刻度描边宽度
arcTickPaddingdimension2dp环刻度与环间距
arcTickSplitAngleinteger5刻度间隔的角度大小
arcBlockAngleinteger1刻度的角度大小
arcThumbStrokeWidthdimension8dp拖动按钮描边宽度
arcThumbColorcolor#FFE8D30F拖动按钮颜色
arcThumbRadiusdimension10dp拖动按钮半径
arcThumbRadiusEnlargesdimension8dp触摸时按钮半径放大量
arcShowThumbbooleantrue是否显示拖动按钮
arcAllowableOffsetsdimension10dp触摸时可偏移距离:偏移量越大,触摸精度越小
arcEnabledDragbooleantrue是否启用通过拖动改变进度
arcEnabledSinglebooleantrue是否启用通过点击改变进度

引入

Maven:

<dependency>
<groupId>com.king.view</groupId>
<artifactId>arcseekbar</artifactId>
<version>1.0.2</version>
<type>pom</type>
</dependency>

Gradle:

implementation 'com.king.view:arcseekbar:1.0.2'

Lvy:

<dependency org='com.king.view' name='arcseekbar' rev='1.0.2'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

布局示例

    <com.king.view.arcseekbar.ArcSeekBar
android:id="@+id/arcSeekBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

代码示例

    //进度改变监听
arcSeekBar.setOnChangeListener(listener);
//设置进度
arcSeekBar.setProgress(progress);
//显示进度动画(进度,动画时长)
arcSeekBar.showAnimation(80,3000);

更多使用详情,请查看app中的源码使用示例

代码下载: ArcSeekBar.zip

收起阅读 »

ImageViewer for Android 是一个图片查看器

ImageViewerImageViewer for Android 是一个图片查看器,一般用来查看图片详情或查看大图时使用。引入Maven:<dependency> <groupId>com.king.image</grou...
继续阅读 »

ImageViewer

ImageViewer for Android 是一个图片查看器,一般用来查看图片详情或查看大图时使用。


引入

Maven:

<dependency>
<groupId>com.king.image</groupId>
<artifactId>imageviewer</artifactId>
<version>1.0.2</version>
<type>pom</type>
</dependency>

Gradle:

implementation 'com.king.image:imageviewer:1.0.2'

Lvy:

<dependency org='com.king.image' name='imageviewer' rev='1.0.2'>
<artifact name='$AID' ext='pom'></artifact>
</dependency>
如果Gradle出现compile失败的情况,可以在Project的build.gradle里面添加如下:(也可以使用上面的GitPack来complie)
allprojects {
repositories {
//...
maven { url 'https://dl.bintray.com/jenly/maven' }
}
}

示例

代码示例

    //图片查看器 - 简单调用

// data 可以多张图片List或单张图片,支持的类型可以是{@link Uri}, {@code url}, {@code path},{@link File}, {@link DrawableRes resId}…等
ImageViewer.load(data)//要加载的图片数据,单张或多张
.imageLoader(new GlideImageLoader())//加载器,imageLoader必须配置,目前内置的有GlideImageLoader或PicassoImageLoader,也可以自己实现
.start(activity,sharedElement);//activity or fragment, 跳转时的共享元素视图
    //图片查看器

// data 可以多张图片List或单张图片,支持的类型可以是{@link Uri}, {@code url}, {@code path},{@link File}, {@link DrawableRes resId}…等
ImageViewer.load(data)//要加载的图片数据,单张或多张
.selection(position)//当前选中位置,默认:0
.indicator(true)//是否显示指示器,默认不显示
.imageLoader(new GlideImageLoader())//加载器,imageLoader必须配置,目前内置的有GlideImageLoader或PicassoImageLoader,也可以自己实现
.theme(R.style.ImageViewerTheme)//设置主题风格,默认:R.style.ImageViewerTheme
.orientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT)//设置屏幕方向,默认:ActivityInfo.SCREEN_ORIENTATION_BEHIND
.start(activity,sharedElement);//activity or fragment, 跳转时的共享元素视图

相关说明

  • 使用 ImageViewer 时,必须配置一个实现的 ImageLoader
  • ImageViewer 一次可以查看多张图片或单张图片,支持的类型可以是 Uri、 url 、 path 、 File、 Drawable、 ImageDataSource 等
  • 目前内置默认实现的 ImageLoader 有和 PicassoImageLoader ,二者选其一即可,如果二者不满足您的需求,您也可以自己实现一个 ImageLoader
  • 为了保证 ImageViewer 体积最小化,和用户更多可能的选择性,并未将 Glide 和 Picasso 打包进 aar

当您使用了 GlideImageLoader 时,必须依赖 Glide 库。

当您使用了 PicassoImageLoader 时,必须依赖 Picasso 库。

更多使用详情,请查看app中的源码使用示例

ImageViewer.zip

收起阅读 »