From 64787d695fbeee3dafdcaebe0092040e193e0222 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tears=E4=B8=B6=E6=AE=8B=E9=98=B3?= <605658769@qq.com> Date: Wed, 4 Dec 2019 21:50:26 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=8A=A8=E6=80=81=E6=8D=A2?= =?UTF-8?q?=E8=82=A4=E5=B7=A5=E5=85=B7chameleon=EF=BC=8C=E5=AE=8C=E5=96=84?= =?UTF-8?q?=E5=B7=B2=E6=9C=89=E6=A8=A1=E5=9D=97=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 19 +- .../numeron/adapter/PagedListAdapterHelper.kt | 12 ++ .../numeron/adapter/SpaceItemDecoration.kt | 36 +++- .../wandroid/activity/ArticleListActivity.kt | 6 +- .../wandroid/activity/WeChatAuthorActivity.kt | 4 +- .../wandroid/contract/ArticleListContract.kt | 4 +- .../wandroid/contract/WeChatAuthorContract.kt | 4 +- .../com/numeron/wandroid/other/Preferences.kt | 2 +- .../java/com/numeron/brick/ApiFactory.java | 4 +- .../src/main/java/com/numeron/brick/Brick.kt | 18 +- .../java/com/numeron/brick/IRetrofit.java | 5 +- .../java/com/numeron/brick/ModelFactory.java | 37 +++- .../main/java/com/numeron/brick/ViewModel.kt | 6 +- chameleon/.gitignore | 1 + chameleon/build.gradle | 29 +++ chameleon/src/main/AndroidManifest.xml | 1 + .../java/com/numeron/chameleon/Chameleon.kt | 185 ++++++++++++++++++ .../main/java/com/numeron/util/ContextUtil.kt | 4 +- delegate/src/main/AndroidManifest.xml | 2 +- .../ActivityExtraDelegate.kt | 5 +- .../java/com/numeron/delegate/BundleKey.kt | 4 + .../{util => delegate}/BundleReader.kt | 4 +- .../delegate/FragmentArgumentsDelegate.kt | 13 ++ .../{util => delegate}/PreferencesDelegate.kt | 2 +- .../numeron/util/FragmentBundleDelegate.kt | 13 -- .../java/com/numeron/http/AbstractHttpUtil.kt | 13 -- .../com/numeron/http/FileConverterFactory.kt | 15 +- .../java/com/numeron/http/FileDownloadApi.kt | 24 --- .../numeron/http/FileDownloadInterceptor.kt | 103 ---------- .../java/com/numeron/http/FileResponseBody.kt | 4 +- .../com/numeron/http/ProgressInterceptor.kt | 130 ++++++++++++ .../com/numeron/http/ProgressResponseBody.kt | 32 +++ http/src/main/java/com/numeron/http/Util.kt | 87 +------- .../src/main/java/com/numeron/result/Exp.kt | 7 + settings.gradle | 2 +- .../main/java/com/numeron/starter/Intents.kt | 5 + .../main/java/com/numeron/starter/Starter.kt | 32 ++- .../java/com/numeron/view/StatefulLayout.kt | 102 +++++----- .../java/com/numeron/view/StatusObservers.kt | 4 +- ...layout.xml => anim_state_layout_enter.xml} | 0 ..._layout.xml => anim_state_layout_exit.xml} | 0 .../main/res/layout/state_empty_layout.xml | 4 +- ...lure_layout.xml => state_error_layout.xml} | 4 +- .../main/res/layout/state_loading_layout.xml | 4 +- stateful-layout/src/main/res/values/attrs.xml | 2 +- .../src/main/res/values/strings.xml | 6 +- stateful-livedata/build.gradle | 1 + .../src/main/AndroidManifest.xml | 2 +- .../main/java/com/numeron/status/Observers.kt | 33 ---- .../src/main/java/com/numeron/status/State.kt | 18 +- .../com/numeron/status/StatefulCallback.kt | 11 ++ .../status/StatefulExceptionHandler.kt | 16 ++ .../com/numeron/status/StatefulLiveData.kt | 29 ++- .../com/numeron/status/StatefulObserver.kt | 27 +++ .../main/java/com/numeron/status/Transform.kt | 12 +- 55 files changed, 720 insertions(+), 429 deletions(-) create mode 100644 adapter/src/main/java/com/numeron/adapter/PagedListAdapterHelper.kt create mode 100644 chameleon/.gitignore create mode 100644 chameleon/build.gradle create mode 100644 chameleon/src/main/AndroidManifest.xml create mode 100644 chameleon/src/main/java/com/numeron/chameleon/Chameleon.kt rename delegate/src/main/java/com/numeron/{util => delegate}/ActivityExtraDelegate.kt (78%) create mode 100644 delegate/src/main/java/com/numeron/delegate/BundleKey.kt rename delegate/src/main/java/com/numeron/{util => delegate}/BundleReader.kt (94%) create mode 100644 delegate/src/main/java/com/numeron/delegate/FragmentArgumentsDelegate.kt rename delegate/src/main/java/com/numeron/{util => delegate}/PreferencesDelegate.kt (98%) delete mode 100644 delegate/src/main/java/com/numeron/util/FragmentBundleDelegate.kt delete mode 100644 http/src/main/java/com/numeron/http/FileDownloadApi.kt delete mode 100644 http/src/main/java/com/numeron/http/FileDownloadInterceptor.kt create mode 100644 http/src/main/java/com/numeron/http/ProgressInterceptor.kt create mode 100644 http/src/main/java/com/numeron/http/ProgressResponseBody.kt rename stateful-layout/src/main/res/anim/{default_enter_state_layout.xml => anim_state_layout_enter.xml} (100%) rename stateful-layout/src/main/res/anim/{default_exit_state_layout.xml => anim_state_layout_exit.xml} (100%) rename stateful-layout/src/main/res/layout/{state_failure_layout.xml => state_error_layout.xml} (72%) delete mode 100644 stateful-livedata/src/main/java/com/numeron/status/Observers.kt create mode 100644 stateful-livedata/src/main/java/com/numeron/status/StatefulCallback.kt create mode 100644 stateful-livedata/src/main/java/com/numeron/status/StatefulExceptionHandler.kt create mode 100644 stateful-livedata/src/main/java/com/numeron/status/StatefulObserver.kt diff --git a/README.md b/README.md index 5f2e370..2d48c6b 100644 --- a/README.md +++ b/README.md @@ -54,12 +54,13 @@ allprojects { 模块|依赖 ---|--- -adapter|implementation 'com.github.xiazunyang.brick:adapter:1.4.0' -brick|implementation 'com.github.xiazunyang.brick:brick:1.4.0' -context-util|implementation 'com.github.xiazunyang.brick:context-util:1.4.0' -delegate|implementation 'com.github.xiazunyang.brick:delegate:1.4.0' -http|implementation 'com.github.xiazunyang.brick:http:1.4.0' -result|implementation 'com.github.xiazunyang.brick:result:1.4.0' -rx|implementation 'com.github.xiazunyang.brick:rx:1.4.0' -stateful-layout|implementation 'com.github.xiazunyang.brick:stateful-layout:1.4.0' -stateful-livedata|implementation 'com.github.xiazunyang.brick:stateful-livedata:1.4.0' +adapter|implementation 'com.github.xiazunyang.brick:adapter:1.4.1' +brick|implementation 'com.github.xiazunyang.brick:brick:1.4.1' +chameleon|implementation 'com.github.xiazunyang.brick:chameleon:1.4.1' +context-util|implementation 'com.github.xiazunyang.brick:context-util:1.4.1' +delegate|implementation 'com.github.xiazunyang.brick:delegate:1.4.1' +http|implementation 'com.github.xiazunyang.brick:http:1.4.1' +result|implementation 'com.github.xiazunyang.brick:result:1.4.1' +rx|implementation 'com.github.xiazunyang.brick:rx:1.4.1' +stateful-layout|implementation 'com.github.xiazunyang.brick:stateful-layout:1.4.1' +stateful-livedata|implementation 'com.github.xiazunyang.brick:stateful-livedata:1.4.1' diff --git a/adapter/src/main/java/com/numeron/adapter/PagedListAdapterHelper.kt b/adapter/src/main/java/com/numeron/adapter/PagedListAdapterHelper.kt new file mode 100644 index 0000000..ddc1fc5 --- /dev/null +++ b/adapter/src/main/java/com/numeron/adapter/PagedListAdapterHelper.kt @@ -0,0 +1,12 @@ +package com.numeron.adapter + +import androidx.paging.AsyncPagedListDiffer +import androidx.paging.PagedListAdapter +import androidx.recyclerview.widget.RecyclerView + +@Suppress("UNCHECKED_CAST") +fun PagedListAdapter.getDiffer(): AsyncPagedListDiffer { + val mDifferField = PagedListAdapter::class.java.getDeclaredField("mDiffer") + mDifferField.isAccessible = true + return mDifferField.get(this) as AsyncPagedListDiffer +} \ No newline at end of file diff --git a/adapter/src/main/java/com/numeron/adapter/SpaceItemDecoration.kt b/adapter/src/main/java/com/numeron/adapter/SpaceItemDecoration.kt index bfe2a27..642342b 100644 --- a/adapter/src/main/java/com/numeron/adapter/SpaceItemDecoration.kt +++ b/adapter/src/main/java/com/numeron/adapter/SpaceItemDecoration.kt @@ -31,18 +31,38 @@ class SpaceItemDecoration( } private fun setOutRectByGrid(rect: Rect, view: View, parent: RecyclerView) { + //获取当前View的位置 val position = parent.getChildAdapterPosition(view) - headerCount - val spanCount = getSpanCount(parent) - val column = position % spanCount - rect.left = space - rect.bottom = space - rect.top = if(position < spanCount) space else 0 - rect.right = if(column == spanCount - 1) space else 0 + //获取有多少个View + val itemCount = parent.adapter?.itemCount ?: 0 + //获取有多少列 + val columnCount = getSpanCount(parent) + //获取有多少行 + val rowCount = getRowNumber(itemCount, columnCount) + //当前是第几列 + val column = position % columnCount + //当前是第几行 + val row = getRowNumber(position + 1, columnCount) + //计算平均间隔 + val average = space / columnCount + rect.left = (columnCount - column) * average + rect.top = space + rect.right = (column + 1) * average + rect.bottom = if (row == rowCount) space else 0 + } + + private fun getRowNumber(countOrPosition: Int, spanCount: Int): Int { + return countOrPosition / spanCount + if (countOrPosition % spanCount > 0) 1 else 0 + } + + private fun getLinearLayoutOrientation(parent: RecyclerView): Int { + return (parent.layoutManager as LinearLayoutManager).orientation } private fun setOutRectByLinear(rect: Rect, view: View, parent: RecyclerView) { - rect.top = if (parent.getChildLayoutPosition(view) - headerCount == 0) space else 0 - rect.left = space + val orientation = getLinearLayoutOrientation(parent) + rect.top = if (orientation == LinearLayoutManager.VERTICAL && parent.getChildLayoutPosition(view) - headerCount == 0) space else 0 + rect.left = if (orientation == LinearLayoutManager.HORIZONTAL && parent.getChildLayoutPosition(view) - headerCount == 0) space else 0 rect.right = space rect.bottom = space } diff --git a/app/src/main/java/com/numeron/wandroid/activity/ArticleListActivity.kt b/app/src/main/java/com/numeron/wandroid/activity/ArticleListActivity.kt index 40ddef6..7705840 100644 --- a/app/src/main/java/com/numeron/wandroid/activity/ArticleListActivity.kt +++ b/app/src/main/java/com/numeron/wandroid/activity/ArticleListActivity.kt @@ -10,12 +10,12 @@ import com.numeron.adapter.BindingHolder import com.numeron.adapter.PagedBindingAdapter import com.numeron.adapter.SpaceItemDecoration import com.numeron.brick.createViewModel +import com.numeron.delegate.ActivityExtraDelegate import com.numeron.starter.startActivity -import com.numeron.util.ActivityExtraDelegate import com.numeron.util.dp +import com.numeron.view.StatefulLayoutMessageObserver import com.numeron.wandroid.entity.db.Article import com.numeron.wandroid.other.* -import com.numeron.view.StatefulMessageObserver import com.numeron.wandroid.R import com.numeron.wandroid.contract.ArticleListParamProvider import com.numeron.wandroid.contract.ArticleListViewModel @@ -49,7 +49,7 @@ class ArticleListActivity : AppCompatActivity(), ArticleListParamProvider { //替换默认的加载动画 articleListStatusLayout.setLoadingOperation(articleListRefreshLayout::setRefreshing) articleListViewModel.articleListLiveData.observe(this, Observer(adapter::submitList)) - articleListViewModel.loadStateLiveData.observe(this, StatefulMessageObserver(articleListStatusLayout)) + articleListViewModel.loadStateLiveData.observe(this, StatefulLayoutMessageObserver(articleListStatusLayout)) } private inner class ArticleAdapter : PagedBindingAdapter() + private val articleRepository = stack() private val pagedConfig = PagedList.Config.Builder() .setPageSize(20) diff --git a/app/src/main/java/com/numeron/wandroid/contract/WeChatAuthorContract.kt b/app/src/main/java/com/numeron/wandroid/contract/WeChatAuthorContract.kt index 9c3079d..72d0285 100644 --- a/app/src/main/java/com/numeron/wandroid/contract/WeChatAuthorContract.kt +++ b/app/src/main/java/com/numeron/wandroid/contract/WeChatAuthorContract.kt @@ -4,7 +4,7 @@ import androidx.lifecycle.MutableLiveData import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList import com.numeron.brick.ViewModel -import com.numeron.brick.createModel +import com.numeron.brick.stack import com.numeron.wandroid.dao.WeChatAuthorDao import com.numeron.wandroid.entity.ApiResponse import com.numeron.wandroid.entity.db.WeChatAuthor @@ -16,7 +16,7 @@ import retrofit2.http.GET class WeChatAuthorViewModel : ViewModel() { - private val weChatAuthorRepository = createModel() + private val weChatAuthorRepository = stack() val loadStatusLiveData = MutableLiveData>() val weChatAuthorLiveData = diff --git a/app/src/main/java/com/numeron/wandroid/other/Preferences.kt b/app/src/main/java/com/numeron/wandroid/other/Preferences.kt index ada08c6..47e3cba 100644 --- a/app/src/main/java/com/numeron/wandroid/other/Preferences.kt +++ b/app/src/main/java/com/numeron/wandroid/other/Preferences.kt @@ -2,7 +2,7 @@ package com.numeron.wandroid.other -import com.numeron.util.PreferencesDelegate +import com.numeron.delegate.PreferencesDelegate var userId: Long by PreferencesDelegate(preferences, 0) diff --git a/brick/src/main/java/com/numeron/brick/ApiFactory.java b/brick/src/main/java/com/numeron/brick/ApiFactory.java index 4a2894c..4dc1185 100644 --- a/brick/src/main/java/com/numeron/brick/ApiFactory.java +++ b/brick/src/main/java/com/numeron/brick/ApiFactory.java @@ -1,5 +1,7 @@ package com.numeron.brick; +import org.jetbrains.annotations.NotNull; + import java.lang.reflect.Method; class ApiFactory implements IRetrofit { @@ -33,7 +35,7 @@ static ApiFactory create(Object retrofit) { @Override @SuppressWarnings("unchecked") - public T create(Class clazz) { + public T create(@NotNull Class clazz) { //尝试通过反射来创建Retrofit Api if (isInitialized()) { try { diff --git a/brick/src/main/java/com/numeron/brick/Brick.kt b/brick/src/main/java/com/numeron/brick/Brick.kt index 53d3d0c..505c9ae 100644 --- a/brick/src/main/java/com/numeron/brick/Brick.kt +++ b/brick/src/main/java/com/numeron/brick/Brick.kt @@ -4,6 +4,7 @@ package com.numeron.brick import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStoreOwner @@ -15,6 +16,13 @@ fun install(retrofit: Any, room: Any? = null) { ModelFactory.install(retrofit, room) } +/** + * 当使用多个数据库时,请在初始化前,使用此方法追加其它数据库 + */ +fun addRoom(vararg room: Any) { + ModelFactory.addRoom(*room) +} + /** * 供外部创建Model的实例的工厂方法 @@ -24,7 +32,7 @@ fun install(retrofit: Any, room: Any? = null) { * @return Model 实例 */ @JvmOverloads -fun createModel(clazz: Class, iRetrofit: Any? = null): M { +fun stack(clazz: Class, iRetrofit: Any? = null): M { return ModelFactory.create(clazz, iRetrofit) } @@ -34,7 +42,13 @@ fun createModel(clazz: Class, iRetrofit: Any? = null): M { * @param iRetrofit Any? 创建Model的实例时,用于创建Retrofit Api实例的Retrofit或其它工具类 * @return M Model的实例 */ -inline fun createModel(iRetrofit: Any? = null) = createModel(M::class.java, iRetrofit) +inline fun stack(iRetrofit: Any? = null) = stack(M::class.java, iRetrofit) + +@JvmOverloads +fun createViewModel(clazz: Class, store: ViewModelStore, factory: ViewModelProvider.Factory = ViewModelFactory()): VM { + return ViewModelProvider(store, factory).get(clazz) +} + /** * 创建ViewModel的工厂方法 diff --git a/brick/src/main/java/com/numeron/brick/IRetrofit.java b/brick/src/main/java/com/numeron/brick/IRetrofit.java index 701db4d..ac1b870 100644 --- a/brick/src/main/java/com/numeron/brick/IRetrofit.java +++ b/brick/src/main/java/com/numeron/brick/IRetrofit.java @@ -1,5 +1,7 @@ package com.numeron.brick; +import androidx.annotation.NonNull; + /** * Retrofit中并没有提供相应的接口,所以此接口是对Retrofit中的create方法的代理 * 当项目中使用了二次封装的Retrofit工具类,让工具类实现此接口,在实现方法中调用Retrofit实际的create方法 @@ -8,8 +10,9 @@ * * @see retrofit2.Retrofit#create(Class) */ +@SuppressWarnings("JavadocReference") public interface IRetrofit { - T create(Class clazz); + T create(@NonNull Class clazz); } \ No newline at end of file diff --git a/brick/src/main/java/com/numeron/brick/ModelFactory.java b/brick/src/main/java/com/numeron/brick/ModelFactory.java index 789a286..84cd27e 100644 --- a/brick/src/main/java/com/numeron/brick/ModelFactory.java +++ b/brick/src/main/java/com/numeron/brick/ModelFactory.java @@ -1,6 +1,8 @@ package com.numeron.brick; import java.lang.reflect.Constructor; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.List; @@ -8,13 +10,13 @@ public final class ModelFactory { private final ApiFactory apiFactory; - private final DaoFactory daoFactory; + private final List daoFactories = new ArrayList<>(); - private static ModelFactory modelFactory = new ModelFactory(null, null); + private static ModelFactory modelFactory = new ModelFactory(null, Collections.EMPTY_LIST); - private ModelFactory(ApiFactory apiFactory, DaoFactory daoFactory) { + private ModelFactory(ApiFactory apiFactory, List daoFactories) { this.apiFactory = apiFactory; - this.daoFactory = daoFactory; + this.daoFactories.addAll(daoFactories); } /** @@ -26,10 +28,21 @@ private ModelFactory(ApiFactory apiFactory, DaoFactory daoFactory) { * @see retrofit2.Retrofit#create(Class) */ @SuppressWarnings("JavadocReference") - static void install(Object retrofit, Object room) { + static void install(Object retrofit, Object... room) { ApiFactory apiFactory = ApiFactory.create(retrofit); - DaoFactory daoFactory = DaoFactory.create(room); - modelFactory = new ModelFactory(apiFactory, daoFactory); + modelFactory = new ModelFactory(apiFactory, transfer(room)); + } + + static void addRoom(Object... rooms) { + modelFactory.daoFactories.addAll(transfer(rooms)); + } + + private static List transfer(Object... rooms) { + ArrayList list = new ArrayList<>(); + for (Object room : rooms) { + list.add(DaoFactory.create(room)); + } + return list; } /** @@ -79,8 +92,14 @@ private Object createInstance(Class clazz, IRetrofit iRetrofit) { //如果接口继承了某一个接口,则说明是RoomDao接口 boolean hasParentInterface = clazz.getInterfaces().length > 0; if (!hasAnnotation || hasParentInterface) { - if (daoFactory == null) throw new RuntimeException("Brick初始化时没有传入Room实例!"); - return daoFactory.getDao(clazz); + if (daoFactories.isEmpty()) throw new RuntimeException("Brick初始化时没有传入Room实例!"); + for (DaoFactory daoFactory : daoFactories) { + try { + return daoFactory.getDao(clazz); + } catch (Exception ignored) { + } + } + throw new RuntimeException("所有的Room实例中都没有返回值为" + clazz + "的方法!"); } else { if (iRetrofit != null) { return iRetrofit.create(clazz); diff --git a/brick/src/main/java/com/numeron/brick/ViewModel.kt b/brick/src/main/java/com/numeron/brick/ViewModel.kt index 60f18d8..d5ef101 100644 --- a/brick/src/main/java/com/numeron/brick/ViewModel.kt +++ b/brick/src/main/java/com/numeron/brick/ViewModel.kt @@ -2,10 +2,8 @@ package com.numeron.brick import kotlinx.coroutines.* -/** - * 拥有一个Dispatchers.IO的协程域 - */ -abstract class ViewModel : androidx.lifecycle.ViewModel(), CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.IO) { +abstract class ViewModel : androidx.lifecycle.ViewModel(), + CoroutineScope by CoroutineScope(SupervisorJob() + Dispatchers.IO) { override fun onCleared() { cancel() diff --git a/chameleon/.gitignore b/chameleon/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/chameleon/.gitignore @@ -0,0 +1 @@ +/build diff --git a/chameleon/build.gradle b/chameleon/build.gradle new file mode 100644 index 0000000..8381efa --- /dev/null +++ b/chameleon/build.gradle @@ -0,0 +1,29 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' +apply plugin: 'com.github.dcendents.android-maven' + +group = 'com.github.xiazunyang' +version = '0.1.0' + +android { + compileSdkVersion 29 + + defaultConfig { + minSdkVersion 21 + targetSdkVersion 29 + versionCode 1 + versionName "0.0.1" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + +} diff --git a/chameleon/src/main/AndroidManifest.xml b/chameleon/src/main/AndroidManifest.xml new file mode 100644 index 0000000..0cddb7f --- /dev/null +++ b/chameleon/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/chameleon/src/main/java/com/numeron/chameleon/Chameleon.kt b/chameleon/src/main/java/com/numeron/chameleon/Chameleon.kt new file mode 100644 index 0000000..6137f0c --- /dev/null +++ b/chameleon/src/main/java/com/numeron/chameleon/Chameleon.kt @@ -0,0 +1,185 @@ +package com.numeron.chameleon + +import android.app.Activity +import android.content.Context +import android.content.res.ColorStateList +import android.util.SparseArray +import android.util.SparseIntArray +import android.util.TypedValue +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import android.widget.TextView + +typealias ColorProcessor = (View, Int) -> Unit + +/** + * 应用当前主题颜色,使用此工具修改主题可立即生效,无需重新创建Activity, + * 也不会出现闪屏的现象,非常轻量级,侵入性和接入成本极低,推荐没有加载外置皮肤需求的项目使用! + * @property themeId Int 主题ID,应该是R.style.xxx这样的格式 + */ +open class Chameleon(context: Context, private val themeId: Int) { + + private val theme = context.resources.newTheme() + + init { + theme.applyStyle(themeId, true) + } + + private val textColors = SparseIntArray() + private val tintColors = SparseIntArray() + private val backgroundColors = SparseIntArray() + private val backgroundTintColors = SparseIntArray() + private val reflectionColors = SparseArray>() + private val processorColors = SparseArray>() + + private fun getColor(attrId: Int): Int { + val value = TypedValue() + theme.resolveAttribute(attrId, value, true) + return value.data + } + + /** + * 使用着色器修改ImageView的显示颜色 + * @param imageViewId Int 要修改颜色的ImageView的Id + * @param attrId Int 要修改的颜色的attr属性 + * @return Chameleon + */ + fun setTintColor(imageViewId: Int, attrId: Int): Chameleon { + tintColors.put(imageViewId, getColor(attrId)) + return this + } + + /** + * 设置TextView的字体颜色 + * @param textViewId Int 要修改颜色的TextView的Id + * @param attrId Int 要修改的颜色的attr属性 + */ + fun setTextColor(textViewId: Int, attrId: Int): Chameleon { + textColors.put(textViewId, getColor(attrId)) + return this + } + + /** + * 设置View的background的颜色 + * @param viewId Int 要修改背景颜色的View的Id + * @param attrId Int 颜色的attr属性 + */ + fun setBackgroundColor(viewId: Int, attrId: Int): Chameleon { + backgroundColors.put(viewId, getColor(attrId)) + return this + } + + /** + * 使用着色器修改View的background的颜色 + * @param viewId Int 要修改背景颜色的View的Id + * @param attrId Int 颜色的attr属性 + */ + fun setBackgroundTintColor(viewId: Int, attrId: Int): Chameleon { + backgroundTintColors.put(viewId, getColor(attrId)) + return this + } + + /** + * 使用反射修改View的颜色,用于其它方法无法满足需求时使用,比如要修改CardView的背景颜色。 + * @param viewId Int 要修改颜色的View的Id + * @param methodName String 方法名称,区分大小写 + * @param attrId Int 颜色的attr属性 + */ + fun setColorByReflection(viewId: Int, methodName: String, attrId: Int): Chameleon { + reflectionColors.put(viewId, methodName to getColor(attrId)) + return this + } + + /** + * 当某些View修改颜色的方法接收的不是Int类型时,可用此方法进行转换。 + * @param viewId Int 要修改颜色的View的Id + * @param attrId Int 颜色的attr属性 + * @param processor Function2 自定义的颜色处理器 + */ + fun setColorByProcessor(viewId: Int, attrId: Int, processor: ColorProcessor): Chameleon { + processorColors.put(viewId, getColor(attrId) to processor) + return this + } + + /** + * 仅在无法获取到Activity时,才使用此方法来应用修改。 + * @param view View 传入当前界面中任意的一个View对象,不能是Dialog中的View。 + */ + fun apply(view: View) { + view.context.currentThemeId = themeId + eachView(view.rootView) + } + + /** + * 调用此方法从Activity的根视图开始遍历View Tree,在此过程中根据 View Id来动态修改View的颜色。 + * @param activity Activity 要修改主题颜色的Activity + * @param statusBarColor Int? 如果要同时修改状态栏的颜色,则需要传入一个非null的值 + */ + @JvmOverloads + fun apply(activity: Activity, statusBarColor: Int? = null) { + val window = activity.window + if (statusBarColor != null) { + window.statusBarColor = getColor(statusBarColor) + } + apply(window.decorView) + } + + private fun nonzero(int: Int) = int != 0 + + private fun eachView(view: View) { + val viewId = view.id + //用着色器修改ImageView的颜色 + tintColors[viewId].takeIf(::nonzero)?.let { + (view as ImageView).imageTintList = ColorStateList.valueOf(it) + } + //修改TextView的字体颜色 + textColors[viewId].takeIf(::nonzero)?.let((view as TextView)::setTextColor) + //修改View的背景颜色 + backgroundColors[viewId].takeIf(::nonzero)?.let(view::setBackgroundColor) + //使用着色器修改背景颜色 + backgroundTintColors[viewId].takeIf(::nonzero)?.let { + view.backgroundTintList = ColorStateList.valueOf(it) + } + //用反射修改以上方法无法满足的颜色 + reflectionColors[viewId]?.let { (methodName, color) -> + view.javaClass.getMethod(methodName, Int::class.java).invoke(view, color) + } + //自定义处理器,用于非颜色参数的修改,比如ColorStateList + processorColors[viewId]?.let { (color, processor) -> + processor(view, color) + } + //遍历子View + if (view is ViewGroup) { + repeat(view.childCount) { + eachView(view.getChildAt(it)) + } + } + } + + companion object { + + private const val SHARED_PREFERENCES_NAME = "chameleon" + private const val SHARED_PREFERENCES_KEY = "current_theme_id" + + /** + * 在项目的Activity基类中重写setContent方法,并在调用super方法之前,调用此静态方法。 + * @param activity Activity Activity基类 + */ + @JvmStatic + fun setTheme(activity: Activity) { + activity.setTheme(activity.currentThemeId) + } + + private var Context.currentThemeId: Int + get() = getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + .getInt(SHARED_PREFERENCES_KEY, 0) + set(value) { + getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE) + .edit() + .putInt(SHARED_PREFERENCES_KEY, value) + .apply() + } + } + +} \ No newline at end of file diff --git a/context-util/src/main/java/com/numeron/util/ContextUtil.kt b/context-util/src/main/java/com/numeron/util/ContextUtil.kt index 4915bd8..95ed182 100644 --- a/context-util/src/main/java/com/numeron/util/ContextUtil.kt +++ b/context-util/src/main/java/com/numeron/util/ContextUtil.kt @@ -65,8 +65,8 @@ val Float.sp: Float get() = scaledDensity * this + 0.5f /* from px to sp */ -val Int.toSp: Int - get() = (this / scaledDensity + 0.5f).toInt() +val Int.toSp: Float + get() = this / scaledDensity + 0.5f val Float.toSp: Float get() = this / scaledDensity + 0.5f diff --git a/delegate/src/main/AndroidManifest.xml b/delegate/src/main/AndroidManifest.xml index cadcf55..94fa194 100644 --- a/delegate/src/main/AndroidManifest.xml +++ b/delegate/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/delegate/src/main/java/com/numeron/util/ActivityExtraDelegate.kt b/delegate/src/main/java/com/numeron/delegate/ActivityExtraDelegate.kt similarity index 78% rename from delegate/src/main/java/com/numeron/util/ActivityExtraDelegate.kt rename to delegate/src/main/java/com/numeron/delegate/ActivityExtraDelegate.kt index aba2735..f5bbe06 100644 --- a/delegate/src/main/java/com/numeron/util/ActivityExtraDelegate.kt +++ b/delegate/src/main/java/com/numeron/delegate/ActivityExtraDelegate.kt @@ -1,9 +1,6 @@ -package com.numeron.util +package com.numeron.delegate import android.app.Activity -import android.os.Bundle -import android.os.Parcelable -import java.io.Serializable import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty diff --git a/delegate/src/main/java/com/numeron/delegate/BundleKey.kt b/delegate/src/main/java/com/numeron/delegate/BundleKey.kt new file mode 100644 index 0000000..f6fd5e1 --- /dev/null +++ b/delegate/src/main/java/com/numeron/delegate/BundleKey.kt @@ -0,0 +1,4 @@ +package com.numeron.delegate + +@Target(AnnotationTarget.PROPERTY) +annotation class BundleKey(val value: String) \ No newline at end of file diff --git a/delegate/src/main/java/com/numeron/util/BundleReader.kt b/delegate/src/main/java/com/numeron/delegate/BundleReader.kt similarity index 94% rename from delegate/src/main/java/com/numeron/util/BundleReader.kt rename to delegate/src/main/java/com/numeron/delegate/BundleReader.kt index ad1bfd0..e32840b 100644 --- a/delegate/src/main/java/com/numeron/util/BundleReader.kt +++ b/delegate/src/main/java/com/numeron/delegate/BundleReader.kt @@ -1,14 +1,16 @@ @file:JvmName("BundleReader") @file:Suppress("UNCHECKED_CAST") -package com.numeron.util +package com.numeron.delegate import android.os.Bundle import android.os.Parcelable +import android.util.Log import java.io.Serializable internal fun Bundle.read(key: String, defaultValue: T): T { + Log.e("Bundle.read", defaultValue.toString()) return when (defaultValue) { //基本类型 is Int -> getInt(key, defaultValue) as T diff --git a/delegate/src/main/java/com/numeron/delegate/FragmentArgumentsDelegate.kt b/delegate/src/main/java/com/numeron/delegate/FragmentArgumentsDelegate.kt new file mode 100644 index 0000000..f9a945d --- /dev/null +++ b/delegate/src/main/java/com/numeron/delegate/FragmentArgumentsDelegate.kt @@ -0,0 +1,13 @@ +package com.numeron.delegate + +import androidx.fragment.app.Fragment +import kotlin.properties.ReadOnlyProperty +import kotlin.reflect.KProperty + + +@Suppress("UNCHECKED_CAST") +class FragmentArgumentsDelegate(private val defaultValue: T, private val key: String?) : ReadOnlyProperty { + override fun getValue(thisRef: Fragment, property: KProperty<*>): T { + return thisRef.arguments!!.read(key ?: property.name, defaultValue) + } +} \ No newline at end of file diff --git a/delegate/src/main/java/com/numeron/util/PreferencesDelegate.kt b/delegate/src/main/java/com/numeron/delegate/PreferencesDelegate.kt similarity index 98% rename from delegate/src/main/java/com/numeron/util/PreferencesDelegate.kt rename to delegate/src/main/java/com/numeron/delegate/PreferencesDelegate.kt index f47cdb3..016c730 100644 --- a/delegate/src/main/java/com/numeron/util/PreferencesDelegate.kt +++ b/delegate/src/main/java/com/numeron/delegate/PreferencesDelegate.kt @@ -1,4 +1,4 @@ -package com.numeron.util +package com.numeron.delegate import android.annotation.SuppressLint import android.content.SharedPreferences diff --git a/delegate/src/main/java/com/numeron/util/FragmentBundleDelegate.kt b/delegate/src/main/java/com/numeron/util/FragmentBundleDelegate.kt deleted file mode 100644 index 4142da6..0000000 --- a/delegate/src/main/java/com/numeron/util/FragmentBundleDelegate.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.numeron.util - -import androidx.fragment.app.Fragment -import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.KProperty - - -@Suppress("UNCHECKED_CAST") -class FragmentBundleDelegate(private val defaultValue: T) : ReadOnlyProperty { - override fun getValue(thisRef: Fragment, property: KProperty<*>): T { - return thisRef.arguments!!.read(property.name, defaultValue) - } -} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/AbstractHttpUtil.kt b/http/src/main/java/com/numeron/http/AbstractHttpUtil.kt index 2f370b2..cfabed8 100644 --- a/http/src/main/java/com/numeron/http/AbstractHttpUtil.kt +++ b/http/src/main/java/com/numeron/http/AbstractHttpUtil.kt @@ -2,7 +2,6 @@ package com.numeron.http import okhttp3.Interceptor import okhttp3.OkHttpClient -import okhttp3.logging.HttpLoggingInterceptor import retrofit2.CallAdapter import retrofit2.Converter import retrofit2.Retrofit @@ -62,25 +61,16 @@ abstract class AbstractHttpUtil { */ protected open val certificates: Array = emptyArray() - protected val httpLoggingInterceptor = HttpLoggingInterceptor() - - protected val loggingLevel = HttpLoggingInterceptor.Level.BODY - val retrofit by lazy(LazyThreadSafetyMode.SYNCHRONIZED, ::createRetrofit) val okHttpClient by lazy(LazyThreadSafetyMode.SYNCHRONIZED, ::createOkHttpClient) - init { - httpLoggingInterceptor.level = loggingLevel - } - /** * 默认的构建Retrofit的方法,若无法满足需求,请重写此方法 */ protected open fun createRetrofit(): Retrofit { return Retrofit.Builder() .baseUrl(baseUrl) - .addConverterFactory(FileConverterFactory()) .apply { convertersFactories.map(::addConverterFactory) callAdapterFactories.map(::addCallAdapterFactory) @@ -106,8 +96,6 @@ abstract class AbstractHttpUtil { .writeTimeout(timeout, TimeUnit.SECONDS) .connectTimeout(timeout, TimeUnit.SECONDS) .addInterceptor(AddHeaderInterceptor(header)) - .addInterceptor(FileDownloadInterceptor()) - .addInterceptor(httpLoggingInterceptor) .apply { interceptors.map(::addInterceptor) } @@ -121,7 +109,6 @@ abstract class AbstractHttpUtil { return okHttpClient.newBuilder().also(build).build() } - @Suppress("DEPRECATION") private fun OkHttpClient.Builder.installHttpsCertificates(): OkHttpClient.Builder { if (certificates.isEmpty()) return this val x509TrustManager: X509TrustManager = prepareTrustManager(*certificates).fetch() diff --git a/http/src/main/java/com/numeron/http/FileConverterFactory.kt b/http/src/main/java/com/numeron/http/FileConverterFactory.kt index baf5f1a..fca009e 100644 --- a/http/src/main/java/com/numeron/http/FileConverterFactory.kt +++ b/http/src/main/java/com/numeron/http/FileConverterFactory.kt @@ -6,7 +6,7 @@ import retrofit2.Retrofit import java.io.File import java.lang.reflect.Type -internal class FileConverterFactory : Converter.Factory() { +class FileConverterFactory : Converter.Factory() { override fun responseBodyConverter(type: Type, annotations: Array, retrofit: Retrofit): Converter? { if (type == File::class.java) { @@ -22,10 +22,19 @@ internal class FileConverterFactory : Converter.Factory() { } fun getFile(responseBody: ResponseBody): File { - return responseBody.toFile() + return if (responseBody is FileResponseBody) { + responseBody.file + } else try { + val field = responseBody.javaClass.getDeclaredField("delegate") + field.isAccessible = true + val delegate = field.get(responseBody) as ResponseBody + getFile(delegate) + } catch (throwable: Throwable) { + throw RuntimeException("响应体中没有记录文件信息!或者没有使用Tag标记File类型的参数!", throwable) + } } } -} +} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/FileDownloadApi.kt b/http/src/main/java/com/numeron/http/FileDownloadApi.kt deleted file mode 100644 index 79e8c35..0000000 --- a/http/src/main/java/com/numeron/http/FileDownloadApi.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.numeron.http - -import retrofit2.http.GET -import retrofit2.http.Streaming -import retrofit2.http.Tag -import retrofit2.http.Url -import java.io.File - - -interface FileDownloadApi { - - /** - * 下载文件专用的Retrofit Api,可以断点续传。 - * @param url String 要下载的文件的网络地址 - * @param path File 文件要保存到的目录,只能是文件夹 - * @param callback ProgressCallback? 进度回调接口 - * @return Response 下载完成的响应体,通过getFile扩展方法来获取下载后的文件 - * @return File 下载完成的文件 - */ - @GET - @Streaming - suspend fun download(@Url url: String, @Tag path: File, @Tag callback: ProgressCallback? = null): File - -} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/FileDownloadInterceptor.kt b/http/src/main/java/com/numeron/http/FileDownloadInterceptor.kt deleted file mode 100644 index 789bc11..0000000 --- a/http/src/main/java/com/numeron/http/FileDownloadInterceptor.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.numeron.http - -import okhttp3.Interceptor -import okhttp3.Response -import java.io.File -import java.io.FileNotFoundException -import java.io.RandomAccessFile -import java.net.HttpURLConnection -import java.net.URL - - -internal class FileDownloadInterceptor : Interceptor { - - override fun intercept(chain: Interceptor.Chain): Response { - //获取原始请求 - val originRequest = chain.request() - //获取存放目录 - val fileOrPath = originRequest.tag() - //如果这个请求的Tag包含一个文件类型,则判断为下载文件 - if (fileOrPath != null) { - //获取请求的url - val httpUrl = originRequest.url - val url = httpUrl.toString() - //构建HEAD请求 - val headRequest = originRequest.newBuilder().head().build() - //获取响应 - val headResponse = chain.proceed(headRequest) - //获取响应头 - val headers = headResponse.headers - //获取文件大小 - val fileSize = headers.getFileSize() - //获取指定的文件名 - val specifiedFileName = originRequest.tag() - if (specifiedFileName != null && !specifiedFileName.contains('.')) { - throw RuntimeException("请指定一个有扩展名的文件!") - } - //取出进度回调 - val progressCallback = originRequest.tag() - //判断提供的File是文件还是文件夹,如果没有指定文件名字并且File有扩展名,则判断为文件 - val file = if (specifiedFileName == null && fileOrPath.extension.isNotEmpty()) fileOrPath else { - //否则在File文件夹下创建一个新文件 - val fileName = specifiedFileName - ?: headers.getFileName() ?: getFileNameByUrl(httpUrl) - File(fileOrPath, fileName) - } - //获取文件类型,从响应头中取类型,如果没有,则通过扩展名来推断 - val fileType = headers.getFileType() ?: getMimeType(file.extension) - //检测或创建存放目录 - val parentFile: File? = file.parentFile - if (parentFile != null) { - if (parentFile.exists()) { - if (parentFile.isFile) throw FileNotFoundException("无法创建${file}的存放目录!") - } else { - parentFile.mkdirs() - } - } - //如果文件体积与要下载的体积相等 - if (file.length() == fileSize) { - //构建一个完成的响应体并返回 - return generateResponse(file, fileType, headResponse) - } else if (file.length() > fileSize) { - //如果文件体积大于要下载的体积,则删除本地文件 - file.delete() - } - //使用HttpUrlConnection下载文件 - download(url, file, fileSize.toFloat(), progressCallback) - //构建一个完成的响应体并返回 - return generateResponse(file, fileType, headResponse) - } else { - return chain.proceed(chain.request()) - } - } - - private fun generateResponse(file: File, fileType: String?, response: Response): Response { - return response.newBuilder().body(FileResponseBody(file, fileType)).build() - } - - private fun download(url: String, file: File, fileSize: Float, progressCallback: ProgressCallback?) { - val connection = URL(url).openConnection() - val httpConnection = connection as HttpURLConnection - httpConnection.addRequestProperty("range", "bytes=${file.length()}-") - httpConnection.readTimeout = 60000 - httpConnection.connectTimeout = 30000 - httpConnection.connect() - val inputStream = httpConnection.inputStream - val randomAccessFile = RandomAccessFile(file, "rws") - randomAccessFile.seek(file.length()) - val buffer = ByteArray(DEFAULT_BUFFER_SIZE) - var readLength = inputStream.read(buffer) - while (readLength > -1) { - randomAccessFile.write(buffer, 0, readLength) - //仅当进度回调不为空,并且文件大小不小于0时,允许回调 - if (progressCallback != null && fileSize > 0) { - progressCallback.update(file.length() / fileSize) - } - readLength = inputStream.read(buffer) - } - httpConnection.disconnect() - randomAccessFile.close() - inputStream.close() - } - -} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/FileResponseBody.kt b/http/src/main/java/com/numeron/http/FileResponseBody.kt index bbf503e..b8f7b1a 100644 --- a/http/src/main/java/com/numeron/http/FileResponseBody.kt +++ b/http/src/main/java/com/numeron/http/FileResponseBody.kt @@ -9,12 +9,12 @@ import okio.source import java.io.File -internal class FileResponseBody(val file: File, private val contentType: String?) : ResponseBody() { +internal class FileResponseBody(val file: File, private val contentType: MediaType?) : ResponseBody() { override fun contentLength(): Long = file.length() override fun source(): BufferedSource = file.source().buffer() - override fun contentType(): MediaType? = contentType?.toMediaTypeOrNull() + override fun contentType(): MediaType? = contentType } \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/ProgressInterceptor.kt b/http/src/main/java/com/numeron/http/ProgressInterceptor.kt new file mode 100644 index 0000000..350bd1e --- /dev/null +++ b/http/src/main/java/com/numeron/http/ProgressInterceptor.kt @@ -0,0 +1,130 @@ +package com.numeron.http + +import okhttp3.* +import okhttp3.internal.closeQuietly +import java.io.File +import java.io.RandomAccessFile +import java.net.URLDecoder + +class ProgressInterceptor : Interceptor { + + override fun intercept(chain: Interceptor.Chain): Response { + //获取原始请求 + val request = chain.request() + .newBuilder() + .addHeader("range", "bytes=0-") + .addHeader("Connection", "keep-alive") + .build() + //取出进度回调 + val progressCallback = request.tag() + //取出文件 + val fileOrPath = request.tag() + //获取原始响应 + val response = chain.proceed(request) + return if (fileOrPath == null && progressCallback == null) { + //如果没有文件也没有回调,则当普通请求处理 + response + } else if (fileOrPath != null) { + //如果有文件参数,则当作下载文件处理 + val responseBody = response.body!! + val contentLength = responseBody.contentLength() + val contentType = responseBody.contentType() + //判断要保存到哪个位置 + val file = if (fileOrPath.extension.isNotEmpty()) { + fileOrPath + } else { + val fileName = response.headers.findFileName() ?: request.url.pathSegments.last() + File(fileOrPath, fileName) + } + //检测、创建存放文件夹 + val parentFile = file.parentFile + if (parentFile != null && !parentFile.exists()) { + parentFile.mkdirs() + } + //获取已保存的文件的大小 + val existLength = file.length() + val inputSource = when { + //如果文件存在,直接返回 + existLength == contentLength -> { + val fileResponseBody = FileResponseBody(file, contentType) + return response.newBuilder().body(fileResponseBody).build() + } + //处理文件名重复的错误文件,使用源数据 + existLength > contentLength -> { + file.delete() + responseBody.source() + } + //未获取到文件大小时,使用源数据 + contentLength == -1L -> { + //如果文件存在,则删除 + if (file.exists()) { + file.delete() + } + responseBody.source() + } + //重新发起请求,获取其余数据 + else -> { + //关闭响应体,释放资源 + response.closeQuietly() + val rangeRequest = request.newBuilder() + .removeHeader("range") + .addHeader("range", "bytes=${existLength}-").build() + chain.proceed(rangeRequest).body!!.source() + } + } + //要写入的文件 + val outputFile = RandomAccessFile(file, "rws") + outputFile.seek(file.length()) + //把数据写入文件 + val buffer = ByteArray(DEFAULT_BUFFER_SIZE) + var readLength = inputSource.read(buffer) + while (readLength > 0) { + outputFile.write(buffer, 0, readLength) + if (contentLength > 0 && progressCallback != null) { + val progress = outputFile.length().toDouble() / contentLength + progressCallback.update(progress.toFloat()) + } + readLength = inputSource.read(buffer) + } + outputFile.closeQuietly() + inputSource.closeQuietly() + //构建新的响应体并返回 + val fileResponseBody = FileResponseBody(file, contentType) + return response.newBuilder().body(fileResponseBody).build() + } else { + //如果只有进度回调,构建有进度回调的响应,不做IO操作 + val responseBody = ProgressResponseBody(response.body!!, progressCallback) + return response.newBuilder().body(responseBody).build() + } + } + + private fun Headers.findFileName(): String? { + return get("Content-Disposition") + ?.split(';') + ?.let { list -> + list.find { + it.contains("filename*") + } ?: list.find { + it.contains("filename") + } + } + ?.split('=') + ?.component2() + ?.removeSurrounding("\"") + ?.let { + if (it.contains('\'')) { + //如果有'字符,则取出编码格式与字符串,解码后返回 + val split = it.split('\'').filter(String::isNotBlank) + URLDecoder.decode(split.component2(), split.component1()) + } else { + it //否则直接返回字符串 + } + } + } + + @Suppress("EXTENSION_SHADOWED_BY_MEMBER") + private inline fun Request.tag(): T? { + return tag(T::class.java) + } + +} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/ProgressResponseBody.kt b/http/src/main/java/com/numeron/http/ProgressResponseBody.kt new file mode 100644 index 0000000..b2cc6a6 --- /dev/null +++ b/http/src/main/java/com/numeron/http/ProgressResponseBody.kt @@ -0,0 +1,32 @@ +package com.numeron.http + +import okhttp3.MediaType +import okhttp3.ResponseBody +import okio.Buffer +import okio.BufferedSource +import okio.ForwardingSource +import okio.buffer + +class ProgressResponseBody(private val delegate: ResponseBody, + private val callback: ProgressCallback?) : ResponseBody() { + + override fun contentLength(): Long = delegate.contentLength() + + override fun contentType(): MediaType? = delegate.contentType() + + override fun source(): BufferedSource { + return object : ForwardingSource(delegate.source()) { + var writtenLength = 0.0 + override fun read(sink: Buffer, byteCount: Long): Long { + val readLength = super.read(sink, byteCount) + val contentLength = contentLength() + if (contentLength > 0 && callback != null) { + writtenLength += readLength.coerceAtLeast(0) + callback.update((writtenLength / contentLength).toFloat()) + } + return readLength + } + }.buffer() + } + +} \ No newline at end of file diff --git a/http/src/main/java/com/numeron/http/Util.kt b/http/src/main/java/com/numeron/http/Util.kt index 44bd5fe..b41a3a8 100644 --- a/http/src/main/java/com/numeron/http/Util.kt +++ b/http/src/main/java/com/numeron/http/Util.kt @@ -2,91 +2,6 @@ package com.numeron.http -import okhttp3.Headers -import okhttp3.HttpUrl -import okhttp3.Request -import okhttp3.ResponseBody import retrofit2.create -import java.io.File -import java.net.URLDecoder - -inline fun AbstractHttpUtil.create() = retrofit.create() - - -fun ResponseBody.toFile(): File { - return if (this is FileResponseBody) { - file - } else try { - val field = javaClass.getDeclaredField("delegate") - field.isAccessible = true - val delegate = field.get(this) as ResponseBody - delegate.toFile() - } catch (throwable: Throwable) { - throw RuntimeException("响应体中没有记录文件信息!或者没有使用Tag标记File类型的参数!", throwable) - } -} - - -fun getFileNameByUrl(url: HttpUrl): String { - return url.pathSegments.last() -} - - -fun Headers.getFileName(): String? { - val filenames = get("Content-Disposition")?.split(';')?.filter { - it.contains("filename") - } - if (filenames != null) { - //优先取编码后的文件名 - val filename = filenames.find { - it.contains("filename*") - } ?: filenames.find { - it.contains("filename") - } - - val removeSurrounding = filename?.split('=') //按=字符分割字符串 - ?.component2() //取第2个 - ?.removeSurrounding("\"") - - if (removeSurrounding != null) { - return if (removeSurrounding.contains('\'')) { - //如果有'字符,则取出编码格式与字符串,解码后返回 - val split = removeSurrounding.split('\'').filter(String::isNotBlank) - URLDecoder.decode(split.component2(), split.component1()) - } else { - removeSurrounding //否则直接返回字符串 - } - } - } - return null -} - - -internal fun Headers.getFileSize(): Long { - return get("Content-Length")?.toLongOrNull() ?: -1 -} - - -internal fun Headers.getFileType(): String? { - return get("Content-Type") -} - - -internal fun getMimeType(extension: String): String? { - return try { - val clazz = Class.forName("android.webkit.MimeTypeMap") - val getSingletonMethod = clazz.getMethod("getSingleton") - val instance = getSingletonMethod.invoke(null) - val getMimeTypeMethod = clazz.getMethod("getMimeTypeFromExtension", String::class.java) - getMimeTypeMethod.invoke(instance, extension) as? String - } catch (e: Throwable) { - null - } -} - - -@Suppress("EXTENSION_SHADOWED_BY_MEMBER") -internal inline fun Request.tag(): T? { - return tag(T::class.java) -} \ No newline at end of file +inline fun AbstractHttpUtil.create() = retrofit.create() \ No newline at end of file diff --git a/result/src/main/java/com/numeron/result/Exp.kt b/result/src/main/java/com/numeron/result/Exp.kt index 6d85844..7d8ca7d 100644 --- a/result/src/main/java/com/numeron/result/Exp.kt +++ b/result/src/main/java/com/numeron/result/Exp.kt @@ -62,6 +62,13 @@ fun F.startActivityForResult( return emptyFragment.startActivityForResult(requestCode, intent, options, callback) } +fun F.startActivityForResult( + intent: Intent, + options: Bundle? = null, + callback: (Intent) -> Unit = {}): LambdaHolder { + return requireActivity().startActivityForResult(intent, options, callback) +} + /** * 申请权限的扩展方法,通过lambda传入回调,不需要重写[Activity#onRequestPermissionsResult]方法 diff --git a/settings.gradle b/settings.gradle index bc960d0..f8894bd 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,2 @@ -include ':app', ':http', ':adapter', ':result', ':rx', ':brick', ':stateful-layout', ':stateful-livedata', ':delegate', ':context-util', ':common', ':starter' +include ':app', ':http', ':adapter', ':result', ':rx', ':brick', ':stateful-layout', ':stateful-livedata', ':delegate', ':context-util', ':common', ':starter', ':chameleon' rootProject.name='Birck' diff --git a/starter/src/main/java/com/numeron/starter/Intents.kt b/starter/src/main/java/com/numeron/starter/Intents.kt index bcd220e..f0ff3be 100644 --- a/starter/src/main/java/com/numeron/starter/Intents.kt +++ b/starter/src/main/java/com/numeron/starter/Intents.kt @@ -7,6 +7,11 @@ import android.content.Intent import androidx.core.os.bundleOf +fun intentOf(vararg extras: Pair): Intent { + return Intent().putExtras(bundleOf(*extras)) +} + + fun Context.intentFor(clazz: Class, vararg extras: Pair): Intent { return Intent(this, clazz).putExtras(bundleOf(*extras)) } diff --git a/starter/src/main/java/com/numeron/starter/Starter.kt b/starter/src/main/java/com/numeron/starter/Starter.kt index 35e034b..ed22837 100644 --- a/starter/src/main/java/com/numeron/starter/Starter.kt +++ b/starter/src/main/java/com/numeron/starter/Starter.kt @@ -7,6 +7,8 @@ import android.app.Service import android.content.ComponentName import android.content.Context import android.os.Bundle +import androidx.core.content.ContextCompat +import androidx.fragment.app.Fragment inline fun Context.startActivity(vararg extras: Pair) { @@ -14,16 +16,36 @@ inline fun Context.startActivity(vararg extras: Pair Context.startActivity(option: Bundle, vararg extras: Pair) { +inline fun Context.startActivity(option: Bundle?, vararg extras: Pair) { startActivity(intentFor(*extras), option) } -inline fun Context.startService(vararg extras: Pair): ComponentName { - return startService(intentFor(*extras))!! +inline fun Context.startService(vararg extras: Pair) { + startService(intentFor(*extras)) } -inline fun Context.startForegroundService(vararg extras: Pair): ComponentName { - return startForegroundService(intentFor(*extras))!! +inline fun Context.startForegroundService(vararg extras: Pair) { + return ContextCompat.startForegroundService(this, intentFor(*extras)) +} + + +inline fun Fragment.startActivity(vararg extras: Pair) { + requireContext().startActivity(*extras) +} + + +inline fun Fragment.startActivity(option: Bundle?, vararg extras: Pair) { + requireContext().startActivity(option, *extras) +} + + +inline fun Fragment.startService(vararg extras: Pair) { + requireContext().startService(*extras) +} + + +inline fun Fragment.startForegroundService(vararg extras: Pair) { + return requireContext().startForegroundService(*extras) } \ No newline at end of file diff --git a/stateful-layout/src/main/java/com/numeron/view/StatefulLayout.kt b/stateful-layout/src/main/java/com/numeron/view/StatefulLayout.kt index 312f9af..60cdaa3 100644 --- a/stateful-layout/src/main/java/com/numeron/view/StatefulLayout.kt +++ b/stateful-layout/src/main/java/com/numeron/view/StatefulLayout.kt @@ -2,6 +2,7 @@ package com.numeron.view import android.content.Context import android.util.AttributeSet +import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.animation.Animation @@ -16,21 +17,13 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu private var previousState: State? = null private var hookLoading = false - /** - * 设置状态,设置空时,将恢复上一个状态 - */ - var state: State? = null - //当设置状态时,将状态压入栈中 + var state: State = State.Empty set(value) { - if (value == null && previousState != null) { - field = previousState - previousState = null - changeView() - } else if (value != field) { + if (value != field) { previousState = field field = value - changeView() } + changeView() } private val failureView @@ -43,14 +36,13 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu get() = getChildAt(2) private val contentView - get() = if (contentViewId == 0) getChildAt(3) else findViewById(contentViewId) + get() = if (successViewId == 0) getChildAt(3) else findViewById(successViewId) - private val contentViewId: Int - private val errorTextViewId: Int + private val defaultState: Int + private val successViewId: Int private val emptyTextViewId: Int + private val failureTextViewId: Int private val loadingTextViewId: Int - - private val defaultStatus: Int private val exitAnimationId: Int private val enterAnimationId: Int @@ -63,13 +55,13 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu val tArray = c.obtainStyledAttributes(a, R.styleable.StatefulLayout) - contentViewId = tArray.getResourceId(R.styleable.StatefulLayout_contentView, 0) + successViewId = tArray.getResourceId(R.styleable.StatefulLayout_successView, 0) - val errorResId = tArray.getResourceId( + val failureResId = tArray.getResourceId( R.styleable.StatefulLayout_failureView, - R.layout.state_failure_layout + R.layout.state_error_layout ) - inflater.inflate(errorResId, this) + inflater.inflate(failureResId, this) val emptyResId = tArray.getResourceId( R.styleable.StatefulLayout_emptyView, @@ -85,28 +77,28 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu loadingTextViewId = tArray.getResourceId( R.styleable.StatefulLayout_loadingTextView, - R.id.loadingStatusTextView + R.id.loadingStateTextView ) - errorTextViewId = tArray.getResourceId( + failureTextViewId = tArray.getResourceId( R.styleable.StatefulLayout_failureTextView, - R.id.errorStatusTextView + R.id.errorStateTextView ) emptyTextViewId = tArray.getResourceId( R.styleable.StatefulLayout_emptyTextView, - R.id.emptyStatusTextView + R.id.emptyStateTextView ) //分配默认状态 - defaultStatus = tArray.getInt(R.styleable.StatefulLayout_state, 0) - + defaultState = tArray.getInt(R.styleable.StatefulLayout_state, 0) + //是否启动状态切换动画 animationEnabled = tArray.getBoolean(R.styleable.StatefulLayout_animationEnabled, true) exitAnimationId = tArray.getResourceId( R.styleable.StatefulLayout_exitAnimation, - R.anim.default_exit_state_layout + R.anim.anim_state_layout_exit ) enterAnimationId = tArray.getResourceId( R.styleable.StatefulLayout_enterAnimation, - R.anim.default_enter_state_layout + R.anim.anim_state_layout_enter ) tArray.recycle() @@ -115,7 +107,7 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu override fun onFinishInflate() { super.onFinishInflate() state = State.values().first { - it.ordinal == defaultStatus + it.ordinal == defaultState } } @@ -124,7 +116,7 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu } fun setFailureText(text: CharSequence?) { - findViewById(errorTextViewId).text = text + findViewById(failureTextViewId).text = text } fun setEmptyText(text: CharSequence?) { @@ -132,13 +124,21 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu } fun setOnFailureTextClickListener(l: (View) -> Unit) { - findViewById(errorTextViewId).setOnClickListener(l) + findViewById(failureTextViewId).setOnClickListener(l) + } + + fun setOnFailureTextClickListener(l: OnClickListener) { + findViewById(failureTextViewId).setOnClickListener(l) } fun setOnEmptyTextClickListener(l: (View) -> Unit) { findViewById(emptyTextViewId).setOnClickListener(l) } + fun setOnEmptyTextClickListener(l: OnClickListener) { + findViewById(emptyTextViewId).setOnClickListener(l) + } + fun setLoadingOperation(operation: (Boolean) -> Unit) { setLoadingOperation(false, operation) } @@ -158,46 +158,32 @@ class StatefulLayout @JvmOverloads constructor(c: Context, a: AttributeSet? = nu if (state == State.Success) contentView?.show() else contentView?.hide() if (state == State.Failure) failureView?.show() else failureView?.hide() if (state == State.Empty) emptyView?.show() else emptyView?.hide() +// Log.e("StatefulLayout", "state=$state") +// Log.e("StatefulLayout", "loadingView=${loadingView.visibility}") +// Log.e("StatefulLayout", "emptyView=${emptyView.visibility}") +// Log.e("StatefulLayout", "failureView=${failureView.visibility}") +// Log.e("StatefulLayout", "contentView=${contentView.visibility}") } private fun View.show() { - val visible = getTag(VISIBILITY) - if (visible != "VISIBLE") { - if (visible != null && animationEnabled) { + if (visibility != VISIBLE) { + visibility = VISIBLE + if (animationEnabled) { + clearAnimation() startAnimation(AnimationUtils.loadAnimation(context, enterAnimationId)) } - setTag(VISIBILITY, "VISIBLE") - visibility = VISIBLE } } private fun View.hide() { - val visible = getTag(VISIBILITY) - if (visible != "GONE") { - if (visible != null && animationEnabled) { + if (visibility != GONE) { + visibility = GONE + if (animationEnabled) { + clearAnimation() val animation = AnimationUtils.loadAnimation(context, exitAnimationId) - animation.setAnimationListener(AnimationListener(this)) startAnimation(animation) - } else { - setTag(VISIBILITY, "GONE") - visibility = GONE } } } - private class AnimationListener(private val view: View) : Animation.AnimationListener { - override fun onAnimationRepeat(animation: Animation?) = Unit - override fun onAnimationStart(animation: Animation?) = Unit - override fun onAnimationEnd(animation: Animation?) { - view.setTag(VISIBILITY, "GONE") - view.visibility = GONE - } - } - - companion object { - - private const val VISIBILITY = 33554432 - - } - } \ No newline at end of file diff --git a/stateful-layout/src/main/java/com/numeron/view/StatusObservers.kt b/stateful-layout/src/main/java/com/numeron/view/StatusObservers.kt index 23c90d1..d2a909f 100644 --- a/stateful-layout/src/main/java/com/numeron/view/StatusObservers.kt +++ b/stateful-layout/src/main/java/com/numeron/view/StatusObservers.kt @@ -3,7 +3,7 @@ package com.numeron.view import androidx.lifecycle.Observer import com.numeron.common.State -class StatefulLayoutObserver(private val statefulLayout: StatefulLayout) : Observer { +class StatfulLayoutObserver(private val statefulLayout: StatefulLayout) : Observer { override fun onChanged(state: State) { statefulLayout.state = state @@ -12,7 +12,7 @@ class StatefulLayoutObserver(private val statefulLayout: StatefulLayout) : Obser } -class StatefulMessageObserver(private val statefulLayout: StatefulLayout) : Observer> { +open class StatefulLayoutMessageObserver(private val statefulLayout: StatefulLayout) : Observer> { override fun onChanged(pair: Pair) { val (status, message) = pair statefulLayout.state = status diff --git a/stateful-layout/src/main/res/anim/default_enter_state_layout.xml b/stateful-layout/src/main/res/anim/anim_state_layout_enter.xml similarity index 100% rename from stateful-layout/src/main/res/anim/default_enter_state_layout.xml rename to stateful-layout/src/main/res/anim/anim_state_layout_enter.xml diff --git a/stateful-layout/src/main/res/anim/default_exit_state_layout.xml b/stateful-layout/src/main/res/anim/anim_state_layout_exit.xml similarity index 100% rename from stateful-layout/src/main/res/anim/default_exit_state_layout.xml rename to stateful-layout/src/main/res/anim/anim_state_layout_exit.xml diff --git a/stateful-layout/src/main/res/layout/state_empty_layout.xml b/stateful-layout/src/main/res/layout/state_empty_layout.xml index 5b2b3ff..2534f44 100644 --- a/stateful-layout/src/main/res/layout/state_empty_layout.xml +++ b/stateful-layout/src/main/res/layout/state_empty_layout.xml @@ -1,7 +1,7 @@ \ No newline at end of file + android:text="@string/state_empty" /> \ No newline at end of file diff --git a/stateful-layout/src/main/res/layout/state_failure_layout.xml b/stateful-layout/src/main/res/layout/state_error_layout.xml similarity index 72% rename from stateful-layout/src/main/res/layout/state_failure_layout.xml rename to stateful-layout/src/main/res/layout/state_error_layout.xml index f551890..059f680 100644 --- a/stateful-layout/src/main/res/layout/state_failure_layout.xml +++ b/stateful-layout/src/main/res/layout/state_error_layout.xml @@ -1,7 +1,7 @@ \ No newline at end of file + android:text="@string/state_error" /> \ No newline at end of file diff --git a/stateful-layout/src/main/res/layout/state_loading_layout.xml b/stateful-layout/src/main/res/layout/state_loading_layout.xml index 3c16254..87ebbbd 100644 --- a/stateful-layout/src/main/res/layout/state_loading_layout.xml +++ b/stateful-layout/src/main/res/layout/state_loading_layout.xml @@ -1,7 +1,7 @@ \ No newline at end of file + android:text="@string/state_loading" /> \ No newline at end of file diff --git a/stateful-layout/src/main/res/values/attrs.xml b/stateful-layout/src/main/res/values/attrs.xml index 326fd73..da3c973 100644 --- a/stateful-layout/src/main/res/values/attrs.xml +++ b/stateful-layout/src/main/res/values/attrs.xml @@ -9,7 +9,7 @@ - + diff --git a/stateful-layout/src/main/res/values/strings.xml b/stateful-layout/src/main/res/values/strings.xml index f893126..db9005b 100644 --- a/stateful-layout/src/main/res/values/strings.xml +++ b/stateful-layout/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ - 没有数据 - 加载失败 - 加载中…… + 没有数据 + 加载失败 + 加载中… \ No newline at end of file diff --git a/stateful-livedata/build.gradle b/stateful-livedata/build.gradle index 234a891..e6b0339 100644 --- a/stateful-livedata/build.gradle +++ b/stateful-livedata/build.gradle @@ -30,6 +30,7 @@ android { dependencies { compileOnly "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + compileOnly "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.2" compileOnly 'androidx.lifecycle:lifecycle-livedata:2.1.0' api project(':common') } diff --git a/stateful-livedata/src/main/AndroidManifest.xml b/stateful-livedata/src/main/AndroidManifest.xml index 5f2157a..a14eb7a 100644 --- a/stateful-livedata/src/main/AndroidManifest.xml +++ b/stateful-livedata/src/main/AndroidManifest.xml @@ -1 +1 @@ - + diff --git a/stateful-livedata/src/main/java/com/numeron/status/Observers.kt b/stateful-livedata/src/main/java/com/numeron/status/Observers.kt deleted file mode 100644 index 088d7f8..0000000 --- a/stateful-livedata/src/main/java/com/numeron/status/Observers.kt +++ /dev/null @@ -1,33 +0,0 @@ -package com.numeron.status - -import androidx.lifecycle.Observer - - -class StatefulObserver( - private val onLoading: (String, Float) -> Unit = { _, _ -> }, - private val onFailure: (String, Throwable) -> Unit = { _, _ -> }, - private val onEmpty: (String) -> Unit = {}, - private val onSuccess: (T) -> Unit = {} -) : Observer> { - - constructor(callback: StatefulCallback) : - this(callback::onLoading, callback::onFailure, callback::onEmpty, callback::onSuccess) - - override fun onChanged(state: State) { - state.onLoading(onLoading).onFailure(onFailure).onEmpty(onEmpty).onSuccess(onSuccess) - } - -} - - -interface StatefulCallback { - - fun onSuccess(value: T) - - fun onFailure(message: String, throwable: Throwable) - - fun onEmpty(message: String) - - fun onLoading(message: String, progress: Float) - -} \ No newline at end of file diff --git a/stateful-livedata/src/main/java/com/numeron/status/State.kt b/stateful-livedata/src/main/java/com/numeron/status/State.kt index aa1227b..9605f6c 100644 --- a/stateful-livedata/src/main/java/com/numeron/status/State.kt +++ b/stateful-livedata/src/main/java/com/numeron/status/State.kt @@ -1,19 +1,19 @@ package com.numeron.status -/* 表示数据的状态,[value]在成功时有值*/ +/* 表示数据的状态,[isSuccess]表示成功状态,[value]在成功时表示数据,其它表示*/ sealed class State(val value: T) /* 表示数据获取成功,用sealed修饰的目的是为了不让其它人使用这个类 */ sealed class Success(value: T) : State(value) -/* 表示数据为空,Empty是Success的子类 */ -class Empty(val message: String) : Success(null) +/* 表示数据为空 */ +data class Empty(val message: String) : Success(null) /* 表示获取数据失败 */ -class Failure(val message: String, val cause: Throwable) : State(null) +data class Failure(val message: String, val cause: Throwable) : State(null) /* 表示正在加载数据 */ -class Loading(val message: String, val progress: Float) : State(null) +data class Loading(val message: String, val progress: Float) : State(null) /* Success的实现类,创建后向上转型为Success后提供给外部 */ @@ -25,14 +25,14 @@ fun successOf(value: T): Success = RealSuccess(value) fun emptyOf(message: String) = Empty(message) -fun failureOf(cause: Throwable, message: String) = Failure(message, cause) +fun failureOf(message: String, cause: Throwable) = Failure(message, cause) -fun loadingOf(message: String, progress: Float) = Loading(message, progress) +fun loadingOf(message: String, progress: Float = 0f) = Loading(message, progress) -/* status functions */ +/* state functions */ inline fun State.onSuccess(block: (T) -> Unit): State { - if (value != null) { + if (this is Success && value != null) { block(value) } return this diff --git a/stateful-livedata/src/main/java/com/numeron/status/StatefulCallback.kt b/stateful-livedata/src/main/java/com/numeron/status/StatefulCallback.kt new file mode 100644 index 0000000..301c363 --- /dev/null +++ b/stateful-livedata/src/main/java/com/numeron/status/StatefulCallback.kt @@ -0,0 +1,11 @@ +package com.numeron.status + + +interface StatefulCallback { + + fun onSuccess(value: T) + fun onEmpty(message: String) + fun onLoading(message: String, progress: Float) + fun onFailure(message: String, cause: Throwable) + +} \ No newline at end of file diff --git a/stateful-livedata/src/main/java/com/numeron/status/StatefulExceptionHandler.kt b/stateful-livedata/src/main/java/com/numeron/status/StatefulExceptionHandler.kt new file mode 100644 index 0000000..007ab46 --- /dev/null +++ b/stateful-livedata/src/main/java/com/numeron/status/StatefulExceptionHandler.kt @@ -0,0 +1,16 @@ +package com.numeron.status + +import kotlinx.coroutines.CoroutineExceptionHandler +import kotlin.coroutines.CoroutineContext + +class StatefulExceptionHandler(private val statefulLiveData: StatefulLiveData) : CoroutineExceptionHandler { + + override val key: CoroutineContext.Key<*> + get() = CoroutineExceptionHandler + + override fun handleException(context: CoroutineContext, exception: Throwable) { + exception.printStackTrace() + statefulLiveData.postFailure(exception) + } + +} \ No newline at end of file diff --git a/stateful-livedata/src/main/java/com/numeron/status/StatefulLiveData.kt b/stateful-livedata/src/main/java/com/numeron/status/StatefulLiveData.kt index b77f6e9..63312f5 100644 --- a/stateful-livedata/src/main/java/com/numeron/status/StatefulLiveData.kt +++ b/stateful-livedata/src/main/java/com/numeron/status/StatefulLiveData.kt @@ -3,10 +3,11 @@ package com.numeron.status import androidx.annotation.MainThread import androidx.lifecycle.LiveData -class StatefulLiveData @JvmOverloads constructor( +open class StatefulLiveData @JvmOverloads constructor( private val empty: String = "没有可用数据", private val loading: String = "正在加载数据", - private val failure: String = "数据加载失败") : LiveData>() { + private val failure: String = "数据加载失败" +) : LiveData>() { @MainThread @JvmOverloads @@ -14,6 +15,15 @@ class StatefulLiveData @JvmOverloads constructor( value = loadingOf(loading, progress) } + @MainThread + fun setLoading(progress: Float) { + setLoading(this.loading, progress) + } + + fun postLoading(progress: Float) { + postLoading(this.loading, progress) + } + @JvmOverloads fun postLoading(loading: String = this.loading, progress: Float = 0f) { postValue(loadingOf(loading, progress)) @@ -48,14 +58,21 @@ class StatefulLiveData @JvmOverloads constructor( } @MainThread - @JvmOverloads fun setFailure(cause: Throwable, failure: String = this.failure) { - value = failureOf(cause, failure) + value = failureOf(failure, cause) } - @JvmOverloads fun postFailure(cause: Throwable, failure: String = this.failure) { - postValue(failureOf(cause, failure)) + postValue(failureOf(failure, cause)) + } + + @MainThread + fun setFailure(cause: Throwable) { + setFailure(cause, this.failure) + } + + fun postFailure(cause: Throwable) { + postFailure(cause, this.failure) } } \ No newline at end of file diff --git a/stateful-livedata/src/main/java/com/numeron/status/StatefulObserver.kt b/stateful-livedata/src/main/java/com/numeron/status/StatefulObserver.kt new file mode 100644 index 0000000..52ba75d --- /dev/null +++ b/stateful-livedata/src/main/java/com/numeron/status/StatefulObserver.kt @@ -0,0 +1,27 @@ +package com.numeron.status + +import androidx.lifecycle.Observer + + +class StatefulObserver(private val callback: StatefulCallback) : Observer> { + + constructor( + onSuccess: (T) -> Unit = {}, + onFailure: (String, Throwable) -> Unit = { _, _ -> }, + onLoading: (String, Float) -> Unit = { _, _ -> }, + onEmpty: (String) -> Unit = {} + ) : this(object : StatefulCallback { + override fun onSuccess(value: T) = onSuccess(value) + override fun onEmpty(message: String) = onEmpty(message) + override fun onFailure(message: String, cause: Throwable) = onFailure(message, cause) + override fun onLoading(message: String, progress: Float) = onLoading(message, progress) + }) + + override fun onChanged(state: State) { + state.onFailure(callback::onFailure) + .onSuccess(callback::onSuccess) + .onLoading(callback::onLoading) + .onEmpty(callback::onEmpty) + } + +} diff --git a/stateful-livedata/src/main/java/com/numeron/status/Transform.kt b/stateful-livedata/src/main/java/com/numeron/status/Transform.kt index ffeca6d..4f67148 100644 --- a/stateful-livedata/src/main/java/com/numeron/status/Transform.kt +++ b/stateful-livedata/src/main/java/com/numeron/status/Transform.kt @@ -2,14 +2,12 @@ package com.numeron.status -import com.numeron.common.State - /* 把密封类转换为StatusLayout可识别的枚举类 */ -fun com.numeron.status.State.convert(): State { +fun State.convert(): com.numeron.common.State { return when (this) { - is Empty -> State.Empty - is Failure -> State.Failure - is Success -> State.Success - is Loading -> State.Loading + is Empty -> com.numeron.common.State.Empty + is Failure -> com.numeron.common.State.Failure + is Success -> com.numeron.common.State.Success + is Loading -> com.numeron.common.State.Loading } } \ No newline at end of file