This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
@@ -0,0 +1 @@
/build
@@ -0,0 +1,88 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'com.google.dagger.hilt.android'
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
multiDexEnabled true
consumerProguardFiles "consumer-rules.pro"
javaCompileOptions {
annotationProcessorOptions {
arguments += [
"room.schemaLocation":"$projectDir/schemas".toString(),
"room.incremental":"true",
"room.expandProjection":"true",
AROUTER_MODULE_NAME: project.getName()
]
}
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8.toString()
}
buildFeatures {
viewBinding true
}
}
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
generateStubs = true
useBuildCache = true
javacOptions {
option("-Xmaxerrs", 500)
}
}
dependencies {
api rootProject.ext.ktxLibs
// surpport
api rootProject.ext.commonUILibs
// Android Jetpack 组件
api rootProject.ext.jetpackLibs
// net
api rootProject.ext.netLibs
// glide
api rootProject.ext.glideLibs
kapt rootProject.ext.compiler["glideCompiler"]
// arouter
api rootProject.ext.arouterLibs
kapt rootProject.ext.compiler["arouterCompiler"]
kapt rootProject.ext.compiler["hiltAndroidCompiler"]
api rootProject.ext.otherLibs
}
@@ -0,0 +1,239 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
###########################通用配置start#########################################
#1.基本指令区
# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
-optimizationpasses 5
# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers
# 不进行优化,建议使用此选项,
-dontoptimize
# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify 能够加快混淆速度
-dontpreverify
# 使我们的项目混淆后产生映射文件包含有类名->混淆后类名的映射关系
-verbose
# 使用printmapping指定映射文件的名称
-printmapping proguardMapping.txt
# 屏蔽警告
-ignorewarnings
# 指定混淆是采用的算法,后面的参数是一个过滤器这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*
#添加支持的jar(引入libs下的所有jar包)
#-libraryjars libs(*.jar)
#将文件来源重命名为“SourceFile”字符串
-renamesourcefileattribute SourceFile
# 保留Annotation不混淆
-keepattributes *Annotation*
-keep class * extends java.lang.annotation.Annotation {*;}
# 避免混淆泛型
-keepattributes Signature
#保持反射不被混淆
-keepattributes EnclosingMethod
#保持异常不被混淆
-keepattributes Exceptions
#保持内部类不被混淆
-keepattributes Exceptions,InnerClasses
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
#2.默认保留区
# 保留我们使用的四大组件,自定义的Application等等这些类不被混淆
# 因为这些子类都有可能被外部调用
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
-keep public class com.google.vending.licensing.ILicensingService
# 保留support下的所有类及其内部类
-keep class android.support.** {*;}
# 保留继承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
-keep public class * extends androidx.appcompat.**
-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留在Activity中的方法参数是view的方法,这样一来我们在layout中写的onClick就不会被影响
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 保留枚举类不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留我们自定义控件(继承自View)不被混淆
-keep public class * extends android.view.View{
*** get*();
void set*(***);
public <init>(android.content.Context);
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
-keepclasseswithmembers class * {
public <init>(android.content.Context, android.util.AttributeSet);
public <init>(android.content.Context, android.util.AttributeSet, int);
}
# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
# 保留Serializable序列化的类不被混淆
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 保留R下面的资源
-keep class **.R$* {
*;
}
# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
# 避免layout中onclick方法(android:onclick="onClick")混淆
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 对WebView的处理
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, java.lang.String);
}
-keepclassmembers class * {
@android.webkit.JavascriptInterface <methods>;
}
# AndroidX混淆
-keep class com.google.android.material.** {*;}
-keep class androidx.** {*;}
-keep public class * extends androidx.**
-keep interface androidx.** {*;}
-dontwarn com.google.android.material.**
-dontnote com.google.android.material.**
-dontwarn androidx.**
###########################通用配置end#########################################
###########################第三方库start#########################################
#Architecture
-dontwarn android.arch.**
-keep public class android.arch.**{*;}
#bugly
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
#okhttp
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
# A resource is loaded with a relative path so the package of this class must be preserved.
-keepnames class okhttp3.internal.publicsuffix.PublicSuffixDatabase
#retrofit
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Exceptions
#glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.module.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-dontwarn com.bumptech.glide.load.model.stream.**
# EventBus
-keepclassmembers class * {
@org.greenrobot.eventbus.Subscribe <methods>;
}
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Only required if you use AsyncExecutor
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
<init>(java.lang.Throwable);
}
# Arouter
-keep public class com.alibaba.android.arouter.routes.**{*;}
-keep public class com.alibaba.android.arouter.facade.**{*;}
-keep class * implements com.alibaba.android.arouter.facade.template.ISyringe{*;}
###########################第三方库end#########################################
-keep class com.bbgo.common_base.bean.** {*;}
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.bbgo.common_base">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
@@ -0,0 +1,29 @@
package com.bbgo.common_base
import android.app.Application
import android.content.Context
import com.alibaba.android.arouter.launcher.ARouter
import com.tencent.mmkv.MMKV
open class BaseApplication : Application() {
override fun onCreate() {
super.onCreate()
context = this
MMKV.initialize(this)
if (BuildConfig.DEBUG) { // 这两行必须写在init之前,否则这些配置在init过程中将无效
ARouter.openLog() // 打印日志
ARouter.openDebug() // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(this)
}
companion object {
private var context: BaseApplication? = null
@JvmStatic
fun getContext(): Context {
return context!!
}
}
}
@@ -0,0 +1,59 @@
package com.bbgo.common_base.base
import android.app.Activity
import android.os.Bundle
import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.bbgo.common_base.util.ActivityCollector
import com.bbgo.library_statusbar.NotchScreenManager
import java.lang.ref.WeakReference
/**
* author: wangyb
* date: 2021/5/20 10:03 上午
* description: todo
*/
abstract class BaseActivity<VB: ViewBinding> : AppCompatActivity() {
/**
* 日志输出标志
*/
protected val TAG: String = this.javaClass.simpleName
protected lateinit var binding: VB
/**
* 当前Activity的实例。
*/
private var activity: Activity? = null
/** 当前Activity的弱引用,防止内存泄露 */
private var activityWR: WeakReference<Activity>? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = inflateViewBinding()
setContentView(binding.root)
NotchScreenManager.getInstance().setDisplayInNotch(this)
activity = this
activityWR = WeakReference(activity!!)
ActivityCollector.pushTask(activityWR)
}
abstract fun inflateViewBinding(): VB
override fun onOptionsItemSelected(item: MenuItem): Boolean {
if (item.itemId == android.R.id.home) {
finish()
}
return super.onOptionsItemSelected(item)
}
override fun onDestroy() {
super.onDestroy()
activity = null
ActivityCollector.removeTask(activityWR)
}
}
@@ -0,0 +1,75 @@
package com.bbgo.common_base.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.viewbinding.ViewBinding
/**
* author: wangyb
* date: 2021/5/20 3:01 下午
* description: todo
*/
abstract class BaseFragment<VB: ViewBinding> : Fragment() {
/**
* 日志输出标志
*/
protected val TAG: String = this.javaClass.simpleName
private var _binding: VB? = null
protected val binding get() = _binding!!
private var isLoaded = false
/**
* 防止回退到上一级页面时还会init view的问题
*/
private var isInitializedRootView = false
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = inflateViewBinding(inflater, container)
return _binding?.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
if (isInitializedRootView) return
super.onViewCreated(view, savedInstanceState)
initView()
observe()
isInitializedRootView = true
}
override fun onResume() {
super.onResume()
if (!isLoaded && !isHidden) {
lazyLoad()
isLoaded = true
}
}
abstract fun inflateViewBinding(inflater: LayoutInflater, container: ViewGroup?): VB
/**
* 初始化 View
*/
abstract fun initView()
/**
* 懒加载
*/
abstract fun lazyLoad()
abstract fun observe()
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
}
@@ -0,0 +1,30 @@
package com.bbgo.common_base.base
import android.util.Log
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.util.concurrent.atomic.AtomicBoolean
class SingleLiveData<T> : MutableLiveData<T>() {
private val TAG = "SingleLiveData"
private val pending = AtomicBoolean(false)
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
if (hasActiveObservers()) {
Log.d(TAG, "multi observers registered but only one will be notified of changes")
}
super.observe(owner) {
if (pending.compareAndSet(true, false)) {
observer.onChanged(it)
}
}
}
override fun setValue(value: T) {
pending.set(true)
super.setValue(value)
}
}
@@ -0,0 +1,14 @@
package com.bbgo.common_base.bean
import androidx.annotation.Keep
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/8/13 11:40 上午
*/
@Keep
open class BaseBean {
var errorCode: Int = 0
var errorMsg: String = ""
}
@@ -0,0 +1,12 @@
package com.bbgo.common_base.bean
import androidx.annotation.Keep
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/8/13 11:41 上午
*/
@Keep
class HttpResult<T>(val data: T) : BaseBean()
@@ -0,0 +1,14 @@
package com.bbgo.common_base.bus
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/8/17 10:17 上午
*/
class BusKey {
companion object {
const val COLLECT = "COLLECT"
const val SCROLL_TOP = "SCROLL_TOP"
const val LOGIN_SUCCESS = "LOGIN_SUCCESS"
}
}
@@ -0,0 +1,92 @@
package com.bbgo.common_base.bus
import androidx.lifecycle.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
/**
* @AuthorWangyuebin
* @Date2022-09-19
* @Description
*/
object EventFlowBus {
private val busMap = mutableMapOf<String, EventBus<*>>()
private val busStickMap = mutableMapOf<String, StickEventBus<*>>()
@Synchronized
fun <T> with(key: String): EventBus<T> {
var eventBus = busMap[key]
if (eventBus == null) {
eventBus = EventBus<T>(key)
busMap[key] = eventBus
}
return eventBus as EventBus<T>
}
@Synchronized
fun <T> withStick(key: String): StickEventBus<T> {
var eventBus = busStickMap[key]
if (eventBus == null) {
eventBus = StickEventBus<T>(key)
busStickMap[key] = eventBus
}
return eventBus as StickEventBus<T>
}
//真正实现类
open class EventBus<T>(private val key: String) : LifecycleEventObserver {
//私有对象用于发送消息
private val _events: MutableSharedFlow<T> by lazy {
obtainEvent()
}
//暴露的公有对象用于接收消息
val events = _events.asSharedFlow()
open fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(0, 1, BufferOverflow.DROP_OLDEST)
//主线程接收数据
fun register(lifecycleOwner: LifecycleOwner, action: (t: T) -> Unit) {
lifecycleOwner.lifecycle.addObserver(this)
lifecycleOwner.lifecycleScope.launch {
events.collect {
try {
action(it)
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
//协程中发送数据
suspend fun post(event: T) {
_events.emit(event)
}
//主线程发送数据
fun post(scope: CoroutineScope, event: T) {
scope.launch {
_events.emit(event)
}
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
//自动销毁
if (event == Lifecycle.Event.ON_DESTROY) {
val subscriptCount = _events.subscriptionCount.value
if (subscriptCount <= 0)
busMap.remove(key)
}
}
}
class StickEventBus<T>(key: String) : EventBus<T>(key) {
override fun obtainEvent(): MutableSharedFlow<T> = MutableSharedFlow(1, 1, BufferOverflow.DROP_OLDEST)
}
}
@@ -0,0 +1,125 @@
package com.bbgo.common_base.bus
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import java.lang.Exception
import java.lang.NullPointerException
import java.util.HashMap
class LiveDataBus private constructor() {
private val bus: MutableMap<String, BusMutableLiveData<Any>>
init {
bus = HashMap()
}
private object SingletonHolder {
val DEFAULT_BUS = LiveDataBus()
}
companion object {
fun get(): LiveDataBus {
return SingletonHolder.DEFAULT_BUS
}
}
fun <T> with(key: String, type: Class<T>?): MutableLiveData<T> {
if (!bus.containsKey(key)) {
val mutableLiveData = BusMutableLiveData<Any>()
bus[key] = mutableLiveData
return mutableLiveData as MutableLiveData<T>
}
return bus[key] as MutableLiveData<T>
}
fun with(key: String): MutableLiveData<Any> {
return with(key, Any::class.java)
}
private class ObserverWrapper<T>(val observer: Observer<in T>) : Observer<T> {
override fun onChanged(t: T) {
if (isCallOnObserve) {
return
}
observer.onChanged(t)
}
private val isCallOnObserve: Boolean
get() {
val stackTrace = Thread.currentThread().stackTrace
if (stackTrace.isNotEmpty()) {
for (element in stackTrace) {
if ("androidx.lifecycle.LiveData" == element.className && "observeForever" == element.methodName) {
return true
}
}
}
return false
}
}
private class BusMutableLiveData<T> : MutableLiveData<T>() {
private val observerMap: MutableMap<Observer<in T>, Observer<in T>> = HashMap()
override fun observe(owner: LifecycleOwner, observer: Observer<in T>) {
super.observe(owner, observer)
try {
hook(observer)
} catch (e: Exception) {
e.printStackTrace()
}
}
override fun observeForever(observer: Observer<in T>) {
if (!observerMap.containsKey(observer)) {
observerMap[observer] = ObserverWrapper(observer)
}
observerMap[observer]?.let { super.observeForever(it) }
}
override fun removeObserver(observer: Observer<in T>) {
val realObserver = if (observerMap.containsKey(observer)) {
observerMap.remove(observer) as Observer<in T>
} else {
observer
}
super.removeObserver(realObserver)
}
private fun hook(observer: Observer<in T>) {
kotlin.runCatching {
//get wrapper's version
val classLiveData = LiveData::class.java
val fieldObservers = classLiveData.getDeclaredField("mObservers")
fieldObservers.isAccessible = true
val objectObservers = fieldObservers[this]
val classObservers: Class<*> = objectObservers.javaClass
val methodGet = classObservers.getDeclaredMethod("get", Any::class.java)
methodGet.isAccessible = true
val objectWrapperEntry = methodGet.invoke(objectObservers, observer)
var objectWrapper: Any? = null
if (objectWrapperEntry is Map.Entry<*, *>) {
objectWrapper = objectWrapperEntry.value
}
if (objectWrapper == null) {
throw NullPointerException("Wrapper can not be bull!")
}
val classObserverWrapper = objectWrapper.javaClass.superclass
val fieldLastVersion = classObserverWrapper.getDeclaredField("mLastVersion")
fieldLastVersion.isAccessible = true
//get livedata's version
val fieldVersion = classLiveData.getDeclaredField("mVersion")
fieldVersion.isAccessible = true
val objectVersion = fieldVersion[this]
//set wrapper's version
fieldLastVersion[objectWrapper] = objectVersion
}.onFailure {
}
}
}
}
@@ -0,0 +1,57 @@
package com.bbgo.common_base.constants
/**
* author: wangyb
* date: 2021/5/21 4:38 下午
* description: todo
*/
object Constants {
const val DB_INSTANCE = "DB_INSTANCE"
const val TOKEN = "token"
const val USER_ID = "userid"
const val USER_NAME = "username"
const val PASSWORD = "password"
const val APP_TAG = "WanAndroid"
/**
* url key
*/
const val CONTENT_URL_KEY = "url"
/**
* title key
*/
const val CONTENT_TITLE_KEY = "title"
/**
* id key
*/
const val CONTENT_ID_KEY = "id"
/**
* id key
*/
const val CONTENT_CID_KEY = "cid"
/**
* share key
*/
const val CONTENT_SHARE_TYPE = "text/plain"
const val POSITION = "position"
const val COLLECT = "isCollect"
const val ROUTER_PATH = "routerPath"
object CollectType {
const val COLLECT = "COLLECT"
const val UNCOLLECT = "UNCOLLECT"
const val UNKNOWN = "UNKNOWN"
}
object FragmentIndex {
const val HOME_INDEX = 0
const val WECHAT_INDEX = 1
const val SYS_INDEX = 2
const val SQUARE_INDEX = 3
const val PROJECT_INDEX = 4
}
}
@@ -0,0 +1,96 @@
package com.bbgo.common_base.constants
/**
* @Description: Arouter路由表
* 不同的module,一级目录必须不能相同
*
* @Author: wangyuebin
* @Date: 2021/9/10 4:00 下午
*/
class RouterPath {
/**
* 登录注册 组件
*/
object LoginRegister {
private const val LOGIN_REGISTER = "module_login"
const val PAGE_LOGIN = "/$LOGIN_REGISTER/login"
const val PAGE_REGISTER = "/$LOGIN_REGISTER/register"
const val SERVICE_LOGOUT = "/$LOGIN_REGISTER/logout"
}
/**
* 主页
*/
object Main {
private const val MAIN = "app_main"
const val PAGE_MAIN = "/$MAIN/main"
}
object Collect {
private const val COLLECT = "module_collect"
const val SERVICE_COLLECT = "/$COLLECT/collect"
}
/**
* 内容 web 组件
*/
object Content {
private const val CONTENT = "module_content"
const val PAGE_CONTENT = "/$CONTENT/content"
}
/**
* 首页 组件
*/
object Home {
private const val HOME = "module_home"
const val PAGE_HOME = "/$HOME/home"
}
/**
* 微信公众号 组件
*/
object WeChat {
private const val WECHAT = "module_wechat"
const val PAGE_WECHAT = "/$WECHAT/wechat"
}
/**
* 体系 组件
*/
object Sys {
private const val SYS = "module_sys"
const val PAGE_SYS = "/$SYS/sys"
}
/**
* 体系 组件
*/
object Square {
private const val SQUARE = "module_square"
const val PAGE_SQUARE = "/$SQUARE/square"
}
/**
* 项目 组件
*/
object Project {
private const val PROJECT = "module_project"
const val PAGE_PROJECT = "/$PROJECT/project"
}
/**
* compose 组件
*/
object Compose {
private const val COMPOSE = "module_compose"
const val PAGE_COMPOSE = "/$COMPOSE/compose"
}
object Media {
private const val MEDIA = "module_media"
const val PAGE_VIDEO = "/$MEDIA/video"
}
}
@@ -0,0 +1,62 @@
//package com.bbgo.common_base.di
//
//import com.bbgo.common_base.BaseApplication
//import com.bbgo.common_base.constants.HttpConstant
//import com.bbgo.common_base.net.GsonTypeAdapterFactory
//import com.bbgo.common_base.net.ServiceCreators
//import com.bbgo.common_base.net.interceptor.*
//import com.google.gson.GsonBuilder
//import dagger.Module
//import dagger.Provides
//import dagger.hilt.InstallIn
//import dagger.hilt.components.SingletonComponent
//import okhttp3.Cache
//import okhttp3.OkHttpClient
//import retrofit2.Retrofit
//import retrofit2.converter.gson.GsonConverterFactory
//import java.io.File
//import javax.inject.Singleton
//
///**
// * @Description:
// * @Author: wangyuebin
// * @Date: 2021/8/30 5:05 下午
// */
//
//@Module
//@InstallIn(SingletonComponent::class)
//object NetWorkModule {
//
// private val BASE_URL = "https://wanandroid.com/"
//
// //设置 请求的缓存的大小跟位置
// private val cacheFile = File(BaseApplication.getContext().cacheDir, "cache")
// private val cache = Cache(cacheFile, HttpConstant.MAX_CACHE_SIZE)
//
// @Provides
// @Singleton
// fun provideOkHttpClient(): OkHttpClient {
// return OkHttpClient.Builder()
// .addInterceptor(MultiBaseUrlInterceptor())
// .addInterceptor(LoggingInterceptor())
// .addInterceptor(HeaderInterceptor())
// .addInterceptor(SaveCookieInterceptor())
// .addInterceptor(CacheInterceptor())
// .cache(cache)
// .addInterceptor(BasicParamsInterceptor())
// .build()
// }
//
// @Provides
// @Singleton
// fun provideRetrofit(): Retrofit {
// return Retrofit.Builder()
// .baseUrl(BASE_URL)
// .client(ServiceCreators.httpClient)
// .addConverterFactory(
// GsonConverterFactory.create(
// GsonBuilder().registerTypeAdapterFactory(
// GsonTypeAdapterFactory()
// ).create())).build()
// }
//}
@@ -0,0 +1,11 @@
package com.bbgo.common_base.event
import androidx.annotation.Keep
/**
* author: wangyb
* date: 2021/5/27 8:12 下午
* description: todo
*/
@Keep
data class MessageEvent(val indexPage: Int, val type: String, val position: Int, val pageId: Int)
@@ -0,0 +1,3 @@
package com.bbgo.common_base.event
data class ScrollEvent(val index: Int)
@@ -0,0 +1,58 @@
/*
* Copyright (C) guolin, Suzhou Quxiang Inc. Open source codes for study only.
* Do not use for commercial purpose.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bbgo.common_base.ext
import com.bbgo.common_base.BaseApplication
/**
* 根据手机的分辨率将dp转成为px。
*/
fun dp2px(dp: Float): Int {
val scale = BaseApplication.getContext().resources.displayMetrics.density
return (dp * scale + 0.5f).toInt()
}
/**
* 根据手机的分辨率将px转成dp。
*/
fun px2dp(px: Float): Int {
val scale = BaseApplication.getContext().resources.displayMetrics.density
return (px / scale + 0.5f).toInt()
}
/**
* 获取屏幕宽值。
*/
val screenWidth
get() = BaseApplication.getContext().resources.displayMetrics.widthPixels
/**
* 获取屏幕高值。
*/
val screenHeight
get() = BaseApplication.getContext().resources.displayMetrics.heightPixels
/**
* 获取屏幕像素:对获取的宽高进行拼接。例:1080X2340。
*/
fun screenPixel(): String {
BaseApplication.getContext().resources.displayMetrics.run {
return "${widthPixels}X${heightPixels}"
}
}
@@ -0,0 +1,21 @@
package com.bbgo.common_base.ext
/**
* Created by AhmedEltaher
*/
class Error(val code: Int, val description: String) {
constructor(exception: Exception) : this(code = DEFAULT_ERROR, description = exception.message
?: "")
}
const val HTTP_REQUEST_ERROR = -1
const val NO_INTERNET_CONNECTION = -1
const val NETWORK_ERROR = -2
const val DEFAULT_ERROR = -3
const val PASS_WORD_ERROR = -101
const val USER_NAME_ERROR = -102
const val CHECK_YOUR_FIELDS = -103
const val SEARCH_ERROR = -104
const val USER_REGISTERED_ERROR = -105
const val USER_NOT_LOGIN = -1001
@@ -0,0 +1,20 @@
package com.bbgo.common_base.ext
import androidx.lifecycle.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
fun <T> LifecycleOwner.observe(liveData: LiveData<T>, action: (t: T) -> Unit) {
liveData.observe(this, { it?.let { t -> action(t) } })
}
inline fun LifecycleOwner.collectWithLifeCycle(viewLifecycleOwner: LifecycleOwner,
minActiveSate: Lifecycle.State = Lifecycle.State.STARTED,
crossinline block: suspend CoroutineScope.() -> Unit
) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(minActiveSate) {
block()
}
}
}
@@ -0,0 +1,89 @@
package com.bbgo.common_base.ext
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore
import com.tencent.mmkv.MMKV
object Prefs {
private val mmkv = MMKV.defaultMMKV()!!
fun putInt(key: String, value: Int) {
mmkv.encode(key, value)
}
fun getInt(key: String, defaultValue: Int = -1): Int {
return mmkv.decodeInt(key, defaultValue)
}
fun putIntAsync(key: String, value: Int) {
mmkv.putInt(key, value).apply()
}
fun putString(key: String, value: String) {
mmkv.encode(key, value)
}
fun putStringAsync(key: String, value: String) {
mmkv.putString(key, value).apply()
}
fun getString(key: String, defaultValue: String = ""): String {
return mmkv.decodeString(key, defaultValue) ?: defaultValue
}
fun putDouble(key: String, value: Double) {
mmkv.encode(key, value)
}
fun putDoubleAsync(key: String, value: Float) {
mmkv.putFloat(key, value).apply()
}
fun getDouble(key: String, defaultValue: Double = 0.00): Double {
return mmkv.decodeDouble(key, defaultValue)
}
fun putLong(key: String, value: Long) {
mmkv.encode(key, value)
}
fun putLongAsync(key: String, value: Long) {
mmkv.putLong(key, value).apply()
}
fun getLong(key: String, defaultValue: Long = 0L): Long {
return mmkv.decodeLong(key, defaultValue)
}
fun putBoolean(key: String, value: Boolean) {
mmkv.encode(key, value)
}
fun putBooleanAsync(key: String, value: Boolean) {
mmkv.putBoolean(key, value).apply()
}
fun getBoolean(key: String, defaultValue: Boolean = false): Boolean {
return mmkv.decodeBool(key, defaultValue)
}
fun clear() {
mmkv.clear().apply()
}
/*****************************************************************************/
/**
* dataStore 存入数据默认就是异步,没有同步方法
* 取数据异步通过Flow实现
*/
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
fun put(key: String, value: Any) {
}
}
@@ -0,0 +1,16 @@
package com.bbgo.common_base.ext
// A generic class that contains data and status about loading this data.
sealed class Resource<out T: Any> {
data class Success<out T: Any>(val data: T) : Resource<T>()
data class Loading(val nothing: Nothing? = null) : Resource<Nothing>()
data class Error(val exception: Throwable) : Resource<Nothing>()
override fun toString(): String {
return when (this) {
is Success -> "Success[data=$data]"
is Error -> "Error[exception=${exception}]"
is Loading -> "Loading"
}
}
}
@@ -0,0 +1,24 @@
package com.bbgo.common_base.ext
import android.content.Context
import android.widget.Toast
import androidx.fragment.app.Fragment
/**
* Created by xuhao on 2017/11/14.
*/
fun Fragment.showToast(content: String): Toast {
val toast = Toast.makeText(this.activity?.applicationContext, content, Toast.LENGTH_SHORT)
toast.show()
return toast
}
fun Context.showToast(content: String): Toast {
val toast = Toast.makeText(this.applicationContext, content, Toast.LENGTH_SHORT)
toast.show()
return toast
}
@@ -0,0 +1,280 @@
package com.bbgo.common_base.ext
import android.annotation.SuppressLint
import android.app.Activity
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.View
import android.view.ViewGroup
import androidx.activity.ComponentActivity
import androidx.annotation.IdRes
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.Fragment
import androidx.lifecycle.*
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty
/**
* Property Delegate of ViewBinding
* <p>
* Created by pengxr on 2/5/2021
*/
// -------------------------------------------------------------------------------------
// ViewBindingProperty for Activity
// -------------------------------------------------------------------------------------
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ComponentActivity) -> View = ::findRootView
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(viewProvider(activity))
}
@JvmName("viewBindingActivity")
inline fun <V : ViewBinding> ComponentActivity.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ComponentActivity, V> = ActivityViewBindingProperty { activity: ComponentActivity ->
viewBinder(activity.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------------------------------------
// ViewBindingProperty for Fragment / DialogFragment
// -------------------------------------------------------------------------------------
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (F) -> View = Fragment::requireView
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> DialogFragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
} as ViewBindingProperty<F, V>
else -> FragmentViewBindingProperty { fragment: F ->
viewBinder(viewProvider(fragment))
}
}
@Suppress("UNCHECKED_CAST")
@JvmName("viewBindingFragment")
inline fun <F : Fragment, V : ViewBinding> Fragment.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<F, V> = when (this) {
is DialogFragment -> viewBinding(viewBinder) { fragment: DialogFragment ->
fragment.getRootView(viewBindingRootId)
} as ViewBindingProperty<F, V>
else -> viewBinding(viewBinder) { fragment: F ->
fragment.requireView().requireViewByIdCompat(viewBindingRootId)
}
}
// -------------------------------------------------------------------------------------
// ViewBindingProperty for ViewGroup
// -------------------------------------------------------------------------------------
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (ViewGroup) -> View = { this }
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewProvider(viewGroup))
}
@JvmName("viewBindingViewGroup")
inline fun <V : ViewBinding> ViewGroup.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<ViewGroup, V> = LazyViewBindingProperty { viewGroup: ViewGroup ->
viewBinder(viewGroup.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------------------------------------
// ViewBindingProperty for RecyclerView#ViewHolder
// -------------------------------------------------------------------------------------
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
crossinline viewProvider: (RecyclerView.ViewHolder) -> View = RecyclerView.ViewHolder::itemView
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(viewProvider(holder))
}
@JvmName("viewBindingViewHolder")
inline fun <V : ViewBinding> RecyclerView.ViewHolder.viewBinding(
crossinline viewBinder: (View) -> V,
@IdRes viewBindingRootId: Int
): ViewBindingProperty<RecyclerView.ViewHolder, V> = LazyViewBindingProperty { holder: RecyclerView.ViewHolder ->
viewBinder(holder.itemView.requireViewByIdCompat(viewBindingRootId))
}
// -------------------------------------------------------------------------------------
// ViewBindingProperty
// -------------------------------------------------------------------------------------
private const val TAG = "ViewBindingProperty"
interface ViewBindingProperty<in R : Any, out V : ViewBinding> : ReadOnlyProperty<R, V> {
@MainThread
fun clear()
}
class LazyViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
private var viewBinding: V? = null
@Suppress("UNCHECKED_CAST")
@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }
return viewBinder(thisRef).also {
this.viewBinding = it
}
}
@MainThread
override fun clear() {
viewBinding = null
}
}
abstract class LifecycleViewBindingProperty<in R : Any, out V : ViewBinding>(
private val viewBinder: (R) -> V
) : ViewBindingProperty<R, V> {
private var viewBinding: V? = null
protected abstract fun getLifecycleOwner(thisRef: R): LifecycleOwner
@SuppressLint("LogNotTimber")
@MainThread
override fun getValue(thisRef: R, property: KProperty<*>): V {
// Already bound
viewBinding?.let { return it }
val lifecycle = getLifecycleOwner(thisRef).lifecycle
val viewBinding = viewBinder(thisRef)
if (lifecycle.currentState == Lifecycle.State.DESTROYED) {
Log.w(
TAG, "Access to viewBinding after Lifecycle is destroyed or hasn't created yet. " +
"The instance of viewBinding will be not cached."
)
// We can access to ViewBinding after Fragment.onDestroyView(), but don't save it to prevent memory leak
} else {
lifecycle.addObserver(ClearOnDestroyLifecycleObserver(this))
this.viewBinding = viewBinding
}
return viewBinding
}
@MainThread
override fun clear() {
viewBinding = null
}
private class ClearOnDestroyLifecycleObserver(
private val property: LifecycleViewBindingProperty<*, *>
) : DefaultLifecycleObserver {
private companion object {
private val mainHandler = Handler(Looper.getMainLooper())
}
@MainThread
override fun onDestroy(owner: LifecycleOwner) {
mainHandler.post { property.clear() }
}
}
}
class FragmentViewBindingProperty<in F : Fragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
try {
return thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}
class DialogFragmentViewBindingProperty<in F : DialogFragment, out V : ViewBinding>(
viewBinder: (F) -> V
) : LifecycleViewBindingProperty<F, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: F): LifecycleOwner {
return if (thisRef.showsDialog) {
thisRef
} else {
try {
thisRef.viewLifecycleOwner
} catch (ignored: IllegalStateException) {
error("Fragment doesn't have view associated with it or the view has been destroyed")
}
}
}
}
// -------------------------------------------------------------------------------------
// Utils
// -------------------------------------------------------------------------------------
@RestrictTo(RestrictTo.Scope.LIBRARY)
class ActivityViewBindingProperty<in A : ComponentActivity, out V : ViewBinding>(
viewBinder: (A) -> V
) : LifecycleViewBindingProperty<A, V>(viewBinder) {
override fun getLifecycleOwner(thisRef: A): LifecycleOwner {
return thisRef
}
}
fun <V : View> View.requireViewByIdCompat(@IdRes id: Int): V {
return ViewCompat.requireViewById(this, id)
}
fun <V : View> Activity.requireViewByIdCompat(@IdRes id: Int): V {
return ActivityCompat.requireViewById(this, id)
}
/**
* Utility to find root view for ViewBinding in Activity
*/
fun findRootView(activity: Activity): View {
val contentView = activity.findViewById<ViewGroup>(android.R.id.content)
checkNotNull(contentView) { "Activity has no content view" }
return when (contentView.childCount) {
1 -> contentView.getChildAt(0)
0 -> error("Content view has no children. Provide root view explicitly")
else -> error("More than one child view found in Activity content view")
}
}
fun DialogFragment.getRootView(viewBindingRootId: Int): View {
val dialog = checkNotNull(dialog) {
"DialogFragment doesn't have dialog. Use viewBinding delegate after onCreateDialog"
}
val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" }
return with(window.decorView) {
if (viewBindingRootId != 0) requireViewByIdCompat(
viewBindingRootId
) else this
}
}
@@ -0,0 +1,54 @@
package com.bbgo.common_base.glide
import android.content.Context
import com.bbgo.common_base.glide.okhttp.OkHttpUrlLoader
import com.bbgo.common_base.net.ServiceCreators
import com.bbgo.common_base.util.FileUtils
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.load.engine.bitmap_recycle.LruBitmapPool
import com.bumptech.glide.load.engine.cache.ExternalPreferredCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.engine.cache.LruResourceCache
import com.bumptech.glide.load.engine.cache.MemorySizeCalculator
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.AppGlideModule
import java.io.InputStream
@GlideModule
class ImageAppModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
//设置缓存大小为20mb
// val memoryCacheSizeBytes = 1024 * 1024 * 20 // 20mb
// //设置内存缓存大小
// builder.setMemoryCache(LruResourceCache(memoryCacheSizeBytes.toLong()))
val calculator = MemorySizeCalculator.Builder(context)
.setMemoryCacheScreens(5.toFloat())
.setBitmapPoolScreens(5.toFloat())
.build()
builder.setMemoryCache(LruResourceCache(calculator.memoryCacheSize.toLong()))
builder.setBitmapPool(LruBitmapPool(calculator.bitmapPoolSize.toLong()))
if (FileUtils.isExistExternalStore()) {
builder.setDiskCache(
ExternalPreferredCacheDiskCacheFactory(context)
)
} else {
builder.setDiskCache(
InternalCacheDiskCacheFactory(context)
)
}
}
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
registry.replace(GlideUrl::class.java, InputStream::class.java, OkHttpUrlLoader.Factory(
ServiceCreators.httpClient))
}
override fun isManifestParsingEnabled(): Boolean {
return false
}
}
@@ -0,0 +1,37 @@
package com.bbgo.common_base.glide;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
public class ProgressInterceptor implements Interceptor {
public static final Map<String, ProgressListener> LISTENER_MAP = new HashMap<>();
//入注册下载监听
public static void addListener(String url, ProgressListener listener) {
LISTENER_MAP.put(url, listener);
}
//取消注册下载监听
public static void removeListener(String url) {
LISTENER_MAP.remove(url);
}
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
Response response = chain.proceed(request);
String url = request.url().toString();
ResponseBody body = response.body();
Response newResponse = response.newBuilder().body(new ProgressResponseBody(url, body)).build();
return newResponse;
}
}
@@ -0,0 +1,5 @@
package com.bbgo.common_base.glide;
public interface ProgressListener {
void onProgress(int progress);
}
@@ -0,0 +1,85 @@
package com.bbgo.common_base.glide;
import android.util.Log;
import androidx.annotation.Nullable;
import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer;
import okio.BufferedSource;
import okio.ForwardingSource;
import okio.Okio;
import okio.Source;
public class ProgressResponseBody extends ResponseBody {
private static final String TAG = ProgressResponseBody.class.getName();
private BufferedSource bufferedSource;
private ResponseBody responseBody;
private ProgressListener listener;
public ProgressResponseBody(String url, ResponseBody responseBody) {
this.responseBody = responseBody;
listener = ProgressInterceptor.LISTENER_MAP.get(url);
}
@Nullable
@Override
public MediaType contentType() {
return responseBody.contentType();
}
@Override
public long contentLength() {
return responseBody.contentLength();
}
@Override
public BufferedSource source() {
if (bufferedSource == null) {
bufferedSource = Okio.buffer(new ProgressSource(responseBody.source()));
}
return bufferedSource;
}
private class ProgressSource extends ForwardingSource {
long totalBytesRead = 0;
int currentProgress;
ProgressSource(Source source) {
super(source);
}
@Override
public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount);
long fullLength = responseBody.contentLength();
if (bytesRead == -1) {
totalBytesRead = fullLength;
} else {
totalBytesRead += bytesRead;
}
int progress = (int) (100f * totalBytesRead / fullLength);
Log.d(TAG, "download progress is " + progress);
if (listener != null && progress != currentProgress) {
listener.onProgress(progress);
}
if (listener != null && totalBytesRead == fullLength) {
listener = null;
}
currentProgress = progress;
return bytesRead;
}
}
}
@@ -0,0 +1,114 @@
package com.bbgo.common_base.glide.okhttp;
import android.util.Log;
import androidx.annotation.NonNull;
import com.bumptech.glide.Priority;
import com.bumptech.glide.load.DataSource;
import com.bumptech.glide.load.HttpException;
import com.bumptech.glide.load.data.DataFetcher;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.util.ContentLengthInputStream;
import com.bumptech.glide.util.Preconditions;
import java.io.IOException;
import java.io.InputStream;
import java.util.Map;
import okhttp3.Call;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
/** Fetches an {@link InputStream} using the okhttp library. */
public class OkHttpStreamFetcher implements DataFetcher<InputStream>, okhttp3.Callback {
private static final String TAG = "OkHttpFetcher";
private final Call.Factory client;
private final GlideUrl url;
private InputStream stream;
private ResponseBody responseBody;
private DataCallback<? super InputStream> callback;
// call may be accessed on the main thread while the object is in use on other threads. All other
// accesses to variables may occur on different threads, but only one at a time.
private volatile Call call;
// Public API.
@SuppressWarnings("WeakerAccess")
public OkHttpStreamFetcher(Call.Factory client, GlideUrl url) {
this.client = client;
this.url = url;
}
@Override
public void loadData(
@NonNull Priority priority, @NonNull final DataCallback<? super InputStream> callback) {
Request.Builder requestBuilder = new Request.Builder().url(url.toStringUrl());
for (Map.Entry<String, String> headerEntry : url.getHeaders().entrySet()) {
String key = headerEntry.getKey();
requestBuilder.addHeader(key, headerEntry.getValue());
}
Request request = requestBuilder.build();
this.callback = callback;
call = client.newCall(request);
call.enqueue(this);
}
@Override
public void onFailure(@NonNull Call call, @NonNull IOException e) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "OkHttp failed to obtain result", e);
}
callback.onLoadFailed(e);
}
@Override
public void onResponse(@NonNull Call call, @NonNull Response response) {
responseBody = response.body();
if (response.isSuccessful()) {
long contentLength = Preconditions.checkNotNull(responseBody).contentLength();
stream = ContentLengthInputStream.obtain(responseBody.byteStream(), contentLength);
callback.onDataReady(stream);
} else {
callback.onLoadFailed(new HttpException(response.message(), response.code()));
}
}
@Override
public void cleanup() {
try {
if (stream != null) {
stream.close();
}
} catch (IOException e) {
// Ignored
}
if (responseBody != null) {
responseBody.close();
}
callback = null;
}
@Override
public void cancel() {
Call local = call;
if (local != null) {
local.cancel();
}
}
@NonNull
@Override
public Class<InputStream> getDataClass() {
return InputStream.class;
}
@NonNull
@Override
public DataSource getDataSource() {
return DataSource.REMOTE;
}
}
@@ -0,0 +1,49 @@
package com.bbgo.common_base.glide.okhttp;
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
import androidx.annotation.NonNull;
import com.bumptech.glide.load.Options;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.ModelLoader;
import com.bumptech.glide.load.model.ModelLoaderFactory;
import com.bumptech.glide.load.model.MultiModelLoaderFactory;
import java.io.InputStream;
public class OkHttpUrlLoader implements ModelLoader<GlideUrl, InputStream> {
private final okhttp3.Call.Factory client;
public OkHttpUrlLoader(@NonNull okhttp3.Call.Factory client) {
this.client = client;
}
public boolean handles(@NonNull GlideUrl url) {
return true;
}
public LoadData<InputStream> buildLoadData(@NonNull GlideUrl model, int width, int height, @NonNull Options options) {
return new LoadData(model, new OkHttpStreamFetcher(this.client, model));
}
public static class Factory implements ModelLoaderFactory<GlideUrl, InputStream> {
private final okhttp3.Call.Factory client;
public Factory(@NonNull okhttp3.Call.Factory client) {
this.client = client;
}
@NonNull
public ModelLoader<GlideUrl, InputStream> build(MultiModelLoaderFactory multiFactory) {
return new OkHttpUrlLoader(this.client);
}
public void teardown() {
}
}
}
@@ -0,0 +1,44 @@
package com.bbgo.common_base.interceptor
import android.content.Context
import com.alibaba.android.arouter.facade.Postcard
import com.alibaba.android.arouter.facade.annotation.Interceptor
import com.alibaba.android.arouter.facade.callback.InterceptorCallback
import com.alibaba.android.arouter.facade.template.IInterceptor
import com.bbgo.common_base.constants.RouterPath
import com.bbgo.common_base.util.AppUtil
/**
* @Description:
* 该拦截器的作用是:统一页面跳转时,判断用户是否已经登录,
* 将业务层判断用户是否登录的逻辑统一到这里,业务层就不需要做if else判断了
* @Author: wangyuebin
* @Date: 2021/9/17 2:25 下午
*/
@Interceptor(name = "login", priority = 1)
class LoginInterceptor : IInterceptor {
/**
* 该集合保存的是需要登录成功之后才跳转的页面,也就是有@RequireLogin注解的页面
*/
private var pageList = mutableListOf<String>()
override fun init(context: Context?) {
pageList.add("com.bbgo.module_compose.activity.ComposeActivity")
}
override fun process(postcard: Postcard, callback: InterceptorCallback) {
// 判断哪些页面需要登录 (在整个应用中,有些页面需要登录,有些是不需要登录的)
if (pageList.contains(postcard.destination.canonicalName)) {
if (AppUtil.isLogin) { // 如果已经登录了,则默认不做任何处理
callback.onContinue(postcard)
} else { // 未登录,拦截
callback.onInterrupt(null)
}
} else {
callback.onContinue(postcard)
}
}
}
@@ -0,0 +1,63 @@
package com.bbgo.common_base.interceptor
import com.alibaba.android.arouter.utils.ClassUtils
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.util.AppUtil
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/18 9:37 上午
*/
class RefletionUtils {
companion object {
private val classList = mutableListOf<String>()
fun getRequireLoginPages(): List<String> {
kotlin.runCatching {
val clzList = ClassUtils.getFileNameByPackageName(BaseApplication.getContext(), "com.android.processor.apt")
for(name in clzList) {
if (!name.endsWith("RequireLogin")) {
continue
}
val clz = Class.forName(name)
val method = clz.getDeclaredMethod("getRequireLoginList")
method.isAccessible = true
val list = method.invoke(null) as List<String>
if (list.isEmpty()) continue
classList.addAll(list)
}
return classList
}.onFailure {
return emptyList()
}
return emptyList()
}
fun getLoginStatus(): Boolean {
kotlin.runCatching {
val clzList = ClassUtils.getFileNameByPackageName(BaseApplication.getContext(), "com.android.processor.apt")
for(name in clzList) {
if (!name.endsWith("InjectLogin")) {
continue
}
val clz = Class.forName(name)
val method = clz.getDeclaredMethod("getInjectLoginClass")
method.isAccessible = true
val targetClzString = method.invoke(null) as String
val targetClz = Class.forName(targetClzString)
val field = targetClz.getDeclaredField("isLogin")
field.isAccessible = true
return field.getBoolean(targetClz)
}
return false
}.onFailure {
return false
}
return false
}
}
}
@@ -0,0 +1,71 @@
package com.bbgo.common_base.net
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.suspendCancellableCoroutine
import retrofit2.Call
import retrofit2.CallAdapter
import retrofit2.Callback
import retrofit2.Response
import java.lang.reflect.Type
import kotlin.coroutines.resume
import kotlin.coroutines.resumeWithException
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/9 3:43 下午
*/
class ResponseCallAdapter<T>(
private val responseType: Type
) : CallAdapter<T, Flow<Response<T>>> {
override fun adapt(call: Call<T>): Flow<Response<T>> {
return flow {
emit(
suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
})
continuation.invokeOnCancellation { call.cancel() }
}
)
}
}
override fun responseType() = responseType
}
class BodyCallAdapter<T>(private val responseType: Type) : CallAdapter<T, Flow<T>> {
override fun adapt(call: Call<T>): Flow<T> {
return flow {
emit(
suspendCancellableCoroutine { continuation ->
call.enqueue(object : Callback<T> {
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
override fun onResponse(call: Call<T>, response: Response<T>) {
try {
continuation.resume(response.body()!!)
} catch (e: Exception) {
continuation.resumeWithException(e)
}
}
})
continuation.invokeOnCancellation { call.cancel() }
}
)
}
}
override fun responseType() = responseType
}
@@ -0,0 +1,45 @@
package com.bbgo.common_base.net
import kotlinx.coroutines.flow.Flow
import retrofit2.CallAdapter
import retrofit2.Response
import retrofit2.Retrofit
import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/9 3:44 下午
*/
class FlowCallAdapterFactory private constructor() : CallAdapter.Factory() {
override fun get(
returnType: Type,
annotations: Array<Annotation>,
retrofit: Retrofit
): CallAdapter<*, *>? {
if (getRawType(returnType) != Flow::class.java) {
return null
}
check(returnType is ParameterizedType) { "Flow return type must be parameterized as Flow<Foo> or Flow<out Foo>" }
val responseType = getParameterUpperBound(0, returnType)
val rawFlowType = getRawType(responseType)
return if (rawFlowType == Response::class.java) {
check(responseType is ParameterizedType) { "Response must be parameterized as Response<Foo> or Response<out Foo>" }
ResponseCallAdapter<Any>(
getParameterUpperBound(
0,
responseType
)
)
} else {
BodyCallAdapter<Any>(responseType)
}
}
companion object {
@JvmStatic
fun create() = FlowCallAdapterFactory()
}
}
@@ -0,0 +1,81 @@
package com.bbgo.common_base.net
import com.google.gson.Gson
import com.google.gson.TypeAdapter
import com.google.gson.TypeAdapterFactory
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import com.google.gson.stream.JsonToken
import com.google.gson.stream.JsonWriter
import java.io.IOException
/**
* author: wangyb
* date: 3/29/21 5:57 PM
* description: 处理gson解析时类型不匹配或者空值问题 https://www.jianshu.com/p/d8dcc656a06e
*/
class GsonTypeAdapterFactory : TypeAdapterFactory {
override fun <T : Any?> create(gson: Gson?, type: TypeToken<T>?): TypeAdapter<T> {
val adapter = gson?.getDelegateAdapter(this, type)
return object : TypeAdapter<T>() {
@Throws(IOException::class)
override fun write(out: JsonWriter?, value: T) {
adapter?.write(out, value)
}
@Throws(IOException::class)
override fun read(jr: JsonReader): T? {
//gson 库会通过JsonReader对json对象的每个字段进项读取,当发现类型不匹配时抛出异常
return try {
adapter?.read(jr)
} catch (e: Throwable) {
//那么我们就在它抛出异常的时候进行处理,让它继续不中断接着往下读取其他的字段就好了
consumeAll(jr)
null
}
}
@Throws(IOException::class)
private fun consumeAll(jr: JsonReader) {
if (jr.hasNext()) {
val peek: JsonToken = jr.peek()
when {
peek === JsonToken.STRING -> {
jr.nextString()
}
peek === JsonToken.BEGIN_ARRAY -> {
jr.beginArray()
consumeAll(jr)
jr.endArray()
}
peek === JsonToken.BEGIN_OBJECT -> {
jr.beginObject()
consumeAll(jr)
jr.endObject()
}
peek === JsonToken.END_ARRAY -> {
jr.endArray()
}
peek === JsonToken.END_OBJECT -> {
jr.endObject()
}
peek === JsonToken.NUMBER -> {
jr.nextString()
}
peek === JsonToken.BOOLEAN -> {
jr.nextBoolean()
}
peek === JsonToken.NAME -> {
jr.nextName()
consumeAll(jr)
}
peek === JsonToken.NULL -> {
jr.nextNull()
}
}
}
}
}
}
}
@@ -0,0 +1,64 @@
package com.bbgo.common_base.net
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.net.interceptor.BasicParamsInterceptor
import com.bbgo.common_base.net.interceptor.CacheInterceptor
import com.bbgo.common_base.net.interceptor.LoggingInterceptor
import com.bbgo.common_base.net.interceptor.MultiBaseUrlInterceptor
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import com.google.gson.GsonBuilder
import okhttp3.Cache
import okhttp3.OkHttpClient
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.File
import java.util.concurrent.TimeUnit
/**
* author: wangyb
* date: 3/29/21 5:46 PM
* description: 服务创建类
*/
object ServiceCreators {
private const val BASE_URL = "https://wanandroid.com/"
private const val MAX_CACHE_SIZE: Long = 1024 * 1024 * 50 // 50M 的缓存大小
//设置 请求的缓存的大小跟位置
private val cacheFile = File(BaseApplication.getContext().cacheDir, "httpCache")
private val cache = Cache(cacheFile, MAX_CACHE_SIZE)
private val cookieJar = PersistentCookieJar(
SetCookieCache(),
SharedPrefsCookiePersistor(BaseApplication.getContext())
)
val httpClient = OkHttpClient.Builder()
.connectTimeout(10, TimeUnit.SECONDS)
.readTimeout(10, TimeUnit.SECONDS)
.writeTimeout(20, TimeUnit.SECONDS)
.retryOnConnectionFailure(true)
.addInterceptor(MultiBaseUrlInterceptor())
.addInterceptor(LoggingInterceptor())
.addInterceptor(CacheInterceptor())
.cookieJar(cookieJar)
.cache(cache)
.addInterceptor(BasicParamsInterceptor())
.build()
private val builder = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(httpClient)
.addCallAdapterFactory(FlowCallAdapterFactory.create())
.addConverterFactory(GsonConverterFactory.create(GsonBuilder().registerTypeAdapterFactory(
GsonTypeAdapterFactory()
).create()))
private val retrofit = builder.build()
// val service: HttpApiService by lazy { retrofit.create(HttpApiService::class.java) }
fun <T> create(serviceClass: Class<T>): T = retrofit.create(serviceClass)
}
@@ -0,0 +1,8 @@
package com.bbgo.common_base.net.download
interface DownloadListener {
fun onStart()
fun onProgress(progress: Int, total: Float)
fun onFinish(path: String, url: String)
fun onError(msg: String?)
}
@@ -0,0 +1,86 @@
package com.bbgo.common_base.net.download
import com.bbgo.common_base.net.ServiceCreators
import com.bbgo.common_base.net.download.api.DownloadApiService
import com.bbgo.common_base.util.log.Logs
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
object FileDownloader {
private val service = ServiceCreators.create(DownloadApiService::class.java)
private var url: String = ""
private var path: String = ""
private var listener: DownloadListener? = null
private var downloadLength: Long = 0
private var contentLength: Long = 0
fun create(url: String) : FileDownloader {
FileDownloader.url = url
return this
}
fun setPath(path: String): FileDownloader {
FileDownloader.path = path
return this
}
fun setListener(listener: DownloadListener): FileDownloader {
FileDownloader.listener = listener
return this
}
suspend fun start() = withContext(Dispatchers.IO) {
kotlin.runCatching {
listener?.onStart()
val responseBody = service.downloadFile(url)
contentLength = responseBody.contentLength()
val file = File(path)
write(responseBody.byteStream(), file)
listener?.onFinish(path, url)
}.onFailure {
listener?.onError(it.message)
Logs.e(it, "download file fail, it's reason is ${it.message}")
}
}
private fun write(inputStream: InputStream, file: File) {
var fos: FileOutputStream? = null
try {
fos = FileOutputStream(file, true)
val buffer = ByteArray(4096)
var len: Int
while (inputStream.read(buffer).also { len = it } != -1) {
fos.write(buffer, 0, len)
downloadLength += len
notifyProgress()
}
fos.flush()
} catch (e: IOException) {
throw IOException(e.message)
} finally {
try {
inputStream.close()
} catch (e: IOException) {
e.printStackTrace()
}
if (fos != null) {
try {
fos.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
}
private fun notifyProgress() {
val progress = ((downloadLength.toFloat() / contentLength.toFloat()) * 100).toInt()
listener?.onProgress(progress, contentLength.toFloat())
}
}
@@ -0,0 +1,19 @@
package com.bbgo.common_base.net.download.api
import okhttp3.ResponseBody
import retrofit2.http.GET
import retrofit2.http.Streaming
import retrofit2.http.Url
/**
* author: wangyb
* date: 4/7/21 9:24 PM
* description: http api
*/
interface DownloadApiService {
@Streaming //添加这个注解用来下载大文件
@GET
suspend fun downloadFile(@Url fileUrl: String): ResponseBody
}
@@ -0,0 +1,33 @@
package com.bbgo.common_base.net.interceptor
import android.os.Build
import com.bbgo.common_base.ext.screenPixel
import com.bbgo.common_base.util.AppUtil
import okhttp3.Interceptor
import okhttp3.Response
/**
* author: wangyb
* date: 2021/5/28 10:20 上午
* description: todo
*/
class BasicParamsInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
val originalHttpUrl = originalRequest.url
val url = originalHttpUrl.newBuilder().apply {
addQueryParameter("udid", AppUtil.getDeviceSerial())
addQueryParameter("vc", AppUtil.eyepetizerVersionCode.toString())
addQueryParameter("vn", AppUtil.eyepetizerVersionName)
addQueryParameter("size", screenPixel())
addQueryParameter("deviceModel", AppUtil.deviceModel)
addQueryParameter("first_channel", AppUtil.deviceBrand)
addQueryParameter("last_channel", AppUtil.deviceBrand)
addQueryParameter("system_version_code", "${Build.VERSION.SDK_INT}")
}.build()
val request = originalRequest.newBuilder().url(url).method(originalRequest.method,
originalRequest.body
).build()
return chain.proceed(request)
}
}
@@ -0,0 +1,37 @@
package com.bbgo.common_base.net.interceptor
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.util.NetWorkUtil
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.Response
class CacheInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!NetWorkUtil.isNetworkAvailable(BaseApplication.getContext())) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val response = chain.proceed(request)
if (NetWorkUtil.isNetworkAvailable(BaseApplication.getContext())) {
val maxAge = 60 * 3
// 有网络时 设置缓存超时时间0个小时 ,意思就是不读取缓存数据,只对get有用,post没有缓冲
response.newBuilder()
.header("Cache-Control", "public, max-age=$maxAge")
.removeHeader("Retrofit")// 清除头信息,因为服务器如果不支持,会返回一些干扰信息,不清除下面无法生效
.build()
} else {
// 无网络时,设置超时为4周 只对get有用,post没有缓冲
val maxStale = 60 * 60 * 24 * 28
response.newBuilder()
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
.removeHeader("nyn")
.build()
}
return response
}
}
@@ -0,0 +1,31 @@
package com.bbgo.common_base.net.interceptor
import com.bbgo.common_base.util.log.Logs
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* author: wangyb
* date: 2021/5/28 10:19 上午
* description: todo
*/
class LoggingInterceptor : Interceptor {
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request()
val t1 = System.nanoTime()
Logs.d("Sending request: ${request.url} \n ${request.headers}")
val response = chain.proceed(request)
val t2 = System.nanoTime()
Logs.d("Received response for ${response.request.url} in ${(t2 - t1) / 1e6} ms\n${response.headers}")
return response
}
companion object {
const val TAG = "LoggingInterceptor"
}
}
@@ -0,0 +1,39 @@
package com.bbgo.common_base.net.interceptor
import android.text.TextUtils
import com.bbgo.common_base.util.log.Logs
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response
class MultiBaseUrlInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
//获取原始的request
val originalRequest = chain.request()
//获取原始的url
val originalUrl = originalRequest.url
//获取原始的request的创建者builder
val builder = originalRequest.newBuilder()
//获取原始的headers
val headers = originalRequest.headers
val baseUrl = headers["baseUrl"]//获取请求头里面设置的baseurl
Logs.d("originalUrl=$originalUrl")
Logs.d("baseUrl=$baseUrl")
return if (!TextUtils.isEmpty(baseUrl)) {
val baseHttpUrl = baseUrl?.toHttpUrlOrNull()//将其解析成httpurl
//重建新的HttpUrl,需要重新设置的url部分
val newHttpUrl = originalUrl.newBuilder()
.scheme(baseHttpUrl?.scheme!!)
.host(baseHttpUrl.host)
.port(baseHttpUrl.port)
.build()
//获取处理后的新newRequest
val newRequest = builder.url(newHttpUrl).build()
Logs.d("newHttpUrl=${newRequest.url}")
chain.proceed(newRequest)
} else {
chain.proceed(originalRequest)
}
}
}
@@ -0,0 +1,45 @@
package com.bbgo.common_base.pool
import androidx.annotation.NonNull
import com.bbgo.common_base.util.log.Logs
import java.util.concurrent.ThreadFactory
import java.util.concurrent.atomic.AtomicInteger
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/24 4:13 下午
*/
class AppThreadFactory : ThreadFactory {
private val TAG = "AppThreadFactory"
private val threadNumber = AtomicInteger(1)
private val group: ThreadGroup
private val namePrefix: String
override fun newThread(@NonNull runnable: Runnable?): Thread {
val threadName = namePrefix + threadNumber.getAndIncrement()
Logs.i("Thread production, name is [$threadName]")
val thread = Thread(group, runnable, threadName, 0)
if (thread.isDaemon) { //设为非后台线程
thread.isDaemon = false
}
if (thread.priority != Thread.NORM_PRIORITY) { //优先级为normal
thread.priority = Thread.NORM_PRIORITY
}
// 捕获多线程处理中的异常
thread.uncaughtExceptionHandler = Thread.UncaughtExceptionHandler { thread, ex ->
Logs.i("Running task appeared exception! Thread [" + thread.name + "], because [" + ex.message + "]")
}
return thread
}
companion object {
private val poolNumber = AtomicInteger(1)
}
init {
val s = System.getSecurityManager()
group = if (s != null) s.threadGroup else Thread.currentThread().threadGroup
namePrefix = "App task pool No." + poolNumber.getAndIncrement() + ", thread No."
}
}
@@ -0,0 +1,82 @@
package com.bbgo.common_base.pool
import com.bbgo.common_base.util.log.Logs
import java.util.concurrent.*
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/9/24 4:10 下午
*/
class ThreadPoolUtils private constructor(
corePoolSize: Int,
maximumPoolSize: Int,
keepAliveTime: Long,
unit: TimeUnit,
workQueue: BlockingQueue<Runnable>,
threadFactory: ThreadFactory
) :
ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory,
RejectedExecutionHandler { r, executor ->
Logs.e("Task rejected, too many task!")
}) {
/*
* 线程执行结束,顺便看一下有么有什么乱七八糟的异常
*
* @param r the runnable that has completed
* @param t the exception that caused termination, or null if
*/
override fun afterExecute(r: Runnable?, t: Throwable?) {
var throwable = t
super.afterExecute(r, t)
if (t == null && r is Future<*>) {
try {
(r as Future<*>).get()
} catch (ce: CancellationException) {
throwable = ce
} catch (ee: ExecutionException) {
throwable = ee.cause
} catch (ie: InterruptedException) {
Thread.currentThread().interrupt() // ignore/reset
}
}
if (throwable != null) {
Logs.w(
throwable,
"Running task appeared exception! Thread [${Thread.currentThread().name}]"
)
}
}
companion object {
private const val TAG = "AppPoolExecutor"
// Thread args
private val CPU_COUNT = Runtime.getRuntime().availableProcessors()
private val INIT_THREAD_COUNT = CPU_COUNT + 1
private val MAX_THREAD_COUNT = INIT_THREAD_COUNT
private const val SURPLUS_THREAD_LIFE = 30L
@Volatile
var instance: ThreadPoolUtils? = null
get() {
if (null == field) {
synchronized(ThreadPoolUtils::class.java) {
if (null == field) {
field = ThreadPoolUtils(
INIT_THREAD_COUNT,
MAX_THREAD_COUNT,
SURPLUS_THREAD_LIFE,
TimeUnit.SECONDS,
ArrayBlockingQueue(64),
AppThreadFactory()
)
}
}
}
return field
}
private set
}
}
@@ -0,0 +1,90 @@
/*
* Copyright (c) 2020. vipyinzhiwei <vipyinzhiwei@gmail.com>
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bbgo.common_base.util
import android.app.Activity
import java.lang.ref.WeakReference
import java.util.*
/**
* @Description: 管理应用程序中所有Activity。
* @Author: wangyuebin
* @Date: 2021/11/09 7:22 下午
*/
object ActivityCollector {
private val activitys = Stack<WeakReference<Activity>>()
/**
* 将Activity压入Application栈
*
* @param task 将要压入栈的Activity对象
*/
fun pushTask(task: WeakReference<Activity>?) {
activitys.push(task)
}
/**
* 将传入的Activity对象从栈中移除
*
* @param task
*/
fun removeTask(task: WeakReference<Activity>?) {
activitys.remove(task)
}
/**
* 根据指定位置从栈中移除Activity
*
* @param taskIndex Activity栈索引
*/
fun removeTask(taskIndex: Int) {
if (activitys.size > taskIndex) activitys.removeAt(taskIndex)
}
/**
* 将栈中Activity移除至栈顶
*/
fun removeToTop() {
val end = activitys.size
val start = 1
for (i in end - 1 downTo start) {
val mActivity = activitys[i].get()
if (null != mActivity && !mActivity.isFinishing) {
mActivity.finish()
}
}
}
/**
* 移除全部(用于整个应用退出)
*/
fun removeAll() {
for (task in activitys) {
val mActivity = task.get()
if (null != mActivity && !mActivity.isFinishing) {
mActivity.finish()
}
}
}
fun getTopActivity(): WeakReference<Activity> {
return activitys[activitys.size - 1]
}
}
@@ -0,0 +1,494 @@
package com.bbgo.common_base.util
import android.annotation.SuppressLint
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.PowerManager
import android.provider.Settings
import android.text.TextUtils
import androidx.annotation.RequiresApi
import androidx.core.content.FileProvider
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.ext.Prefs
import com.bbgo.common_base.util.log.Logs
import java.io.File
import java.util.*
/**
* 应用程序全局的通用工具类,功能比较单一,经常被复用的功能,应该封装到此工具类当中,从而给全局代码提供方面的操作。
*
* @author wangyb
* @since 17/2/18
*/
object AppUtil {
private var TAG = "AppUtil"
/**
* 获取当前应用程序的包名。
*
* @return 当前应用程序的包名。
*/
val appPackage: String
get() = BaseApplication.getContext().packageName
/**
* 获取当前应用程序的名称。
* @return 当前应用程序的名称。
*/
val appName: String
get() = BaseApplication.getContext().resources.getString(BaseApplication.getContext().applicationInfo.labelRes)
/**
* 获取当前应用程序的版本名。
* @return 当前应用程序的版本名。
*/
val appVersionName: String
get() = BaseApplication.getContext().packageManager.getPackageInfo(appPackage, 0).versionName
/**
* 获取当前应用程序的版本号。
* @return 当前应用程序的版本号。
*/
val appVersionCode: Long
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
BaseApplication.getContext().packageManager.getPackageInfo(appPackage, 0).longVersionCode
} else {
BaseApplication.getContext().packageManager.getPackageInfo(appPackage, 0).versionCode.toLong()
}
/**
* 获取开眼应用程序的版本名。
* @return 开眼当前应用程序的版本名。
*/
val eyepetizerVersionName: String
get() = "6.3.01"
/**
* 获取开眼应用程序的版本号。
* @return 开眼当前应用程序的版本号。
*/
val eyepetizerVersionCode: Long
get() = 6030012
/**
* 获取设备的的型号,如果无法获取到,则返回Unknown。
* @return 设备型号。
*/
val deviceModel: String
get() {
var deviceModel = Build.MODEL
if (TextUtils.isEmpty(deviceModel)) {
deviceModel = "unknown"
}
return deviceModel
}
/**
* 获取设备的品牌,如果无法获取到,则返回Unknown。
* @return 设备品牌,全部转换为小写格式。
*/
val deviceBrand: String
get() {
var deviceBrand = Build.BRAND
if (TextUtils.isEmpty(deviceBrand)) {
deviceBrand = "unknown"
}
return deviceBrand.toLowerCase(Locale.getDefault())
}
private var deviceSerial: String? = null
/**
* 获取设备的序列号。如果无法获取到设备的序列号,则会生成一个随机的UUID来作为设备的序列号,UUID生成之后会存入缓存,
* 下次获取设备序列号的时候会优先从缓存中读取。
* @return 设备的序列号。
*/
@SuppressLint("HardwareIds")
fun getDeviceSerial(): String {
if (deviceSerial == null) {
var deviceId: String? = null
// val appChannel = getApplicationMetaData("APP_CHANNEL")
kotlin.runCatching {
deviceId = Settings.Secure.getString(
BaseApplication.getContext().contentResolver,
Settings.Secure.ANDROID_ID
)
deviceId?.let {
if (it.length < 255) {
deviceSerial = deviceId
return deviceSerial.toString()
}
}
}.onFailure {
Logs.e(TAG, "get android_id with error", it)
}
var uuid = Prefs.getString("uuid", "")
if (uuid.isNotEmpty()) {
deviceSerial = uuid
return deviceSerial.toString()
}
uuid = UUID.randomUUID().toString().replace("-", "").uppercase(Locale.getDefault())
Prefs.putString("uuid", uuid)
deviceSerial = uuid
return deviceSerial.toString()
} else {
return deviceSerial.toString()
}
}
/**
* 获取资源文件中定义的字符串。
*
* @param resId
* 字符串资源id
* @return 字符串资源id对应的字符串内容。
*/
fun getString(resId: Int): String {
return BaseApplication.getContext().resources.getString(resId)
}
/**
* 获取资源文件中定义的字符串。
*
* @param resId
* 字符串资源id
* @return 字符串资源id对应的字符串内容。
*/
fun getDimension(resId: Int): Int {
return BaseApplication.getContext().resources.getDimensionPixelOffset(resId)
}
/**
* 获取指定资源名的资源id。
*
* @param name
* 资源名
* @param type
* 资源类型
* @return 指定资源名的资源id。
*/
fun getResourceId(name: String, type: String): Int {
return BaseApplication.getContext().resources.getIdentifier(name, type, appPackage)
}
/**
* 获取AndroidManifest.xml文件中,<application>标签下的meta-data值。
*
* @param key
* <application>标签下的meta-data健
*/
fun getApplicationMetaData(key: String): String? {
var applicationInfo: ApplicationInfo? = null
try {
applicationInfo = BaseApplication.getContext().packageManager.getApplicationInfo(
appPackage, PackageManager.GET_META_DATA)
} catch (e: PackageManager.NameNotFoundException) {
Logs.e(e, e.message)
}
if (applicationInfo == null) return ""
return applicationInfo.metaData.getString(key)
}
/**
* 判断某个应用是否安装。
* @param packageName
* 要检查是否安装的应用包名
* @return 安装返回true,否则返回false。
*/
fun isInstalled(packageName: String): Boolean {
val packageInfo: PackageInfo? = try {
BaseApplication.getContext().packageManager.getPackageInfo(packageName, 0)
} catch (e: PackageManager.NameNotFoundException) {
null
}
return packageInfo != null
}
/**
* 获取当前应用程序的图标。
*/
fun getAppIcon(): Drawable {
val packageManager = BaseApplication.getContext().packageManager
val applicationInfo = packageManager.getApplicationInfo(appPackage, 0)
return packageManager.getApplicationIcon(applicationInfo)
}
/**
* 判断手机是否安装了QQ。
*/
fun isQQInstalled() = isInstalled("com.tencent.mobileqq")
/**
* 判断手机是否安装了微信。
*/
fun isWechatInstalled() = isInstalled("com.tencent.mm")
/**
* 判断手机是否安装了微博。
* */
fun isWeiboInstalled() = isInstalled("com.sina.weibo")
var isLogin = false
@RequiresApi(Build.VERSION_CODES.M)
private fun isIgnoringBatteryOptimizations(context: Context): Boolean {
var isIgnoring = false
val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager?
if (powerManager != null) {
isIgnoring = powerManager.isIgnoringBatteryOptimizations(appPackage)
}
return isIgnoring
}
/**
* 将应用加入后台白名单,提高应用存活率
*/
@SuppressLint("BatteryLife")
@RequiresApi(api = Build.VERSION_CODES.M)
fun requestIgnoreBatteryOptimizations(context: Context) {
if (isIgnoringBatteryOptimizations(context)) {
return
}
kotlin.runCatching {
val intent = Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
intent.data = Uri.parse("package:$appPackage")
context.startActivity(intent)
}.onFailure {
it.printStackTrace()
}
}
fun goToSetting(context: Context) {
if (Build.BRAND == null) {
return
}
when(Build.BRAND.lowercase(Locale.getDefault())) {
"realme" -> goOPPOSetting(context)
"oppo" -> goOPPOSetting(context)
"huawei" -> goHuaWeiSetting(context)
"honor" -> goHuaWeiSetting(context)
"xiaomi" -> goXiaoMiSetting(context)
"vivo" -> goVivoSetting(context)
"meizu" -> goFlyeMeSetting(context)
"samsung" -> goSamSungSetting(context)
}
}
private fun goOPPOSetting(context: Context) {
var intent = Intent()
try {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("packageName", appPackage)
val comp = ComponentName(
"com.color.safecenter",
"com.color.safecenter.permission.PermissionManagerActivity"
)
intent.component = comp
context.startActivity(intent)
} catch (e: Exception) {
try {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.putExtra("pkg_name", context.packageName)
intent.putExtra("class_name", "com.welab.notificationdemo.MainActivity")
val comp = ComponentName(
"com.coloros.notificationmanager",
"com.coloros.notificationmanager.AppDetailPreferenceActivity"
)
intent.component = comp
context.startActivity(intent)
} catch (e1: Exception) {
// 否则跳转到应用详情
intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
}
}
}
private fun goVivoSetting(context: Context) {
var localIntent: Intent
try {
if (Build.MODEL.contains("Y85") && !Build.MODEL.contains("Y85A") || Build.MODEL.contains(
"vivo Y53L"
)
) {
localIntent = Intent()
localIntent.setClassName(
"com.vivo.permissionmanager",
"com.vivo.permissionmanager.activity.PurviewTabActivity"
)
localIntent.putExtra("packagename", context.packageName)
localIntent.putExtra("tabId", "1")
context.startActivity(localIntent)
} else {
localIntent = Intent()
localIntent.setClassName(
"com.vivo.permissionmanager",
"com.vivo.permissionmanager.activity.SoftPermissionDetailActivity"
)
localIntent.action = "secure.intent.action.softPermissionDetail"
localIntent.putExtra("packagename", context.packageName)
context.startActivity(localIntent)
}
} catch (e: Exception) {
// 否则跳转到应用详情
localIntent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
localIntent.data = uri
context.startActivity(localIntent)
}
}
private fun goHuaWeiSetting(context: Context) {
var componentName: ComponentName? = null
val sdkVersion = Build.VERSION.SDK_INT
try {
val intent = Intent()
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//跳自启动管理
if (sdkVersion >= 28) { //9:已测试
componentName =
ComponentName.unflattenFromString("com.huawei.systemmanager/.startupmgr.ui.StartupNormalAppListActivity") //跳自启动管理
} else if (sdkVersion >= 26) { //8:已测试
componentName =
ComponentName.unflattenFromString("com.huawei.systemmanager/.appcontrol.activity.StartupAppControlActivity")
} else if (sdkVersion >= 23) { //7.6:已测试
componentName =
ComponentName.unflattenFromString("com.huawei.systemmanager/.startupmgr.ui.StartupNormalAppListActivity")
} else if (sdkVersion >= 21) { //5
componentName =
ComponentName.unflattenFromString("com.huawei.systemmanager/com.huawei.permissionmanager.ui.MainActivity")
}
//componentName = new ComponentName("com.huawei.systemmanager", "com.huawei.systemmanager.optimize.process.ProtectActivity");//锁屏清理
intent.component = componentName
context.startActivity(intent)
} catch (e: Exception) {
//跳转失败
// 否则跳转到应用详情
val intentDetail = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intentDetail.data = uri
context.startActivity(intentDetail)
}
}
private fun goXiaoMiSetting(context: Context) {
try {
// MIUI 8
val localIntent = Intent("miui.intent.action.APP_PERM_EDITOR")
localIntent.setClassName(
"com.miui.securitycenter",
"com.miui.permcenter.permissions.PermissionsEditorActivity"
)
localIntent.putExtra("extra_pkgname", context.packageName)
context.startActivity(localIntent)
} catch (e: Exception) {
try {
// MIUI 5/6/7
val localIntent = Intent("miui.intent.action.APP_PERM_EDITOR")
localIntent.setClassName(
"com.miui.securitycenter",
"com.miui.permcenter.permissions.AppPermissionsEditorActivity"
)
localIntent.putExtra("extra_pkgname", context.packageName)
context.startActivity(localIntent)
} catch (e1: Exception) {
// 否则跳转到应用详情
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intent.data = uri
context.startActivity(intent)
}
}
}
private fun goSamSungSetting(context: Context) {
val intent = Intent()
var componentName: ComponentName? = null
try {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//跳自启动管理
componentName =
ComponentName.unflattenFromString("com.samsung.android.sm/.app.dashboard.SmartManagerDashBoardActivity")
intent.component = componentName
context.startActivity(intent)
} catch (e: Exception) {
try {
componentName = ComponentName(
"com.samsung.android.sm_cn",
"com.samsung.android.sm.ui.ram.AutoRunActivity"
)
intent.component = componentName
context.startActivity(intent)
} catch (e1: Exception) {
//跳转失败
// 否则跳转到应用详情
val intentDetail = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intentDetail.data = uri
context.startActivity(intentDetail)
}
}
}
private fun goFlyeMeSetting(context: Context) {
val intent = Intent()
var componentName: ComponentName? = null
try {
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
//跳自启动管理
componentName =
ComponentName.unflattenFromString("com.meizu.safe/.permission.SmartBGActivity") //跳转到后台管理页面
intent.component = componentName
context.startActivity(intent)
} catch (e: Exception) {
try {
componentName =
ComponentName.unflattenFromString("com.meizu.safe/.permission.PermissionMainActivity") //跳转到手机管家
intent.component = componentName
context.startActivity(intent)
} catch (e1: Exception) {
//跳转失败
// 否则跳转到应用详情
val intentDetail = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS)
val uri = Uri.fromParts("package", context.packageName, null)
intentDetail.data = uri
context.startActivity(intentDetail)
}
}
}
fun installApk(context: Context, downloadApk: String) {
val intent = Intent(Intent.ACTION_VIEW)
val file = File(downloadApk)
Logs.d("安装路径==$downloadApk")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
val apkUri: Uri = FileProvider.getUriForFile(
context,
"$appPackage.fileprovider",
file
)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.setDataAndType(apkUri, "application/vnd.android.package-archive")
} else {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
val uri = Uri.fromFile(file)
intent.setDataAndType(uri, "application/vnd.android.package-archive")
}
context.startActivity(intent)
}
}
@@ -0,0 +1,72 @@
package com.bbgo.common_base.util
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.annotation.FloatRange
import androidx.annotation.IntRange
/**
* @author chenxz
* @date 2019/11/24
* @desc ColorUtil
*/
object ColorUtil {
/**
* 计算颜色
*
* @param color color值
* @param alpha alpha值
* @return 最终的状态栏颜色
*/
fun alphaColor(@ColorInt color: Int, @IntRange(from = 0, to = 255) alpha: Int): Int {
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
return Color.argb(alpha, red, green, blue)
}
/**
* 计算颜色
*
* @param color color值
* @param alpha alpha值[0-1]
* @return 最终的状态栏颜色
*/
fun alphaColor(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int {
return alphaColor(color, (alpha * 255).toInt())
}
/**
* 根据fraction值来计算当前的颜色
*
* @param colorFrom 起始颜色
* @param colorTo 结束颜色
* @param fraction 变量
* @return 当前颜色
*/
fun changingColor(@ColorInt colorFrom: Int, @ColorInt colorTo: Int, @FloatRange(from = 0.0, to = 1.0) fraction: Float): Int {
val redStart = Color.red(colorFrom)
val blueStart = Color.blue(colorFrom)
val greenStart = Color.green(colorFrom)
val alphaStart = Color.alpha(colorFrom)
val redEnd = Color.red(colorTo)
val blueEnd = Color.blue(colorTo)
val greenEnd = Color.green(colorTo)
val alphaEnd = Color.alpha(colorTo)
val redDifference = redEnd - redStart
val blueDifference = blueEnd - blueStart
val greenDifference = greenEnd - greenStart
val alphaDifference = alphaEnd - alphaStart
val redCurrent = (redStart + fraction * redDifference).toInt()
val blueCurrent = (blueStart + fraction * blueDifference).toInt()
val greenCurrent = (greenStart + fraction * greenDifference).toInt()
val alphaCurrent = (alphaStart + fraction * alphaDifference).toInt()
return Color.argb(alphaCurrent, redCurrent, greenCurrent, blueCurrent)
}
}
@@ -0,0 +1,102 @@
package com.bbgo.common_base.util
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import com.bbgo.common_base.BaseApplication
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
object DataStoreUtils {
/**
* dataStore 存入数据默认就是异步,没有同步方法
* 取数据异步通过Flow实现
*/
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
/**
* 写入数据默认就是异步
*/
suspend fun put(key: String, value: Any) {
when(value) {
is Int -> putInt(key, value)
is Long -> putLong(key, value)
is Float -> putFloat(key, value)
is String -> putString(key, value)
is Boolean -> putBoolean(key, value)
}
}
private suspend fun putInt(key: String, value: Int) {
BaseApplication.getContext().dataStore.edit { settings ->
settings[intPreferencesKey(key)] = value
}
}
private suspend fun putLong(key: String, value: Long) {
BaseApplication.getContext().dataStore.edit { settings ->
settings[longPreferencesKey(key)] = value
}
}
private suspend fun putFloat(key: String, value: Float) {
BaseApplication.getContext().dataStore.edit { settings ->
settings[floatPreferencesKey(key)] = value
}
}
private suspend fun putString(key: String, value: String) {
BaseApplication.getContext().dataStore.edit { settings ->
settings[stringPreferencesKey(key)] = value
}
}
private suspend fun putBoolean(key: String, value: Boolean) {
BaseApplication.getContext().dataStore.edit { settings ->
settings[booleanPreferencesKey(key)] = value
}
}
/************************************************************************************/
/**
* 获取数据是同步还是异步由启动协程决定
*/
fun getInt(key: String, defaultValue: Int) : Flow<Int> {
return BaseApplication.getContext().dataStore.data
.map { preferences ->
preferences[intPreferencesKey(key)] ?: defaultValue
}
}
fun getLong(key: String, defaultValue: Long) : Flow<Long> {
return BaseApplication.getContext().dataStore.data
.map { preferences ->
preferences[longPreferencesKey(key)] ?: defaultValue
}
}
fun getFloat(key: String, defaultValue: Float) : Flow<Float> {
return BaseApplication.getContext().dataStore.data
.map { preferences ->
preferences[floatPreferencesKey(key)] ?: defaultValue
}
}
fun getString(key: String, defaultValue: String) : Flow<String> {
return BaseApplication.getContext().dataStore.data
.map { preferences ->
preferences[stringPreferencesKey(key)] ?: defaultValue
}
}
fun getBoolean(key: String, defaultValue: Boolean) : Flow<Boolean> {
return BaseApplication.getContext().dataStore.data
.map { preferences ->
preferences[booleanPreferencesKey(key)] ?: defaultValue
}
}
}
@@ -0,0 +1,165 @@
package com.bbgo.common_base.util
import android.app.ProgressDialog
import android.content.Context
import android.content.DialogInterface
import android.text.TextUtils
import androidx.appcompat.app.AlertDialog
/**
* Created by chenxz on 2018/6/9.
* 对话框辅助类,需要自己调用show方法
*/
object DialogUtil {
/**
* 获取一个Dialog
*
* @param context
* @return
*/
fun getDialog(context: Context): AlertDialog.Builder {
return AlertDialog.Builder(context)
}
/**
* 获取一个耗时的对话框 ProgressDialog
*
* @param context
* @param message
* @return
*/
fun getWaitDialog(context: Context, message: String): ProgressDialog {
val waitDialog = ProgressDialog(context)
if (!TextUtils.isEmpty(message)) {
waitDialog.setMessage(message)
}
return waitDialog
}
/**
* 获取一个信息对话框,注意需要自己手动调用show方法
*
* @param context
* @param message
* @param onClickListener
* @return
*/
@JvmOverloads
fun getMessageDialog(context: Context, message: String,
onClickListener: DialogInterface.OnClickListener? = null): AlertDialog.Builder {
val builder = getDialog(context)
builder.setMessage(message)
builder.setPositiveButton("确定", onClickListener)
return builder
}
fun getConfirmDialog(context: Context, message: String,
onClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
val builder = getDialog(context)
builder.setMessage(message)
builder.setPositiveButton("确定", onClickListener)
builder.setNegativeButton("取消", null)
return builder
}
fun getConfirmDialog(context: Context, message: String,
onOKClickListener: DialogInterface.OnClickListener, onCancleClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
val builder = getDialog(context)
builder.setMessage(message)
builder.setPositiveButton("确定", onOKClickListener)
builder.setNegativeButton("取消", onCancleClickListener)
return builder
}
fun getSelectDialog(context: Context, title: String, arrays: Array<String>,
onClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
val builder = getDialog(context)
builder.setItems(arrays, onClickListener)
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title)
}
builder.setNegativeButton("取消", null)
return builder
}
fun getSelectDialog(context: Context, arrays: Array<String>,
onClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
return getSelectDialog(context, "", arrays, onClickListener)
}
/**
* 获取一个单选的对话框
*
* @param context
* @param title
* @param arrays
* @param selectIndex
* @param onClickListener
* @param onOKClickListener
* 点击确定的回调接口
* @param onCancleClickListener
* 点击取消的回调接口
* @return
*/
fun getSingleChoiceDialog(context: Context, title: String, arrays: Array<String>,
selectIndex: Int, onClickListener: DialogInterface.OnClickListener,
onOKClickListener: DialogInterface.OnClickListener, onCancleClickListener: DialogInterface.OnClickListener? = null): AlertDialog.Builder {
val builder = getDialog(context)
builder.setSingleChoiceItems(arrays, selectIndex, onClickListener)
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title)
}
builder.setPositiveButton("确定", onOKClickListener)
builder.setNegativeButton("取消", onCancleClickListener)
return builder
}
fun getSingleChoiceDialog(context: Context, title: String, arrays: Array<String>,
selectIndex: Int, onClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
val builder = getDialog(context)
builder.setSingleChoiceItems(arrays, selectIndex, onClickListener)
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title)
}
builder.setPositiveButton("取消", null)
return builder
}
fun getSingleChoiceDialog(context: Context, arrays: Array<String>, selectIndex: Int,
onClickListener: DialogInterface.OnClickListener, onOKClickListener: DialogInterface.OnClickListener,
onCancleClickListener: DialogInterface.OnClickListener): AlertDialog.Builder {
return getSingleChoiceDialog(context, "", arrays, selectIndex, onClickListener, onOKClickListener,
onCancleClickListener)
}
/**
* 获取一个多选的对话框
*
* @param context
* @param title
* @param arrays
* @param checkedItems
* @param onMultiChoiceClickListener
* @param onOKClickListener
* 点击确定的回调接口
* @param onCancleListener
* 点击取消的回调接口
* @return
*/
fun getMultiChoiceDialog(context: Context, title: String, arrays: Array<String>,
checkedItems: BooleanArray, onMultiChoiceClickListener: DialogInterface.OnMultiChoiceClickListener,
onOKClickListener: DialogInterface.OnClickListener, onCancleListener: DialogInterface.OnClickListener): AlertDialog.Builder {
val builder = getDialog(context)
builder.setMultiChoiceItems(arrays, checkedItems, onMultiChoiceClickListener)
if (!TextUtils.isEmpty(title)) {
builder.setTitle(title)
}
builder.setPositiveButton("确定", onOKClickListener)
builder.setNegativeButton("取消", onCancleListener)
return builder
}
}
@@ -0,0 +1,154 @@
package com.bbgo.common_base.util
import android.os.Environment
import android.os.Environment.MEDIA_MOUNTED
import com.bbgo.common_base.BaseApplication
import java.io.File
import java.math.BigDecimal
class FileUtils {
companion object {
/**
* 外置存储卡的路径
* /storage/emulated/0
*/
fun getExternalStorePath(): String? {
return if (isExistExternalStore()) {
Environment.getExternalStorageDirectory().absolutePath
} else null
}
/**
* 外置存储卡文件路径
* /storage/emulated/0/Android/data/com.bbgo.wanandroid/files
*/
@JvmStatic
fun getExternalFilePath(): String? {
return BaseApplication.getContext().getExternalFilesDir(null)?.absolutePath
}
/**
* 外置存储卡缓存的路径
* /storage/emulated/0/Android/data/com.bbgo.wanandroid/cache
*/
fun getExternalCachePath(): String? {
return if (isExistExternalStore()) {
return BaseApplication.getContext().externalCacheDir?.absolutePath
} else null
}
/**
* 内置存储卡文件的路径
* /data/user/0/com.bbgo.wanandroid/files
*/
fun getInternalFilePath(): String {
return BaseApplication.getContext().filesDir.absolutePath
}
/**
* 内置存储卡缓存的路径
* /data/user/0/com.bbgo.wanandroid/cache
*/
fun getInternalCachePath(): String {
return BaseApplication.getContext().cacheDir.absolutePath
}
/**
* 优先使用外置存储卡,没有则使用内部存储卡
*/
fun getStoreFilePath(): String? {
return if (isExistExternalStore()) {
getExternalFilePath()
} else {
getInternalFilePath()
}
}
/**
* 优先使用外置存储卡,没有则使用内部存储卡
*/
fun getStoreCachePath(): String? {
return if (isExistExternalStore()) {
getExternalCachePath()
} else {
getInternalCachePath()
}
}
/**
* 获取指定路径
*/
fun getFilePath(dir: String): String? {
val filePath = if (isExistExternalStore()) {
BaseApplication.getContext().getExternalFilesDir(dir)?.absolutePath
} else {
BaseApplication.getContext().filesDir.absolutePath + File.separator + dir
}
val file = File(filePath)
if (!file.exists()) {
file.mkdir()
}
return filePath
}
/**
* 是否有外存卡
*/
fun isExistExternalStore(): Boolean {
return Environment.getExternalStorageState() == MEDIA_MOUNTED
}
/**
* 格式化单位
*/
fun formatSize(size: Double): String {
val kiloByte = size / 1024
if (kiloByte < 1) {
return "0KB"
}
val megaByte = kiloByte / 1024
if (megaByte < 1) {
val result1 = BigDecimal(java.lang.Double.toString(kiloByte))
return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB"
}
val gigaByte = megaByte / 1024
if (gigaByte < 1) {
val result2 = BigDecimal(java.lang.Double.toString(megaByte))
return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB"
}
val teraBytes = gigaByte / 1024
if (teraBytes < 1) {
val result3 = BigDecimal(java.lang.Double.toString(gigaByte))
return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB"
}
val result4 = BigDecimal(teraBytes)
return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB"
}
/**
* 获取文件名,不包含后缀
*
* @param file 文件
* @return 文件名
*/
fun getFileNameNotSuffix(file: File?): String {
return if (file == null || !file.exists()) {
""
} else getStringNotSuffix(file.name)
}
/**
* 获取字符串,不包含后缀
*
* @param str 字符串 适用于文件名
* @return 文件名
*/
fun getStringNotSuffix(str: String): String {
val indexOf = str.lastIndexOf(".")
return str.substring(0, if (indexOf < 0) str.length - 1 else indexOf)
}
}
}
@@ -0,0 +1,40 @@
package com.bbgo.common_base.util
import android.content.Context
import android.widget.ImageView
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.R
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
/**
* Created by chenxz on 2018/6/12.
*/
object ImageLoader {
// 1.开启无图模式 2.非WiFi环境 不加载图片
private val isLoadImage = !SettingUtil.getIsNoPhotoMode() || NetWorkUtil.isWifi(BaseApplication.getContext())
/**
* 加载图片
* @param context
* @param url
* @param iv
*/
fun load(context: Context, url: String?, iv: ImageView) {
if (isLoadImage) {
val options = RequestOptions()
.diskCacheStrategy(DiskCacheStrategy.DATA)
.placeholder(R.drawable.bg_placeholder)
Glide.with(context)
.load(url)
.transition(DrawableTransitionOptions().crossFade())
.apply(options)
.into(iv)
}
}
}
@@ -0,0 +1,45 @@
package com.bbgo.common_base.util
import java.security.MessageDigest
import java.util.*
/**
* @Description:
* @Author: wangyuebin
* @Date: 2021/8/27 4:33 下午
*/
class MD5Utils {
companion object {
//生成MD5
fun getMD5(message: String): String {
var md5 = ""
try {
val md = MessageDigest.getInstance("MD5") // 创建一个md5算法对象
val messageByte = message.toByteArray(charset("UTF-8"))
val md5Byte = md.digest(messageByte) // 获得MD5字节数组,16*8=128位
md5 = bytesToHex(md5Byte) // 转换为16进制字符串
} catch (e: Exception) {
e.printStackTrace()
}
return md5
}
// 二进制转十六进制
private fun bytesToHex(bytes: ByteArray): String {
val hexStr = StringBuffer()
var num: Int
for (i in bytes.indices) {
num = bytes[i].toInt()
if (num < 0) {
num += 256
}
if (num < 16) {
hexStr.append("0")
}
hexStr.append(Integer.toHexString(num))
}
return hexStr.toString().uppercase(Locale.getDefault())
}
}
}
@@ -0,0 +1,161 @@
package com.bbgo.common_base.util
import android.content.Context
import android.net.ConnectivityManager
import android.telephony.TelephonyManager
import java.io.IOException
import java.net.HttpURLConnection
import java.net.NetworkInterface
import java.net.SocketException
import java.net.URL
/**
* Created by chenxz on 2018/4/21.
*/
class NetWorkUtil {
companion object {
var NET_CNNT_BAIDU_OK = 1 // NetworkAvailable
var NET_CNNT_BAIDU_TIMEOUT = 2 // no NetworkAvailable
var NET_NOT_PREPARE = 3 // Net no ready
var NET_ERROR = 4 //net error
private val TIMEOUT = 3000 // TIMEOUT
/**
* check NetworkAvailable
*
* @param context
* @return
*/
@JvmStatic
fun isNetworkAvailable(context: Context): Boolean {
val manager = context.applicationContext.getSystemService(
Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val info = manager.activeNetworkInfo
return !(null == info || !info.isAvailable)
}
/**
* check NetworkConnected
*
* @param context
* @return
*/
fun isNetworkConnected(context: Context): Boolean {
val manager = context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val info = manager.activeNetworkInfo
return !(null == info || !info.isConnected)
}
/**
* 得到ip地址
*
* @return
*/
@JvmStatic
fun getLocalIpAddress(): String {
var ret = ""
try {
val en = NetworkInterface.getNetworkInterfaces()
while (en.hasMoreElements()) {
val enumIpAddress = en.nextElement().inetAddresses
while (enumIpAddress.hasMoreElements()) {
val netAddress = enumIpAddress.nextElement()
if (!netAddress.isLoopbackAddress) {
ret = netAddress.hostAddress.toString()
}
}
}
} catch (ex: SocketException) {
ex.printStackTrace()
}
return ret
}
/**
* ping "http://www.baidu.com"
*
* @return
*/
@JvmStatic
private fun pingNetWork(): Boolean {
var result = false
var httpUrl: HttpURLConnection? = null
try {
httpUrl = URL("http://www.baidu.com")
.openConnection() as HttpURLConnection
httpUrl.connectTimeout = TIMEOUT
httpUrl.connect()
result = true
} catch (e: IOException) {
} finally {
if (null != httpUrl) {
httpUrl.disconnect()
}
}
return result
}
/**
* check is3G
*
* @param context
* @return boolean
*/
@JvmStatic
fun is3G(context: Context): Boolean {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetInfo = connectivityManager.activeNetworkInfo
return activeNetInfo != null && activeNetInfo.type == ConnectivityManager.TYPE_MOBILE
}
/**
* isWifi
*
* @param context
* @return boolean
*/
@JvmStatic
fun isWifi(context: Context): Boolean {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetInfo = connectivityManager.activeNetworkInfo
return activeNetInfo != null && activeNetInfo.type == ConnectivityManager.TYPE_WIFI
}
/**
* is2G
*
* @param context
* @return boolean
*/
@JvmStatic
fun is2G(context: Context): Boolean {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetInfo = connectivityManager.activeNetworkInfo
return activeNetInfo != null && (activeNetInfo.subtype == TelephonyManager.NETWORK_TYPE_EDGE
|| activeNetInfo.subtype == TelephonyManager.NETWORK_TYPE_GPRS || activeNetInfo
.subtype == TelephonyManager.NETWORK_TYPE_CDMA)
}
/**
* 判断MOBILE网络是否可用
*/
fun isMobile(context: Context?): Boolean {
if (context != null) {
//获取手机所有连接管理对象(包括对wi-fi,net等连接的管理)
val manager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
//获取NetworkInfo对象
val networkInfo = manager.activeNetworkInfo
//判断NetworkInfo对象是否为空 并且类型是否为MOBILE
if (null != networkInfo && networkInfo.type == ConnectivityManager.TYPE_MOBILE)
return networkInfo.isAvailable
}
return false
}
}
}
@@ -0,0 +1,101 @@
package com.bbgo.common_base.util
import android.os.Build
import android.text.TextUtils
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
/**
* Created by wangyb on 2018/4/21.
*/
object RomUtil {
internal object AvailableRomType {
val MIUI = 1
val FLYME = 2
val ANDROID_NATIVE = 3
val NA = 4
}
fun isLightStatusBarAvailable(): Boolean {
return isMIUIV6OrAbove() || isFlymeV4OrAbove() || isAndroidMOrAbove()
}
fun getLightStatausBarAvailableRomType(): Int {
if (isMIUIV6OrAbove()) {
return AvailableRomType.MIUI
}
if (isFlymeV4OrAbove()) {
return AvailableRomType.FLYME
}
return if (isAndroidMOrAbove()) {
AvailableRomType.ANDROID_NATIVE
} else AvailableRomType.NA
}
//Flyme V4的displayId格式为 [Flyme OS 4.x.x.xA]
//Flyme V5的displayId格式为 [Flyme 5.x.x.x beta]
private fun isFlymeV4OrAbove(): Boolean {
val displayId = Build.DISPLAY
if (!TextUtils.isEmpty(displayId) && displayId.contains("Flyme")) {
val displayIdArray = displayId.split(" ".toRegex()).dropLastWhile({ it.isEmpty() }).toTypedArray()
for (temp in displayIdArray) {
//版本号4以上,形如4.x.
if (temp.matches("^[4-9]\\.(\\d+\\.)+\\S*".toRegex())) {
return true
}
}
}
return false
}
//MIUI V6对应的versionCode是4
//MIUI V7对应的versionCode是5
private fun isMIUIV6OrAbove(): Boolean {
val miuiVersionCodeStr = getSystemProperty("ro.miui.ui.version.code")
if (!TextUtils.isEmpty(miuiVersionCodeStr)) {
try {
val miuiVersionCode = Integer.parseInt(miuiVersionCodeStr)
if (miuiVersionCode >= 4) {
return true
}
} catch (e: Exception) {
}
}
return false
}
//Android Api 23以上
private fun isAndroidMOrAbove(): Boolean {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
}
private fun getSystemProperty(propName: String): String? {
val line: String
var input: BufferedReader? = null
try {
val p = Runtime.getRuntime().exec("getprop $propName")
input = BufferedReader(InputStreamReader(p.inputStream), 1024)
line = input.readLine()
input.close()
} catch (ex: IOException) {
return null
} finally {
if (input != null) {
try {
input.close()
} catch (e: IOException) {
}
}
}
return line
}
}
@@ -0,0 +1,106 @@
package com.bbgo.common_base.util
import android.graphics.Color
import com.bbgo.common_base.BaseApplication
import com.bbgo.common_base.R
import com.bbgo.common_base.ext.Prefs
/**
* author: wangyb
* date: 2021/5/24 2:19 下午
* description: todo
*/
object SettingUtil {
/**
* 获取是否开启无图模式
*/
fun getIsNoPhotoMode(): Boolean {
return Prefs.getBoolean("switch_noPhotoMode", false) //&& NetWorkUtil.isMobile(App.context)
}
/**
* 获取是否开启显示首页置顶文章,true: 不显示 false: 显示
*/
fun getIsShowTopArticle(): Boolean {
return Prefs.getBoolean("switch_show_top", false)
}
/**
* 获取主题颜色
*/
fun getColor(): Int {
val defaultColor = BaseApplication.getContext().resources.getColor(R.color.colorPrimary)
val color = Prefs.getInt("color", defaultColor)
return if (color != 0 && Color.alpha(color) != 255) {
defaultColor
} else color
}
/**
* 设置主题颜色
*/
fun setColor(color: Int) {
Prefs.putInt("color", color)
}
/**
* 获取是否开启导航栏上色
*/
fun getNavBar(): Boolean {
return Prefs.getBoolean("nav_bar", false)
}
/**
* 设置夜间模式
*/
fun setIsNightMode(flag: Boolean) {
Prefs.putBoolean("switch_nightMode", flag)
}
/**
* 获取是否开启夜间模式
*/
fun getIsNightMode(): Boolean {
return Prefs.getBoolean("switch_nightMode", false)
}
/**
* 获取是否开启自动切换夜间模式
*/
fun getIsAutoNightMode(): Boolean {
return Prefs.getBoolean("auto_nightMode", false)
}
fun getNightStartHour(): String {
return Prefs.getString("night_startHour", "22")
}
fun setNightStartHour(nightStartHour: String) {
Prefs.putString("night_startHour", nightStartHour)
}
fun getNightStartMinute(): String {
return Prefs.getString("night_startMinute", "00")
}
fun setNightStartMinute(nightStartMinute: String) {
Prefs.putString("night_startMinute", nightStartMinute)
}
fun getDayStartHour(): String {
return Prefs.getString("day_startHour", "06")
}
fun setDayStartHour(dayStartHour: String) {
Prefs.putString("day_startHour", dayStartHour)
}
fun getDayStartMinute(): String {
return Prefs.getString("day_startMinute", "00")
}
fun setDayStartMinute(dayStartMinute: String) {
Prefs.putString("day_startMinute", dayStartMinute)
}
}
@@ -0,0 +1,804 @@
package com.bbgo.common_base.util
import android.annotation.TargetApi
import android.app.Activity
import android.content.Context
import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.MarginLayoutParams
import android.view.Window
import android.view.WindowManager
import android.widget.LinearLayout
import androidx.annotation.ColorInt
import androidx.annotation.IntRange
import androidx.annotation.NonNull
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.drawerlayout.widget.DrawerLayout
import com.bbgo.common_base.R
/**
* Created by Jaeger on 16/2/14.
*
*
* Email: chjie.jaeger@gmail.com
* GitHub: https://github.com/laobie
*/
object StatusBarUtil {
const val DEFAULT_STATUS_BAR_ALPHA = 112
private val FAKE_STATUS_BAR_VIEW_ID = R.id.statusbarutil_fake_status_bar_view
private val FAKE_TRANSLUCENT_VIEW_ID = R.id.statusbarutil_translucent_view
private const val TAG_KEY_HAVE_SET_OFFSET = -123
/**
* 设置状态栏颜色
*
* @param activity 需要设置的 activity
* @param color 状态栏颜色值
*/
fun setColor(activity: Activity, @ColorInt color: Int) {
setColor(activity, color, DEFAULT_STATUS_BAR_ALPHA)
}
/**
* 设置状态栏颜色
*
* @param activity 需要设置的activity
* @param color 状态栏颜色值
* @param statusBarAlpha 状态栏透明度
*/
fun setColor(
activity: Activity,
@ColorInt color: Int,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
activity.window.statusBarColor =
calculateStatusColor(color, statusBarAlpha)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
val decorView = activity.window.decorView as ViewGroup
val fakeStatusBarView = decorView.findViewById<View>(
FAKE_STATUS_BAR_VIEW_ID
)
if (fakeStatusBarView != null) {
if (fakeStatusBarView.visibility == View.GONE) {
fakeStatusBarView.visibility = View.VISIBLE
}
fakeStatusBarView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha))
} else {
decorView.addView(createStatusBarView(activity, color, statusBarAlpha))
}
setRootView(activity)
}
}
/**
* 为滑动返回界面设置状态栏颜色
*
* @param activity 需要设置的activity
* @param color 状态栏颜色值
*/
fun setColorForSwipeBack(activity: Activity, color: Int) {
setColorForSwipeBack(activity, color, DEFAULT_STATUS_BAR_ALPHA)
}
/**
* 为滑动返回界面设置状态栏颜色
*
* @param activity 需要设置的activity
* @param color 状态栏颜色值
* @param statusBarAlpha 状态栏透明度
*/
fun setColorForSwipeBack(
activity: Activity, @ColorInt color: Int,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
val contentView = activity.findViewById<View>(android.R.id.content) as ViewGroup
val rootView = contentView.getChildAt(0)
val statusBarHeight = getStatusBarHeight(activity)
if (rootView != null && rootView is CoordinatorLayout) {
val coordinatorLayout: CoordinatorLayout = rootView as CoordinatorLayout
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
coordinatorLayout.fitsSystemWindows = false
contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha))
val isNeedRequestLayout = contentView.paddingTop < statusBarHeight
if (isNeedRequestLayout) {
contentView.setPadding(0, statusBarHeight, 0, 0)
coordinatorLayout.post(Runnable { coordinatorLayout.requestLayout() })
}
} else {
coordinatorLayout.setStatusBarBackgroundColor(
calculateStatusColor(
color,
statusBarAlpha
)
)
}
} else {
contentView.setPadding(0, statusBarHeight, 0, 0)
contentView.setBackgroundColor(calculateStatusColor(color, statusBarAlpha))
}
setTransparentForWindow(activity)
}
}
/**
* 设置状态栏纯色 不加半透明效果
*
* @param activity 需要设置的 activity
* @param color 状态栏颜色值
*/
fun setColorNoTranslucent(activity: Activity, @ColorInt color: Int) {
setColor(activity, color, 0)
}
/**
* 设置状态栏颜色(5.0以下无半透明效果,不建议使用)
*
* @param activity 需要设置的 activity
* @param color 状态栏颜色值
*/
@Deprecated("")
fun setColorDiff(activity: Activity, @ColorInt color: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
transparentStatusBar(activity)
val contentView = activity.findViewById<View>(android.R.id.content) as ViewGroup
// 移除半透明矩形,以免叠加
val fakeStatusBarView = contentView.findViewById<View>(
FAKE_STATUS_BAR_VIEW_ID
)
if (fakeStatusBarView != null) {
if (fakeStatusBarView.visibility == View.GONE) {
fakeStatusBarView.visibility = View.VISIBLE
}
fakeStatusBarView.setBackgroundColor(color)
} else {
contentView.addView(createStatusBarView(activity, color))
}
setRootView(activity)
}
/**
* 使状态栏半透明
*
*
* 适用于图片作为背景的界面,此时需要图片填充到状态栏
*
* @param activity 需要设置的activity
*/
fun setTranslucent(activity: Activity) {
setTranslucent(activity, DEFAULT_STATUS_BAR_ALPHA)
}
/**
* 使状态栏半透明
*
*
* 适用于图片作为背景的界面,此时需要图片填充到状态栏
*
* @param activity 需要设置的activity
* @param statusBarAlpha 状态栏透明度
*/
fun setTranslucent(activity: Activity, @IntRange(from = 0, to = 255) statusBarAlpha: Int) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
setTransparent(activity)
addTranslucentView(activity, statusBarAlpha)
}
/**
* 针对根布局是 CoordinatorLayout, 使状态栏半透明
*
*
* 适用于图片作为背景的界面,此时需要图片填充到状态栏
*
* @param activity 需要设置的activity
* @param statusBarAlpha 状态栏透明度
*/
fun setTranslucentForCoordinatorLayout(
activity: Activity,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
transparentStatusBar(activity)
addTranslucentView(activity, statusBarAlpha)
}
/**
* 设置状态栏全透明
*
* @param activity 需要设置的activity
*/
fun setTransparent(activity: Activity) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
transparentStatusBar(activity)
setRootView(activity)
}
/**
* 使状态栏透明(5.0以上半透明效果,不建议使用)
*
*
* 适用于图片作为背景的界面,此时需要图片填充到状态栏
*
* @param activity 需要设置的activity
*/
@Deprecated("")
fun setTranslucentDiff(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 设置状态栏透明
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
setRootView(activity)
}
}
/**
* 为DrawerLayout 布局设置状态栏变色
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
* @param color 状态栏颜色值
*/
fun setColorForDrawerLayout(
activity: Activity,
drawerLayout: DrawerLayout,
@ColorInt color: Int
) {
setColorForDrawerLayout(activity, drawerLayout, color, DEFAULT_STATUS_BAR_ALPHA)
}
/**
* 为DrawerLayout 布局设置状态栏颜色,纯色
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
* @param color 状态栏颜色值
*/
fun setColorNoTranslucentForDrawerLayout(
activity: Activity,
drawerLayout: DrawerLayout,
@ColorInt color: Int
) {
setColorForDrawerLayout(activity, drawerLayout, color, 0)
}
/**
* 为DrawerLayout 布局设置状态栏变色
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
* @param color 状态栏颜色值
* @param statusBarAlpha 状态栏透明度
*/
fun setColorForDrawerLayout(
activity: Activity, drawerLayout: DrawerLayout, @ColorInt color: Int,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
activity.window.statusBarColor = Color.TRANSPARENT
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
}
// 生成一个状态栏大小的矩形
// 添加 statusBarView 到布局中
val contentLayout = drawerLayout.getChildAt(0) as ViewGroup
val fakeStatusBarView = contentLayout.findViewById<View>(
FAKE_STATUS_BAR_VIEW_ID
)
if (fakeStatusBarView != null) {
if (fakeStatusBarView.visibility == View.GONE) {
fakeStatusBarView.visibility = View.VISIBLE
}
fakeStatusBarView.setBackgroundColor(color)
} else {
contentLayout.addView(createStatusBarView(activity, color), 0)
}
// 内容布局不是 LinearLayout 时,设置padding top
if (contentLayout !is LinearLayout && contentLayout.getChildAt(1) != null) {
contentLayout.getChildAt(1)
.setPadding(
contentLayout.paddingLeft,
getStatusBarHeight(activity) + contentLayout.paddingTop,
contentLayout.paddingRight,
contentLayout.paddingBottom
)
}
// 设置属性
setDrawerLayoutProperty(drawerLayout, contentLayout)
addTranslucentView(activity, statusBarAlpha)
}
/**
* 设置 DrawerLayout 属性
*
* @param drawerLayout DrawerLayout
* @param drawerLayoutContentLayout DrawerLayout 的内容布局
*/
private fun setDrawerLayoutProperty(
drawerLayout: DrawerLayout,
drawerLayoutContentLayout: ViewGroup
) {
val drawer = drawerLayout.getChildAt(1) as ViewGroup
drawerLayout.setFitsSystemWindows(false)
drawerLayoutContentLayout.fitsSystemWindows = false
drawerLayoutContentLayout.clipToPadding = true
drawer.fitsSystemWindows = false
}
/**
* 为DrawerLayout 布局设置状态栏变色(5.0以下无半透明效果,不建议使用)
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
* @param color 状态栏颜色值
*/
@Deprecated("")
fun setColorForDrawerLayoutDiff(
activity: Activity,
drawerLayout: DrawerLayout,
@ColorInt color: Int
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
// 生成一个状态栏大小的矩形
val contentLayout = drawerLayout.getChildAt(0) as ViewGroup
val fakeStatusBarView = contentLayout.findViewById<View>(
FAKE_STATUS_BAR_VIEW_ID
)
if (fakeStatusBarView != null) {
if (fakeStatusBarView.visibility == View.GONE) {
fakeStatusBarView.visibility = View.VISIBLE
}
fakeStatusBarView.setBackgroundColor(
calculateStatusColor(
color,
DEFAULT_STATUS_BAR_ALPHA
)
)
} else {
// 添加 statusBarView 到布局中
contentLayout.addView(createStatusBarView(activity, color), 0)
}
// 内容布局不是 LinearLayout 时,设置padding top
if (contentLayout !is LinearLayout && contentLayout.getChildAt(1) != null) {
contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0)
}
// 设置属性
setDrawerLayoutProperty(drawerLayout, contentLayout)
}
}
/**
* 为 DrawerLayout 布局设置状态栏透明
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
*/
fun setTranslucentForDrawerLayout(activity: Activity, drawerLayout: DrawerLayout) {
setTranslucentForDrawerLayout(activity, drawerLayout, DEFAULT_STATUS_BAR_ALPHA)
}
/**
* 为 DrawerLayout 布局设置状态栏透明
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
*/
fun setTranslucentForDrawerLayout(
activity: Activity, drawerLayout: DrawerLayout,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
setTransparentForDrawerLayout(activity, drawerLayout)
addTranslucentView(activity, statusBarAlpha)
}
/**
* 为 DrawerLayout 布局设置状态栏透明
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
*/
fun setTransparentForDrawerLayout(activity: Activity, drawerLayout: DrawerLayout) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
activity.window.statusBarColor = Color.TRANSPARENT
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
}
val contentLayout = drawerLayout.getChildAt(0) as ViewGroup
// 内容布局不是 LinearLayout 时,设置padding top
if (contentLayout !is LinearLayout && contentLayout.getChildAt(1) != null) {
contentLayout.getChildAt(1).setPadding(0, getStatusBarHeight(activity), 0, 0)
}
// 设置属性
setDrawerLayoutProperty(drawerLayout, contentLayout)
}
/**
* 为 DrawerLayout 布局设置状态栏透明(5.0以上半透明效果,不建议使用)
*
* @param activity 需要设置的activity
* @param drawerLayout DrawerLayout
*/
@Deprecated("")
fun setTranslucentForDrawerLayoutDiff(activity: Activity, drawerLayout: DrawerLayout) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
// 设置状态栏透明
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
// 设置内容布局属性
val contentLayout = drawerLayout.getChildAt(0) as ViewGroup
contentLayout.fitsSystemWindows = true
contentLayout.clipToPadding = true
// 设置抽屉布局属性
val vg = drawerLayout.getChildAt(1) as ViewGroup
vg.fitsSystemWindows = false
// 设置 DrawerLayout 属性
drawerLayout.setFitsSystemWindows(false)
}
}
/**
* 为头部是 ImageView 的界面设置状态栏全透明
*
* @param activity 需要设置的activity
* @param needOffsetView 需要向下偏移的 View
*/
fun setTransparentForImageView(activity: Activity, needOffsetView: View?) {
setTranslucentForImageView(activity, 0, needOffsetView)
}
/**
* 为头部是 ImageView 的界面设置状态栏透明(使用默认透明度)
*
* @param activity 需要设置的activity
* @param needOffsetView 需要向下偏移的 View
*/
fun setTranslucentForImageView(activity: Activity, needOffsetView: View?) {
setTranslucentForImageView(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView)
}
/**
* 为头部是 ImageView 的界面设置状态栏透明
*
* @param activity 需要设置的activity
* @param statusBarAlpha 状态栏透明度
* @param needOffsetView 需要向下偏移的 View
*/
fun setTranslucentForImageView(
activity: Activity, @IntRange(from = 0, to = 255) statusBarAlpha: Int,
needOffsetView: View?
) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
return
}
setTransparentForWindow(activity)
addTranslucentView(activity, statusBarAlpha)
if (needOffsetView != null) {
val haveSetOffset = needOffsetView.getTag(TAG_KEY_HAVE_SET_OFFSET)
if (haveSetOffset != null && haveSetOffset as Boolean) {
return
}
val layoutParams = needOffsetView.layoutParams as MarginLayoutParams
layoutParams.setMargins(
layoutParams.leftMargin, layoutParams.topMargin + getStatusBarHeight(activity),
layoutParams.rightMargin, layoutParams.bottomMargin
)
needOffsetView.setTag(TAG_KEY_HAVE_SET_OFFSET, true)
}
}
/**
* 为 fragment 头部是 ImageView 的设置状态栏透明
*
* @param activity fragment 对应的 activity
* @param needOffsetView 需要向下偏移的 View
*/
fun setTranslucentForImageViewInFragment(activity: Activity, needOffsetView: View?) {
setTranslucentForImageViewInFragment(activity, DEFAULT_STATUS_BAR_ALPHA, needOffsetView)
}
/**
* 为 fragment 头部是 ImageView 的设置状态栏透明
*
* @param activity fragment 对应的 activity
* @param needOffsetView 需要向下偏移的 View
*/
fun setTransparentForImageViewInFragment(activity: Activity, needOffsetView: View?) {
setTranslucentForImageViewInFragment(activity, 0, needOffsetView)
}
/**
* 为 fragment 头部是 ImageView 的设置状态栏透明
*
* @param activity fragment 对应的 activity
* @param statusBarAlpha 状态栏透明度
* @param needOffsetView 需要向下偏移的 View
*/
fun setTranslucentForImageViewInFragment(
activity: Activity, @IntRange(from = 0, to = 255) statusBarAlpha: Int,
needOffsetView: View?
) {
setTranslucentForImageView(activity, statusBarAlpha, needOffsetView)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
clearPreviousSetting(activity)
}
}
/**
* 隐藏伪状态栏 View
*
* @param activity 调用的 Activity
*/
fun hideFakeStatusBarView(activity: Activity) {
val decorView = activity.window.decorView as ViewGroup
val fakeStatusBarView = decorView.findViewById<View>(FAKE_STATUS_BAR_VIEW_ID)
if (fakeStatusBarView != null) {
fakeStatusBarView.visibility = View.GONE
}
val fakeTranslucentView = decorView.findViewById<View>(
FAKE_TRANSLUCENT_VIEW_ID
)
if (fakeTranslucentView != null) {
fakeTranslucentView.visibility = View.GONE
}
}
@TargetApi(Build.VERSION_CODES.M)
fun setLightMode(activity: Activity) {
setMIUIStatusBarDarkIcon(activity, true)
setMeizuStatusBarDarkIcon(activity, true)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
}
@TargetApi(Build.VERSION_CODES.M)
fun setDarkMode(activity: Activity) {
setMIUIStatusBarDarkIcon(activity, false)
setMeizuStatusBarDarkIcon(activity, false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
}
/**
* 修改 MIUI V6 以上状态栏颜色
*/
private fun setMIUIStatusBarDarkIcon(@NonNull activity: Activity, darkIcon: Boolean) {
val clazz: Class<out Window?> = activity.window.javaClass
try {
val layoutParams = Class.forName("android.view.MiuiWindowManager\$LayoutParams")
val field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE")
val darkModeFlag = field.getInt(layoutParams)
val extraFlagField = clazz.getMethod(
"setExtraFlags",
Int::class.javaPrimitiveType,
Int::class.javaPrimitiveType
)
extraFlagField.invoke(activity.window, if (darkIcon) darkModeFlag else 0, darkModeFlag)
} catch (e: Exception) {
//e.printStackTrace();
}
}
/**
* 修改魅族状态栏字体颜色 Flyme 4.0
*/
private fun setMeizuStatusBarDarkIcon(@NonNull activity: Activity, darkIcon: Boolean) {
try {
val lp = activity.window.attributes
val darkFlag =
WindowManager.LayoutParams::class.java.getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON")
val meizuFlags = WindowManager.LayoutParams::class.java.getDeclaredField("meizuFlags")
darkFlag.isAccessible = true
meizuFlags.isAccessible = true
val bit = darkFlag.getInt(null)
var value = meizuFlags.getInt(lp)
value = if (darkIcon) {
value or bit
} else {
value and bit.inv()
}
meizuFlags.setInt(lp, value)
activity.window.attributes = lp
} catch (e: Exception) {
//e.printStackTrace();
}
}
///////////////////////////////////////////////////////////////////////////////////
@TargetApi(Build.VERSION_CODES.KITKAT)
private fun clearPreviousSetting(activity: Activity) {
val decorView = activity.window.decorView as ViewGroup
val fakeStatusBarView = decorView.findViewById<View>(FAKE_STATUS_BAR_VIEW_ID)
if (fakeStatusBarView != null) {
decorView.removeView(fakeStatusBarView)
val rootView =
(activity.findViewById<View>(android.R.id.content) as ViewGroup).getChildAt(0) as ViewGroup
rootView.setPadding(0, 0, 0, 0)
}
}
/**
* 添加半透明矩形条
*
* @param activity 需要设置的 activity
* @param statusBarAlpha 透明值
*/
private fun addTranslucentView(
activity: Activity,
@IntRange(from = 0, to = 255) statusBarAlpha: Int
) {
val contentView = activity.findViewById<View>(android.R.id.content) as ViewGroup
val fakeTranslucentView = contentView.findViewById<View>(
FAKE_TRANSLUCENT_VIEW_ID
)
if (fakeTranslucentView != null) {
if (fakeTranslucentView.visibility == View.GONE) {
fakeTranslucentView.visibility = View.VISIBLE
}
fakeTranslucentView.setBackgroundColor(Color.argb(statusBarAlpha, 0, 0, 0))
} else {
contentView.addView(createTranslucentStatusBarView(activity, statusBarAlpha))
}
}
/**
* 生成一个和状态栏大小相同的半透明矩形条
*
* @param activity 需要设置的activity
* @param color 状态栏颜色值
* @param alpha 透明值
* @return 状态栏矩形条
*/
/**
* 生成一个和状态栏大小相同的彩色矩形条
*
* @param activity 需要设置的 activity
* @param color 状态栏颜色值
* @return 状态栏矩形条
*/
private fun createStatusBarView(
activity: Activity,
@ColorInt color: Int,
alpha: Int = 0
): View {
// 绘制一个和状态栏一样高的矩形
val statusBarView = View(activity)
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity)
)
statusBarView.layoutParams = params
statusBarView.setBackgroundColor(calculateStatusColor(color, alpha))
statusBarView.id = FAKE_STATUS_BAR_VIEW_ID
return statusBarView
}
/**
* 设置根布局参数
*/
private fun setRootView(activity: Activity) {
val parent = activity.findViewById<View>(android.R.id.content) as ViewGroup
var i = 0
val count = parent.childCount
while (i < count) {
val childView = parent.getChildAt(i)
if (childView is ViewGroup) {
childView.setFitsSystemWindows(true)
childView.clipToPadding = true
}
i++
}
}
/**
* 设置透明
*/
private fun setTransparentForWindow(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.statusBarColor = Color.TRANSPARENT
activity.window
.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
activity.window
.setFlags(
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS,
WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
)
}
}
/**
* 使状态栏透明
*/
@TargetApi(Build.VERSION_CODES.KITKAT)
private fun transparentStatusBar(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION)
activity.window.statusBarColor = Color.TRANSPARENT
} else {
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
}
}
/**
* 创建半透明矩形 View
*
* @param alpha 透明值
* @return 半透明 View
*/
private fun createTranslucentStatusBarView(activity: Activity, alpha: Int): View {
// 绘制一个和状态栏一样高的矩形
val statusBarView = View(activity)
val params = LinearLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
getStatusBarHeight(activity)
)
statusBarView.layoutParams = params
statusBarView.setBackgroundColor(Color.argb(alpha, 0, 0, 0))
statusBarView.id = FAKE_TRANSLUCENT_VIEW_ID
return statusBarView
}
/**
* 获取状态栏高度
*
* @param context context
* @return 状态栏高度
*/
private fun getStatusBarHeight(context: Context): Int {
// 获得状态栏高度
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
return context.resources.getDimensionPixelSize(resourceId)
}
/**
* 计算状态栏颜色
*
* @param color color值
* @param alpha alpha值
* @return 最终的状态栏颜色
*/
private fun calculateStatusColor(@ColorInt color: Int, alpha: Int): Int {
if (alpha == 0) {
return color
}
val a = 1 - alpha / 255f
var red = color shr 16 and 0xff
var green = color shr 8 and 0xff
var blue = color and 0xff
red = (red * a + 0.5).toInt()
green = (green * a + 0.5).toInt()
blue = (blue * a + 0.5).toInt()
return 0xff shl 24 or (red shl 16) or (green shl 8) or blue
}
}
@@ -0,0 +1,25 @@
package com.bbgo.common_base.util
import java.io.BufferedReader
import java.io.InputStream
import java.io.InputStreamReader
/**
* @author chenxz
* @date 2019/11/24
* @desc StringUtil
*/
object StringUtil {
fun getString(stream: InputStream): String {
val reader = BufferedReader(InputStreamReader(stream, "utf-8"))
val sb = StringBuilder()
var s: String? = reader.readLine()
while (s != null) {
sb.append(s).append("\n")
s = reader.readLine()
}
return sb.toString()
}
}
@@ -0,0 +1,843 @@
package com.bbgo.common_base.util;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
/**
* 时间相关工具类
*/
public class TimeUtils {
private TimeUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* <p>在工具类中经常使用到工具类的格式化描述,这个主要是一个日期的操作类,所以日志格式主要使用 SimpleDateFormat的定义格式.</p>
* 格式的意义如下: 日期和时间模式 <br>
* <p>日期和时间格式由日期和时间模式字符串指定。在日期和时间模式字符串中,未加引号的字母 'A' 到 'Z' 和 'a' 到 'z'
* 被解释为模式字母,用来表示日期或时间字符串元素。文本可以使用单引号 (') 引起来,以免进行解释。"''"
* 表示单引号。所有其他字符均不解释;只是在格式化时将它们简单复制到输出字符串,或者在分析时与输入字符串进行匹配。
* </p>
* 定义了以下模式字母(所有其他字符 'A' 到 'Z' 和 'a' 到 'z' 都被保留): <br>
* <table border="1" cellspacing="1" cellpadding="1" summary="Chart shows pattern letters, date/time component,
* presentation, and examples.">
* <tr>
* <th align="left">字母</th>
* <th align="left">日期或时间元素</th>
* <th align="left">表示</th>
* <th align="left">示例</th>
* </tr>
* <tr>
* <td><code>G</code></td>
* <td>Era 标志符</td>
* <td>Text</td>
* <td><code>AD</code></td>
* </tr>
* <tr>
* <td><code>y</code> </td>
* <td>年 </td>
* <td>Year </td>
* <td><code>1996</code>; <code>96</code> </td>
* </tr>
* <tr>
* <td><code>M</code> </td>
* <td>年中的月份 </td>
* <td>Month </td>
* <td><code>July</code>; <code>Jul</code>; <code>07</code> </td>
* </tr>
* <tr>
* <td><code>w</code> </td>
* <td>年中的周数 </td>
* <td>Number </td>
* <td><code>27</code> </td>
* </tr>
* <tr>
* <td><code>W</code> </td>
* <td>月份中的周数 </td>
* <td>Number </td>
* <td><code>2</code> </td>
* </tr>
* <tr>
* <td><code>D</code> </td>
* <td>年中的天数 </td>
* <td>Number </td>
* <td><code>189</code> </td>
* </tr>
* <tr>
* <td><code>d</code> </td>
* <td>月份中的天数 </td>
* <td>Number </td>
* <td><code>10</code> </td>
* </tr>
* <tr>
* <td><code>F</code> </td>
* <td>月份中的星期 </td>
* <td>Number </td>
* <td><code>2</code> </td>
* </tr>
* <tr>
* <td><code>E</code> </td>
* <td>星期中的天数 </td>
* <td>Text </td>
* <td><code>Tuesday</code>; <code>Tue</code> </td>
* </tr>
* <tr>
* <td><code>a</code> </td>
* <td>Am/pm 标记 </td>
* <td>Text </td>
* <td><code>PM</code> </td>
* </tr>
* <tr>
* <td><code>H</code> </td>
* <td>一天中的小时数(0-23 </td>
* <td>Number </td>
* <td><code>0</code> </td>
* </tr>
* <tr>
* <td><code>k</code> </td>
* <td>一天中的小时数(1-24 </td>
* <td>Number </td>
* <td><code>24</code> </td>
* </tr>
* <tr>
* <td><code>K</code> </td>
* <td>am/pm 中的小时数(0-11 </td>
* <td>Number </td>
* <td><code>0</code> </td>
* </tr>
* <tr>
* <td><code>h</code> </td>
* <td>am/pm 中的小时数(1-12 </td>
* <td>Number </td>
* <td><code>12</code> </td>
* </tr>
* <tr>
* <td><code>m</code> </td>
* <td>小时中的分钟数 </td>
* <td>Number </td>
* <td><code>30</code> </td>
* </tr>
* <tr>
* <td><code>s</code> </td>
* <td>分钟中的秒数 </td>
* <td>Number </td>
* <td><code>55</code> </td>
* </tr>
* <tr>
* <td><code>S</code> </td>
* <td>毫秒数 </td>
* <td>Number </td>
* <td><code>978</code> </td>
* </tr>
* <tr>
* <td><code>z</code> </td>
* <td>时区 </td>
* <td>General time zone </td>
* <td><code>Pacific Standard Time</code>; <code>PST</code>; <code>GMT-08:00</code> </td>
* </tr>
* <tr>
* <td><code>Z</code> </td>
* <td>时区 </td>
* <td>RFC 822 time zone </td>
* <td><code>-0800</code> </td>
* </tr>
* </table>
* <pre>
* HH:mm 15:44
* h:mm a 3:44 下午
* HH:mm z 15:44 CST
* HH:mm Z 15:44 +0800
* HH:mm zzzz 15:44 中国标准时间
* HH:mm:ss 15:44:40
* yyyy-MM-dd 2016-08-12
* yyyy-MM-dd HH:mm 2016-08-12 15:44
* yyyy-MM-dd HH:mm:ss 2016-08-12 15:44:40
* yyyy-MM-dd HH:mm:ss zzzz 2016-08-12 15:44:40 中国标准时间
* EEEE yyyy-MM-dd HH:mm:ss zzzz 星期五 2016-08-12 15:44:40 中国标准时间
* yyyy-MM-dd HH:mm:ss.SSSZ 2016-08-12 15:44:40.461+0800
* yyyy-MM-dd'T'HH:mm:ss.SSSZ 2016-08-12T15:44:40.461+0800
* yyyy.MM.dd G 'at' HH:mm:ss z 2016.08.12 公元 at 15:44:40 CST
* K:mm a 3:44 下午
* EEE, MMM d, ''yy 星期五, 八月 12, '16
* hh 'o''clock' a, zzzz 03 o'clock 下午, 中国标准时间
* yyyyy.MMMMM.dd GGG hh:mm aaa 02016.八月.12 公元 03:44 下午
* EEE, d MMM yyyy HH:mm:ss Z 星期五, 12 八月 2016 15:44:40 +0800
* yyMMddHHmmssZ 160812154440+0800
* yyyy-MM-dd'T'HH:mm:ss.SSSZ 2016-08-12T15:44:40.461+0800
* EEEE 'DATE('yyyy-MM-dd')' 'TIME('HH:mm:ss')' zzzz 星期五 DATE(2016-08-12) TIME(15:44:40) 中国标准时间
* </pre>
* 注意:SimpleDateFormat不是线程安全的,线程安全需用{@code ThreadLocal<SimpleDateFormat>}
*/
public static final String DEFAULT_PATTERN = "yyyy-MM-dd HH:mm:ss";
/**
* 将时间戳转为时间字符串
* <p>格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param millis 毫秒时间戳
* @return 时间字符串
*/
public static String millis2String(long millis) {
return new SimpleDateFormat(DEFAULT_PATTERN, Locale.getDefault()).format(new Date(millis));
}
/**
* 将时间戳转为时间字符串
* <p>格式为pattern</p>
*
* @param millis 毫秒时间戳
* @param pattern 时间格式
* @return 时间字符串
*/
public static String millis2String(long millis, String pattern) {
return new SimpleDateFormat(pattern, Locale.getDefault()).format(new Date(millis));
}
/**
* 将时间字符串转为时间戳
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 毫秒时间戳
*/
public static long string2Millis(String time) {
return string2Millis(time, DEFAULT_PATTERN);
}
/**
* 将时间字符串转为时间戳
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 毫秒时间戳
*/
public static long string2Millis(String time, String pattern) {
try {
return new SimpleDateFormat(pattern, Locale.getDefault()).parse(time).getTime();
} catch (ParseException e) {
e.printStackTrace();
}
return -1;
}
/**
* 将时间字符串转为Date类型
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return Date类型
*/
public static Date string2Date(String time) {
return string2Date(time, DEFAULT_PATTERN);
}
/**
* 将时间字符串转为Date类型
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return Date类型
*/
public static Date string2Date(String time, String pattern) {
return new Date(string2Millis(time, pattern));
}
/**
* 将Date类型转为时间字符串
* <p>格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param date Date类型时间
* @return 时间字符串
*/
public static String date2String(Date date) {
return date2String(date, DEFAULT_PATTERN);
}
/**
* 将Date类型转为时间字符串
* <p>格式为pattern</p>
*
* @param date Date类型时间
* @param pattern 时间格式
* @return 时间字符串
*/
public static String date2String(Date date, String pattern) {
return new SimpleDateFormat(pattern, Locale.getDefault()).format(date);
}
/**
* 将Date类型转为时间戳
*
* @param date Date类型时间
* @return 毫秒时间戳
*/
public static long date2Millis(Date date) {
return date.getTime();
}
/**
* 将时间戳转为Date类型
*
* @param millis 毫秒时间戳
* @return Date类型时间
*/
public static Date millis2Date(long millis) {
return new Date(millis);
}
/**
* 获取当前毫秒时间戳
*
* @return 毫秒时间戳
*/
public static long getNowTimeMills() {
return System.currentTimeMillis();
}
/**
* 获取当前时间字符串
* <p>格式为yyyy-MM-dd HH:mm:ss</p>
*
* @return 时间字符串
*/
public static String getNowTimeString() {
return millis2String(System.currentTimeMillis(), DEFAULT_PATTERN);
}
/**
* 获取当前时间字符串
* <p>格式为pattern</p>
*
* @param pattern 时间格式
* @return 时间字符串
*/
public static String getNowTimeString(String pattern) {
return millis2String(System.currentTimeMillis(), pattern);
}
/**
* 获取当前Date
*
* @return Date类型时间
*/
public static Date getNowTimeDate() {
return new Date();
}
/**
* 是否是同一天
*/
public static boolean isSameDay(long first, long second) {
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(first);
Calendar calendar1 = Calendar.getInstance();
calendar1.setTimeInMillis(second);
return calendar.get(Calendar.YEAR) == calendar1.get(Calendar.YEAR) &&
calendar.get(Calendar.DAY_OF_YEAR) == calendar1.get(Calendar.DAY_OF_YEAR);
}
/**
* 判断是否闰年
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return {@code true}: 闰年<br>{@code false}: 平年
*/
public static boolean isLeapYear(String time) {
return isLeapYear(string2Date(time, DEFAULT_PATTERN));
}
/**
* 判断是否闰年
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return {@code true}: 闰年<br>{@code false}: 平年
*/
public static boolean isLeapYear(String time, String pattern) {
return isLeapYear(string2Date(time, pattern));
}
/**
* 判断是否闰年
*
* @param date Date类型时间
* @return {@code true}: 闰年<br>{@code false}: 平年
*/
public static boolean isLeapYear(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int year = cal.get(Calendar.YEAR);
return isLeapYear(year);
}
/**
* 判断是否闰年
*
* @param millis 毫秒时间戳
* @return {@code true}: 闰年<br>{@code false}: 平年
*/
public static boolean isLeapYear(long millis) {
return isLeapYear(millis2Date(millis));
}
/**
* 判断是否闰年
*
* @param year 年份
* @return {@code true}: 闰年<br>{@code false}: 平年
*/
public static boolean isLeapYear(int year) {
return year % 4 == 0 && year % 100 != 0 || year % 400 == 0;
}
/**
* 获取星期
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 星期
*/
public static String getWeek(String time) {
return getWeek(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取星期
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 星期
*/
public static String getWeek(String time, String pattern) {
return getWeek(string2Date(time, pattern));
}
/**
* 获取星期
*
* @param date Date类型时间
* @return 星期
*/
public static String getWeek(Date date) {
return new SimpleDateFormat("EEEE", Locale.getDefault()).format(date);
}
/**
* 获取星期
*
* @param millis 毫秒时间戳
* @return 星期
*/
public static String getWeek(long millis) {
return getWeek(new Date(millis));
}
/**
* 获取星期
* <p>注意:周日的Index才是1,周六为7</p>
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 1...5
*/
public static int getWeekIndex(String time) {
return getWeekIndex(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取星期
* <p>注意:周日的Index才是1,周六为7</p>
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 1...7
*/
public static int getWeekIndex(String time, String pattern) {
return getWeekIndex(string2Date(time, pattern));
}
/**
* 获取星期
* <p>注意:周日的Index才是1,周六为7</p>
*
* @param date Date类型时间
* @return 1...7
*/
public static int getWeekIndex(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal.get(Calendar.DAY_OF_WEEK);
}
/**
* 获取星期
* <p>注意:周日的Index才是1,周六为7</p>
*
* @param millis 毫秒时间戳
* @return 1...7
*/
public static int getWeekIndex(long millis) {
return getWeekIndex(millis2Date(millis));
}
/**
* 获取月份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 1...5
*/
public static int getWeekOfMonth(String time) {
return getWeekOfMonth(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取月份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 1...5
*/
public static int getWeekOfMonth(String time, String pattern) {
return getWeekOfMonth(string2Date(time, pattern));
}
/**
* 获取月份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
*
* @param date Date类型时间
* @return 1...5
*/
public static int getWeekOfMonth(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal.get(Calendar.WEEK_OF_MONTH);
}
/**
* 获取月份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
*
* @param millis 毫秒时间戳
* @return 1...5
*/
public static int getWeekOfMonth(long millis) {
return getWeekOfMonth(millis2Date(millis));
}
/**
* 获取年份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 1...54
*/
public static int getWeekOfYear(String time) {
return getWeekOfYear(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取年份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 1...54
*/
public static int getWeekOfYear(String time, String pattern) {
return getWeekOfYear(string2Date(time, pattern));
}
/**
* 获取年份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
*
* @param date Date类型时间
* @return 1...54
*/
public static int getWeekOfYear(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return cal.get(Calendar.WEEK_OF_YEAR);
}
/**
* 获取年份中的第几周
* <p>注意:国外周日才是新的一周的开始</p>
*
* @param millis 毫秒时间戳
* @return 1...54
*/
public static int getWeekOfYear(long millis) {
return getWeekOfYear(millis2Date(millis));
}
private static final String[] CHINESE_ZODIAC = {"", "", "", "", "", "", "", "", "", "", "", ""};
/**
* 获取生肖
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 生肖
*/
public static String getChineseZodiac(String time) {
return getChineseZodiac(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取生肖
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 生肖
*/
public static String getChineseZodiac(String time, String pattern) {
return getChineseZodiac(string2Date(time, pattern));
}
/**
* 获取生肖
*
* @param date Date类型时间
* @return 生肖
*/
public static String getChineseZodiac(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
return CHINESE_ZODIAC[cal.get(Calendar.YEAR) % 12];
}
/**
* 获取生肖
*
* @param millis 毫秒时间戳
* @return 生肖
*/
public static String getChineseZodiac(long millis) {
return getChineseZodiac(millis2Date(millis));
}
/**
* 获取生肖
*
* @param year 年
* @return 生肖
*/
public static String getChineseZodiac(int year) {
return CHINESE_ZODIAC[year % 12];
}
private static final String[] ZODIAC = {"水瓶座", "双鱼座", "白羊座", "金牛座", "双子座", "巨蟹座", "狮子座", "处女座", "天秤座", "天蝎座", "射手座", "魔羯座"};
private static final int[] ZODIAC_FLAGS = {20, 19, 21, 21, 21, 22, 23, 23, 23, 24, 23, 22};
/**
* 获取星座
* <p>time格式为yyyy-MM-dd HH:mm:ss</p>
*
* @param time 时间字符串
* @return 生肖
*/
public static String getZodiac(String time) {
return getZodiac(string2Date(time, DEFAULT_PATTERN));
}
/**
* 获取星座
* <p>time格式为pattern</p>
*
* @param time 时间字符串
* @param pattern 时间格式
* @return 生肖
*/
public static String getZodiac(String time, String pattern) {
return getZodiac(string2Date(time, pattern));
}
/**
* 获取星座
*
* @param date Date类型时间
* @return 星座
*/
public static String getZodiac(Date date) {
Calendar cal = Calendar.getInstance();
cal.setTime(date);
int month = cal.get(Calendar.MONTH) + 1;
int day = cal.get(Calendar.DAY_OF_MONTH);
return getZodiac(month, day);
}
/**
* 获取星座
*
* @param millis 毫秒时间戳
* @return 星座
*/
public static String getZodiac(long millis) {
return getZodiac(millis2Date(millis));
}
/**
* 获取星座
*
* @param month 月
* @param day 日
* @return 星座
*/
public static String getZodiac(int month, int day) {
return ZODIAC[day >= ZODIAC_FLAGS[month - 1]
? month - 1
: (month + 10) % 12];
}
/**
* 视频播放毫秒值转成时间串
*
* @param second 当前毫秒值
* @return 时间串
*/
public static String secondForTime(long second) {
if (second == 0) {
return "00:00";
}
long seconds = second % 60;
long minutes = (second / 60) % 60;
long hours = second / 3600;
if (hours > 0) {
return String.format(Locale.US, "%02d:%02d:%02d", hours, minutes, seconds).toString();
} else {
return String.format(Locale.US, "%02d:%02d", minutes, seconds).toString();
}
}
/**
* 视频播放毫秒值转成时间串
*
* @param second 当前毫秒值
* @return 时间串
*/
public static String secondForTime(String second) {
long timeSecond = 0;
try {
timeSecond = Long.valueOf(second);
} catch (NumberFormatException e) {
return "00:00";
}
return secondForTime(timeSecond);
}
/**
* 视频播放毫秒值转成时间串
*
* @param millisecond 当前毫秒值
* @return 时间串
*/
public static String stringForTime(long millisecond) {
if (millisecond <= 0) {
return "00:00";
}
int totalSeconds = (int) (millisecond / 1000);
return secondForTime(totalSeconds);
}
/**
* 获取当时时分
*/
public static String getCurHourMinute() {
Calendar calendar = Calendar.getInstance();
return millis2String(calendar.getTime().getTime(), "HH:mm");
}
/**
* long转文本时间格式
* 00:00
*/
public static String timeParse(long duration) {
String time = "";
long minute = duration / 60000;
long seconds = duration % 60000;
long second = Math.round((float) seconds / 1000);
if (minute < 10) {
time += "0";
}
time += minute + ":";
if (second < 10) {
time += "0";
}
time += second;
return time;
}
/**
* 文件名转换成时间
*/
public static String FileNameToTime(String nameStr) {
String tmpStr = "";
if (nameStr.length() > 0) {
for (int i = 0; i < nameStr.length(); i++) {
String tmp = "" + nameStr.charAt(i);
if ((tmp).matches("[0-9]")) {
tmpStr += tmp;
}
}
}
if (tmpStr.length() < 14) { // 防止相机返回异常数据导致崩溃
return "2000-01-01 01:01:01";
}
tmpStr = tmpStr.substring(0, 4) + "-" + tmpStr.substring(4, 6) + "-" + tmpStr.substring(6, 8) + " "
+ tmpStr.substring(8, 10) + ":" + tmpStr.substring(10, 12) + ":" + tmpStr.substring(12, 14);
return tmpStr;
}
public static boolean isEvening() {
return !isCurrentInTimeScope(7, 0, 19, 0);
}
/**
* 判断当前系统时间是否在指定时间的范围内
*
* @param beginHour 开始小时,例如22
* @param beginMin 开始小时的分钟数,例如30
* @param endHour 结束小时,例如 8
* @param endMin 结束小时的分钟数,例如0
* @return true表示在范围内,否则false
*/
public static boolean isCurrentInTimeScope(int beginHour, int beginMin, int endHour, int endMin) {
Calendar cal = Calendar.getInstance();// 当前日期
int hour = cal.get(Calendar.HOUR_OF_DAY);// 获取小时
int minute = cal.get(Calendar.MINUTE);// 获取分钟
int minuteOfDay = hour * 60 + minute;// 从0:00分开是到目前为止的分钟数
final int start = beginHour * 60 + beginMin;// 起始时间 00:20的分钟数
final int end = endHour * 60 + endMin;// 结束时间 8:00的分钟数
return (minuteOfDay >= start && minuteOfDay <= end);
// if (minuteOfDay >= start && minuteOfDay <= end) {
// System.out.println("当前时间-->白天");
// } else {
// System.out.println("当前时间-->夜里");
// }
// return true;
}
}
@@ -0,0 +1,173 @@
package com.bbgo.common_base.util.log
import com.bbgo.common_base.BuildConfig
import timber.log.Timber
object Logs {
private var TAG = "WanAndroid"
init {
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
} else {
Timber.plant(ReleaseTree())
}
}
@JvmStatic
fun tag(tag: String) {
TAG = tag
}
@JvmStatic
fun v(message: String?, vararg args: Any?) {
Timber.tag(TAG).v(message, args)
}
@JvmStatic
fun v(tag: String = TAG, message: String?, vararg args: Any?) {
Timber.tag(tag).v(message, args)
}
@JvmStatic
fun v(tag: String = TAG, t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(tag).v(t, message, *args)
}
@JvmStatic
fun v(t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(TAG).v(t, message, *args)
}
@JvmStatic
fun v(t: Throwable?) {
Timber.tag(TAG).v(t)
}
@JvmStatic
fun v(tag: String = TAG, t: Throwable?) {
Timber.tag(tag).v(t)
}
@JvmStatic
fun d(message: String?, vararg args: Any?) {
Timber.tag(TAG).d(message, *args)
}
@JvmStatic
fun d(tag: String = TAG, message: String?, vararg args: Any?) {
Timber.tag(tag).d(message, *args)
}
@JvmStatic
fun d(t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(TAG).v(t, message, *args)
}
@JvmStatic
fun d(tag: String = TAG, t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(tag).v(t, message, *args)
}
@JvmStatic
fun d(t: Throwable?) {
Timber.tag(TAG).d(t)
}
@JvmStatic
fun d(tag: String = TAG, t: Throwable?) {
Timber.tag(tag).d(t)
}
@JvmStatic
fun i(message: String?, vararg args: Any?) {
Timber.tag(TAG).i(message, *args)
}
@JvmStatic
fun i(tag: String = TAG, message: String?, vararg args: Any?) {
Timber.tag(tag).i(message, *args)
}
@JvmStatic
fun i(t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(TAG).i(t, message, *args)
}
@JvmStatic
fun i(tag: String = TAG, t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(tag).i(t, message, *args)
}
@JvmStatic
fun i(t: Throwable?) {
Timber.tag(TAG).i(t)
}
@JvmStatic
fun i(tag: String = TAG, t: Throwable?) {
Timber.tag(tag).i(t)
}
@JvmStatic
fun w(message: String?, vararg args: Any?) {
Timber.tag(TAG).w(message, *args)
}
@JvmStatic
fun w(tag: String = TAG, message: String?, vararg args: Any?) {
Timber.tag(tag).w(message, *args)
}
@JvmStatic
fun w(t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(TAG).w(t, message, *args)
}
@JvmStatic
fun w(tag: String = TAG, t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(tag).w(t, message, *args)
}
@JvmStatic
fun w(t: Throwable?) {
Timber.tag(TAG).w(t)
}
@JvmStatic
fun w(tag: String = TAG, t: Throwable?) {
Timber.tag(tag).w(t)
}
@JvmStatic
fun e(message: String?, vararg args: Any?) {
Timber.tag(TAG).e(message, *args)
}
@JvmStatic
fun e(tag: String = TAG, message: String?, vararg args: Any?) {
Timber.tag(tag).e(message, *args)
}
@JvmStatic
fun e(t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(TAG).e(message, *args)
}
@JvmStatic
fun e(tag: String = TAG, t: Throwable?, message: String?, vararg args: Any?) {
Timber.tag(tag).e(message, *args)
}
@JvmStatic
fun e(t: Throwable?) {
Timber.tag(TAG).e(t)
}
@JvmStatic
fun e(tag: String = TAG, t: Throwable?) {
Timber.tag(tag).e(t)
}
}
@@ -0,0 +1,194 @@
package com.bbgo.common_base.util.log
import android.text.TextUtils
import android.util.Log
import com.bbgo.common_base.BaseApplication.Companion.getContext
import com.bbgo.common_base.pool.ThreadPoolUtils
import com.bbgo.common_base.util.FileUtils
import com.bbgo.common_base.util.TimeUtils
import timber.log.Timber
import java.io.*
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.atomic.AtomicBoolean
/**
* @AuthorWangyuebin
* @Date2022-09-22
* @Description
*/
class ReleaseTree : Timber.Tree() {
companion object {
private const val LOG_DIR = "log"
// 日志文件名日期格式
private const val LOG_FILE_NAME_DATE_FORMAT = "yyyy-MM-dd"
// 保存日志的天数
private const val SAVE_LOG_DAYS = 7
// 日志文件名
private const val LOG_FILE_NAME_FORMAT = "%s.log"
// 日志文件名称
private var mLogFile: File? = null
// 当前日期
private var mCurrentDate: String? = null
// 日志队列
private val mLogQueue = LinkedBlockingQueue<String>()
/**
* 初始化日志文件名
*/
private fun init() {
mCurrentDate = TimeUtils.date2String(Date(), LOG_FILE_NAME_DATE_FORMAT)
val logFileName = String.format(LOG_FILE_NAME_FORMAT, mCurrentDate)
val logDir = FileUtils.getExternalFilePath() + File.separator + LOG_DIR
val fileDir = File(logDir)
if (!fileDir.exists()) {
fileDir.mkdirs()
}
if (mLogFile == null) {
mLogFile = File(logDir, logFileName)
}
}
}
// 日志写入器
private val mLogWriter: LogWriter = LogWriter()
init {
init()
}
override fun isLoggable(tag: String?, priority: Int): Boolean {
return priority >= Log.INFO
}
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
putLogQueue(priority, tag, message, t)
}
/**
* 保存日志
*/
private fun putLogQueue(priority: Int, tag: String?, message: String, t: Throwable?) {
val logContent = StringBuilder()
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
logContent.append(sdf.format(Date())).append(" /").append(getContext().packageName).append(" ")
var level = "I"
when (priority) {
Log.DEBUG -> level = "D"
Log.INFO -> level = "I"
Log.VERBOSE -> level = "V"
Log.WARN -> level = "W"
Log.ERROR -> level = "E"
}
logContent.append(level).append("/").append(tag).append(": ").append(message).append("\n")
if (t != null) {
val writer: Writer = StringWriter()
val pw = PrintWriter(writer)
t.printStackTrace(pw)
var cause = t.cause
// 循环着把所有的异常信息写入writer中
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close() // 记得关闭
val result = writer.toString()
logContent.delete(0, logContent.length)
logContent.append("FATAL Exception").append("\n").append(result).append("\n")
}
mLogQueue.put(logContent.toString())
// 确保运行着
mLogWriter.ensureRunning()
}
/**
* 日志写入器
*/
class LogWriter : Runnable {
// 消费者线程是否在运行
private val isLogWriterRunning = AtomicBoolean(false)
override fun run() {
while (isLogWriterRunning.get()) {
try {
// 检查日期发生变化
val currentDate: String = TimeUtils.getNowTimeString()
if (TextUtils.isEmpty(mCurrentDate) || mCurrentDate != currentDate) {
checkDeletePastDueLog()
init()
}
if (mLogFile != null) {
var bw: BufferedWriter? = null
try {
if (mLogFile?.exists() == false) {
mLogFile?.createNewFile()
}
bw = BufferedWriter(FileWriter(mLogFile, true))
bw.write(mLogQueue.take())
} catch (e: IOException) {
e.printStackTrace()
} finally {
bw?.close()
}
}
} catch (e: Throwable) {
e.printStackTrace()
}
}
isLogWriterRunning.set(false)
}
/**
* 确保运行着
*/
fun ensureRunning() {
// 写入线程在运行
if (isLogWriterRunning.get()) {
return
}
// 启动线程
isLogWriterRunning.set(true)
Thread(this).start()
}
/**
* 检测删除过期日志,只保存近7天的日志
*/
private fun checkDeletePastDueLog() {
val logDirPath: String = FileUtils.getExternalFilePath() + File.separator + LOG_DIR
val logDir = File(logDirPath)
if (!logDir.exists() || logDir.listFiles().isNullOrEmpty()) {
return
}
logDir.listFiles()?.let {
for (logFile in it) {
if (logFile.exists() && isPastDueLogFile(logFile)) {
logFile.delete()
}
}
}
}
/**
* 是否是过期的日志文件
*/
private fun isPastDueLogFile(logFile: File): Boolean {
val fileName: String = FileUtils.getFileNameNotSuffix(logFile)
// 日志文件创建时间
val logFileCreateTime = TimeUtils.string2Millis(
fileName,
LOG_FILE_NAME_DATE_FORMAT
)
// 日志文件名称格式解析错误,删除
if (logFileCreateTime <= 0) {
return true
}
// 检测时间是否已经过期
val calendar = Calendar.getInstance()
calendar.add(Calendar.DATE, -SAVE_LOG_DAYS)
return calendar.timeInMillis > logFileCreateTime
}
}
}
@@ -0,0 +1,49 @@
package com.bbgo.common_base.widget;
import android.animation.ObjectAnimator;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.core.view.ViewCompat;
/**
* Created by Meiji on 2016/12/12.
*/
public class BottomNavigationBehavior extends CoordinatorLayout.Behavior<View> {
private ObjectAnimator outAnimator, inAnimator;
public BottomNavigationBehavior(Context context, AttributeSet attrs) {
super(context, attrs);
}
// 垂直滑动
@Override
public boolean onStartNestedScroll(CoordinatorLayout coordinatorLayout, View child, View directTargetChild, View target, int nestedScrollAxes, int type) {
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL;
}
@Override
public void onNestedPreScroll(CoordinatorLayout coordinatorLayout, View child, View target, int dx, int dy, int[] consumed, int type) {
if (dy > 0) {// 上滑隐藏
if (outAnimator == null) {
outAnimator = ObjectAnimator.ofFloat(child, "translationY", 0, child.getHeight());
outAnimator.setDuration(200);
}
if (!outAnimator.isRunning() && child.getTranslationY() <= 0) {
outAnimator.start();
}
} else if (dy < 0) {// 下滑显示
if (inAnimator == null) {
inAnimator = ObjectAnimator.ofFloat(child, "translationY", child.getHeight(), 0);
inAnimator.setDuration(200);
}
if (!inAnimator.isRunning() && child.getTranslationY() >= child.getHeight()) {
inAnimator.start();
}
}
}
}
@@ -0,0 +1,129 @@
package com.bbgo.common_base.widget
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatImageView
import com.bbgo.common_base.R
/**
* Created by chenxz on 2018/4/22.
*/
class CircleImageView(context: Context, attrs: AttributeSet) : AppCompatImageView(context, attrs) {
private val mType: Int
private val mBorderColor: Int
private val mBorderWidth: Int
private val mRectRoundRadius: Int
private val mPaintBitmap = Paint(Paint.ANTI_ALIAS_FLAG)
private val mPaintBorder = Paint(Paint.ANTI_ALIAS_FLAG)
private val mRectBorder = RectF()
private val mRectBitmap = RectF()
private var mRawBitmap: Bitmap? = null
private var mShader: BitmapShader? = null
private val mMatrix = Matrix()
init {
//取xml文件中设定的参数
val ta = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView)
mType = ta.getInt(R.styleable.CircleImageView_type, DEFAULT_TYPE)
mBorderColor = ta.getColor(R.styleable.CircleImageView_borderColor, DEFAULT_BORDER_COLOR)
mBorderWidth = ta.getDimensionPixelSize(R.styleable.CircleImageView_borderWidth, dip2px(DEFAULT_BORDER_WIDTH))
mRectRoundRadius = ta.getDimensionPixelSize(R.styleable.CircleImageView_rectRoundRadius, dip2px(DEFAULT_RECT_ROUND_RADIUS))
ta.recycle()
}
override fun onDraw(canvas: Canvas) {
val rawBitmap = getBitmap(drawable)
if (rawBitmap != null && mType != TYPE_NONE) {
val viewWidth = width
val viewHeight = height
val viewMinSize = Math.min(viewWidth, viewHeight)
val dstWidth = (if (mType == TYPE_CIRCLE) viewMinSize else viewWidth).toFloat()
val dstHeight = (if (mType == TYPE_CIRCLE) viewMinSize else viewHeight).toFloat()
val halfBorderWidth = mBorderWidth / 2.0f
val doubleBorderWidth = (mBorderWidth * 2).toFloat()
if (mShader == null || rawBitmap != mRawBitmap) {
mRawBitmap = rawBitmap
mShader = BitmapShader(mRawBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
}
if (mShader != null) {
mMatrix.setScale((dstWidth - doubleBorderWidth) / rawBitmap.width, (dstHeight - doubleBorderWidth) / rawBitmap.height)
mShader!!.setLocalMatrix(mMatrix)
}
mPaintBitmap.shader = mShader
mPaintBorder.style = Paint.Style.STROKE
mPaintBorder.strokeWidth = mBorderWidth.toFloat()
mPaintBorder.color = if (mBorderWidth > 0) mBorderColor else Color.TRANSPARENT
if (mType == TYPE_CIRCLE) {
val radius = viewMinSize / 2.0f
canvas.drawCircle(radius, radius, radius - halfBorderWidth, mPaintBorder)
canvas.translate(mBorderWidth.toFloat(), mBorderWidth.toFloat())
canvas.drawCircle(radius - mBorderWidth, radius - mBorderWidth, radius - mBorderWidth, mPaintBitmap)
} else if (mType == TYPE_ROUNDED_RECT) {
mRectBorder.set(halfBorderWidth, halfBorderWidth, dstWidth - halfBorderWidth, dstHeight - halfBorderWidth)
mRectBitmap.set(0.0f, 0.0f, dstWidth - doubleBorderWidth, dstHeight - doubleBorderWidth)
val borderRadius = if (mRectRoundRadius - halfBorderWidth > 0.0f) mRectRoundRadius - halfBorderWidth else 0.0f
val bitmapRadius = if (mRectRoundRadius - mBorderWidth > 0.0f) (mRectRoundRadius - mBorderWidth).toFloat() else 0.0f
canvas.drawRoundRect(mRectBorder, borderRadius, borderRadius, mPaintBorder)
canvas.translate(mBorderWidth.toFloat(), mBorderWidth.toFloat())
canvas.drawRoundRect(mRectBitmap, bitmapRadius, bitmapRadius, mPaintBitmap)
}
} else {
super.onDraw(canvas)
}
}
private fun dip2px(dipVal: Int): Int {
val scale = resources.displayMetrics.density
return (dipVal * scale + 0.5f).toInt()
}
private fun getBitmap(drawable: Drawable?): Bitmap? {
if (drawable == null) return null
return when (drawable) {
is BitmapDrawable -> drawable.bitmap
is ColorDrawable -> {
val rect = drawable.getBounds()
val width = rect.right - rect.left
val height = rect.bottom - rect.top
val color = drawable.color
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.drawARGB(Color.alpha(color), Color.red(color), Color.green(color), Color.blue(color))
bitmap
}
else -> null
}
}
companion object {
/**
* android.widget.ImageView
*/
val TYPE_NONE = 0
/**
* 圆形
*/
val TYPE_CIRCLE = 1
/**
* 圆角矩形
*/
val TYPE_ROUNDED_RECT = 2
private val DEFAULT_TYPE = TYPE_NONE
private val DEFAULT_BORDER_COLOR = Color.TRANSPARENT
private val DEFAULT_BORDER_WIDTH = 0
private val DEFAULT_RECT_ROUND_RADIUS = 0
}
}
@@ -0,0 +1,40 @@
package com.bbgo.common_base.widget
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import com.google.android.material.floatingactionbutton.FloatingActionButton
/**
* Created by chenxz on 2018/5/13.
*
* FAB 行为控制器
*/
class ScaleDownShowBehavior : FloatingActionButton.Behavior {
constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
override fun onStartNestedScroll(coordinatorLayout: CoordinatorLayout,
child: FloatingActionButton,
directTargetChild: View,
target: View,
axes: Int,
type: Int): Boolean {
return axes == ViewCompat.SCROLL_AXIS_VERTICAL ||
super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, axes, type)
}
@SuppressLint("RestrictedApi")
override fun onNestedScroll(coordinatorLayout: CoordinatorLayout, child: FloatingActionButton, target: View, dxConsumed: Int, dyConsumed: Int, dxUnconsumed: Int, dyUnconsumed: Int, type: Int) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type)
if (dyConsumed > 0 && child.visibility == View.VISIBLE) {
child.visibility = View.INVISIBLE
} else if (dyConsumed < 0 && child.visibility != View.VISIBLE) {
child.show()
}
}
}
@@ -0,0 +1,86 @@
package com.bbgo.common_base.widget
import android.content.Context
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.core.view.ViewCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.OrientationHelper
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.StaggeredGridLayoutManager
/**
* Created by chenxz on 2018/5/9.
*
* 简单的 RecyclerView 分割线
*/
class SpaceItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
private var mDivider: Drawable? = null
private val mSectionOffsetV: Int = 0
private val mSectionOffsetH: Int = 0
private var mDrawOver = true
private var attrs: IntArray = intArrayOf(android.R.attr.listDivider)
init {
var a = context.obtainStyledAttributes(attrs)
mDivider = a.getDrawable(0)
a.recycle()
}
override fun onDrawOver(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(c, parent, state)
if (mDivider != null && mDrawOver) {
draw(c, parent)
}
}
override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDraw(c, parent, state)
if (mDivider != null && mDrawOver) {
draw(c, parent)
}
}
override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) {
super.getItemOffsets(outRect, view, parent, state)
if (getOrientation(parent.layoutManager!!) == RecyclerView.VERTICAL) {
outRect.set(mSectionOffsetH, 0, mSectionOffsetH, mSectionOffsetV)
} else {
outRect.set(0, 0, mSectionOffsetV, 0)
}
}
private fun draw(c: Canvas, parent: RecyclerView) {
val left = parent.paddingLeft
val right = parent.width - parent.paddingRight
val childCount = parent.childCount
for (i in 0 until childCount) {
val child = parent.getChildAt(i)
val params = child.layoutParams as RecyclerView.LayoutParams
val top = child.bottom + params.bottomMargin + Math.round(ViewCompat.getTranslationY(child))
val bottom = top + if (mDivider!!.intrinsicHeight <= 0) 1 else mDivider!!.intrinsicHeight
mDivider?.let {
it.setBounds(left, top, right, bottom)
it.draw(c)
}
}
}
private fun getOrientation(layoutManager: RecyclerView.LayoutManager): Int {
if (layoutManager is LinearLayoutManager) {
return layoutManager.orientation
} else if (layoutManager is StaggeredGridLayoutManager) {
return layoutManager.orientation
}
return OrientationHelper.HORIZONTAL
}
}
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator">
<scale android:fromXScale="2.0" android:toXScale="1.0"
android:fromYScale="2.0" android:toYScale="1.0"
android:pivotX="50%p" android:pivotY="50%p"
android:duration="@android:integer/config_mediumAnimTime" />
</set>
@@ -0,0 +1,10 @@
<set xmlns:android="http://schemas.android.com/apk/res/android"
android:interpolator="@android:anim/decelerate_interpolator"
android:zAdjustment="top">
<scale android:fromXScale="1.0" android:toXScale=".5"
android:fromYScale="1.0" android:toYScale=".5"
android:pivotX="50%p" android:pivotY="50%p"
android:duration="@android:integer/config_mediumAnimTime" />
<alpha android:fromAlpha="1.0" android:toAlpha="0"
android:duration="@android:integer/config_mediumAnimTime"/>
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1000"
android:fromAlpha="0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toAlpha="1.0"/>
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<alpha
android:duration="1500"
android:fromAlpha="1.0"
android:interpolator="@android:anim/decelerate_interpolator"
android:toAlpha="0"/>
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="-50%p" android:toYDelta="0" android:duration="1000"/>
<alpha android:fromAlpha="0.0" android:toAlpha="1.0" android:duration="1000" />
</set>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<set xmlns:android="http://schemas.android.com/apk/res/android">
<translate android:fromYDelta="0" android:toYDelta="-50%p" android:duration="1000"/>
<alpha android:fromAlpha="1.0" android:toAlpha="0.0" android:duration="1000" />
</set>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/list_divider" />
</shape>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/Grey300"/>
</shape>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape>
<solid android:color="#A6000000" />
<padding android:left="8dp" android:top="4dp" android:right="8dp" android:bottom="4dp" />
<corners android:radius="2dp" />
</shape>
</item>
<item>
<shape>
<solid android:color="#99000000" />
<padding android:left="8dp" android:top="4dp" android:right="8dp" android:bottom="4dp" />
<corners android:radius="2dp" />
</shape>
</item>
</selector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
android:tint="#FFFFFF">
<group android:scaleX="2.39598"
android:scaleY="2.39598"
android:translateX="25.24824"
android:translateY="21.25494">
<path
android:pathData="M17.6,11.48 L19.44,8.3a0.63,0.63 0,0 0,-1.09 -0.63l-1.88,3.24a11.43,11.43 0,0 0,-8.94 0L5.65,7.67a0.63,0.63 0,0 0,-1.09 0.63L6.4,11.48A10.81,10.81 0,0 0,1 20L23,20A10.81,10.81 0,0 0,17.6 11.48ZM7,17.25A1.25,1.25 0,1 1,8.25 16,1.25 1.25,0 0,1 7,17.25ZM17,17.25A1.25,1.25 0,1 1,18.25 16,1.25 1.25,0 0,1 17,17.25Z"
android:fillColor="#FF000000"/>
</group>
</vector>
@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:indeterminate="true"
android:indeterminateTint="@color/colorPrimary"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</merge>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<attr name="textColorPrimary" format="color" />
<attr name="viewBackground" format="color" />
</resources>
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">@color/Cyan</color>
<color name="colorPrimaryDark">@color/transparent</color>
<color name="colorAccent">@color/Cyan</color>
<color name="textColorPrimary">#616161</color>
<color name="viewBackground">@color/White</color>
<color name="windowBackground">@color/Grey100</color>
<color name="line_divider">@color/Grey300</color>
<color name="list_divider">@color/Grey400</color>
<color name="mask_color">#f5f5f5</color>
<color name="transparent">#00000000</color>
<!-- item -->
<color name="common_color">#19191B</color>
<color name="item_title">#19191B</color>
<color name="item_author">@color/Grey700</color>
<color name="item_desc">@color/Grey600</color>
<color name="item_time">@color/Grey600</color>
<color name="item_date">@color/Grey600</color>
<color name="item_chapter">@color/Grey600</color>
<color name="item_tag_tv">#ffFFFFFF</color>
<color name="item_nav_color_tv">#19191B</color>
<color name="color_title_bg">#CCFFFFFF</color>
<color name="color_about_tv">@color/Grey600</color>
<color name="transparent_75">#ba000000</color>
<color name="vertical_tab_layout_bg">@color/Grey100</color>
<color name="vertical_tab_layout_indicator_color">@color/White</color>
<color name="item_flow_layout_bg">@color/Grey200</color>
<color name="arrow_color">@color/Grey700</color>
<color name="sticky_header_bg">@color/Grey100</color>
<color name="light_red">#e26d2e</color>
<color name="deep_red">#dc3b1f</color>
<color name="while_most_color">#e4e4e4</color>
<color name="title_black">#222222</color>
<color name="grey_search">#FF999999</color>
<color name="White">#FFFFFF</color>
<color name="Black">#000000</color>
<color name="Red">#f44336</color>
<color name="Pink">#e91e63</color>
<color name="Purple">#9c27b0</color>
<color name="Deep_Purple">#673ab7</color>
<color name="Indigo">#3f51b5</color>
<color name="Blue">#2196f3</color>
<color name="Light_Blue">#03a9f4</color>
<color name="Cyan">#00bcd4</color>
<color name="Teal">#009688</color>
<color name="Green">#4caf50</color>
<color name="Light_Green">#8bc34a</color>
<color name="Lime">#cddc39</color>
<color name="Yellow">#ffeb3b</color>
<color name="Amber">#ffc107</color>
<color name="Orange">#ff9800</color>
<color name="Deep_Orange">#ff5722</color>
<color name="Brown">#795548</color>
<color name="Grey">#9e9e9e</color>
<color name="Blue_Grey">#607d8b</color>
<color name="Grey50">#FAFAFA</color>
<color name="Grey100">#F5F5F5</color>
<color name="Grey200">#EEEEEE</color>
<color name="Grey300">#E0E0E0</color>
<color name="Grey400">#bdbdbd</color>
<color name="Grey500">#9e9e9e</color>
<color name="Grey600">#757575</color>
<color name="Grey700">#616161</color>
<color name="Grey800">#424242</color>
<color name="Grey900">#212121</color>
</resources>
@@ -0,0 +1,85 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
<dimen name="avatar_width">80dp</dimen>
<dimen name="avatar_height">80dp</dimen>
<dimen name="appbar_layout_height">260dp</dimen>
<!-- item -->
<dimen name="item_tv_title">16sp</dimen>
<dimen name="item_tv_content">14sp</dimen>
<dimen name="item_tv_date">12sp</dimen>
<dimen name="item_tv_tag">12sp</dimen>
<dimen name="item_tv_author">12sp</dimen>
<dimen name="item_tv_via">12sp</dimen>
<dimen name="item_content_padding">10dp</dimen>
<dimen name="item_img_width">120dp</dimen>
<dimen name="item_img_height">90dp</dimen>
<dimen name="textSize_small_10">10sp</dimen>
<dimen name="textSize_small">12sp</dimen>
<dimen name="textSize_content">14sp</dimen>
<dimen name="textSize_titleSmall">16sp</dimen>
<dimen name="textSize_title">18sp</dimen>
<!-- dp -->
<dimen name="dp_05">0.5dp</dimen>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_8">8dp</dimen>
<dimen name="dp_10">10dp</dimen>
<dimen name="dp_12">12dp</dimen>
<dimen name="dp_16">16dp</dimen>
<dimen name="dp_18">18dp</dimen>
<dimen name="dp_20">20dp</dimen>
<dimen name="dp_24">24dp</dimen>
<dimen name="dp_26">26dp</dimen>
<dimen name="dp_28">28dp</dimen>
<dimen name="dp_30">30dp</dimen>
<dimen name="dp_36">36dp</dimen>
<dimen name="dp_46">46dp</dimen>
<dimen name="dp_50">50dp</dimen>
<dimen name="dp_54">54dp</dimen>
<dimen name="dp_60">60dp</dimen>
<dimen name="dp_80">80dp</dimen>
<dimen name="dp_90">90dp</dimen>
<dimen name="dp_100">100dp</dimen>
<dimen name="dp_110">110dp</dimen>
<dimen name="dp_168">168dp</dimen>
<dimen name="dp_180">180dp</dimen>
<dimen name="dp_200">200dp</dimen>
<dimen name="dp_230">230dp</dimen>
<dimen name="dp_240">240dp</dimen>
<dimen name="dp_260">260dp</dimen>
<dimen name="dp_280">280dp</dimen>
<dimen name="dp_300">300dp</dimen>
<!-- sp -->
<dimen name="sp_8">8sp</dimen>
<dimen name="sp_9">9sp</dimen>
<dimen name="sp_10">10sp</dimen>
<dimen name="sp_12">12sp</dimen>
<dimen name="sp_13">13sp</dimen>
<dimen name="sp_14">14sp</dimen>
<dimen name="sp_15">15sp</dimen>
<dimen name="sp_16">16sp</dimen>
<dimen name="sp_17">17sp</dimen>
<dimen name="sp_18">18sp</dimen>
<dimen name="sp_20">20sp</dimen>
<dimen name="sp_22">22sp</dimen>
<dimen name="sp_24">24sp</dimen>
<dimen name="sp_40">40sp</dimen>
<dimen name="sp_50">50sp</dimen>
<!--BottomNavigationView 的选中没有选中的字体大小-->
<dimen name="design_bottom_navigation_active_text_size">@dimen/sp_13</dimen>
<dimen name="design_bottom_navigation_text_size">@dimen/sp_12</dimen>
</resources>
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item type="id" name="expandable_text"/>
<item type="id" name="expand_collapse"/>
<item name="statusbarutil_fake_status_bar_view" type="id" />
<item name="statusbarutil_translucent_view" type="id" />
</resources>
@@ -0,0 +1,3 @@
<resources>
<string name="app_name">common-base</string>
</resources>
@@ -0,0 +1,146 @@
<resources>
<!-- Base application theme. -->
<style name="BaseAppTheme" parent="@style/Theme.AppCompat.DayNight">
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="textColorPrimary">@color/textColorPrimary</item>
<item name="viewBackground">@color/viewBackground</item>
<!--关闭启动窗口-->
<item name="android:windowDisablePreview">true</item>
<item name="android:listDivider">@drawable/bg_divider</item>
</style>
<style name="AppTheme" parent="BaseAppTheme.NoActionBar"></style>
<style name="AppTheme.FullScreen" parent="@style/Theme.AppCompat.DayNight">
<item name="windowNoTitle">true</item>
<item name="windowActionBar">false</item>
<item name="android:windowFullscreen">true</item>
<item name="android:windowContentOverlay">@null</item>
<item name="android:windowBackground">@null</item>
<item name="android:windowDisablePreview">true</item>
</style>
<style name="SplashTheme" parent="BaseAppTheme.NoActionBar">
<item name="android:windowFullscreen">true</item>
<item name="android:windowIsTranslucent">true</item>
</style>
<style name="BaseAppTheme.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
<style name="BaseAppTheme.NoActionBar.Slidable" parent="BaseAppTheme.NoActionBar">
<item name="android:windowIsTranslucent">true</item>
<item name="android:windowBackground">@android:color/transparent</item>
</style>
<style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" />
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="MyTabLayoutStyle" parent="Base.Widget.Design.TabLayout">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">wrap_content</item>
<item name="android:background">@color/colorPrimary</item>
<item name="tabIndicatorColor">@color/viewBackground</item>
<item name="tabIndicatorHeight">@dimen/dp_1</item>
<item name="tabSelectedTextColor">@color/viewBackground</item>
<item name="tabTextAppearance">@style/MyTabTextAppearance</item>
</style>
<style name="MyTabTextAppearance">
<item name="android:textSize">@dimen/sp_14</item>
<item name="textAllCaps">false</item>
<item name="android:textColor">?android:textColorSecondary</item>
</style>
<style name="WindowAnimationFadeInOut">
<item name="android:windowEnterAnimation">@anim/fade_in</item>
<item name="android:windowExitAnimation">@anim/fade_out</item>
</style>
<style name="ToolbarStyle" parent="AppTheme">
<item name="android:textColorSecondary">@android:color/white</item>
<item name="searchHintIcon">@null</item>
</style>
<!-- 设置Toolbar标题字体的大小 -->
<style name="Toolbar.TitleText" parent="TextAppearance.Widget.AppCompat.Toolbar.Title">
<item name="android:textSize">@dimen/sp_18</item>
</style>
<!-- 浮动窗口动画 -->
<style name="anim_float_view">
<item name="android:windowEnterAnimation">@anim/push_up_in</item>
<item name="android:windowExitAnimation">@anim/push_up_out</item>
</style>
<style name="MenuLabelsStyle">
<item name="android:background">@drawable/fab_label_background</item>
<item name="android:textColor">#FFFFFF</item>
<item name="android:textSize">@dimen/sp_14</item>
<item name="android:maxLines">2</item>
<item name="android:ellipsize">end</item>
</style>
<style name="RecyclerViewStyle">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_height">match_parent</item>
<item name="android:scrollbarSize">@dimen/dp_2</item>
<item name="android:scrollbars">vertical</item>
</style>
<style name="TabLayout" parent="Base.Widget.Design.TabLayout">
<item name="tabIndicatorColor">@color/colorPrimary</item>
<item name="tabIndicatorHeight">2dp</item>
<item name="tabPaddingStart">12dp</item>
<item name="tabPaddingEnd">12dp</item>
<item name="tabBackground">@color/colorPrimary</item>
<item name="tabTextAppearance">@style/TextAppearance.Design.Tab</item>
<item name="tabSelectedTextColor">@color/colorPrimary</item>
<item name="tabTextColor">@color/colorPrimary</item>
</style>
<declare-styleable name="CircleProgressView">
<attr name="cpv_progressNormalColor" format="color"/>
<attr name="cpv_progressReachColor" format="color"/>
<attr name="cpv_progressTextColor" format="color"/>
<attr name="cpv_progressTextSize" format="dimension"/>
<attr name="cpv_progressTextOffset" format="dimension"/>
<attr name="cpv_progressNormalSize" format="dimension"/>
<attr name="cpv_progressReachSize" format="dimension"/>
<attr name="cpv_radius" format="dimension"/>
<attr name="cpv_progressTextVisible" format="boolean"/>
<attr name="cpv_progressStartArc" format="integer"/>
<attr name="cpv_progressTextSkewX" format="dimension"/>
<attr name="cpv_progressTextPrefix" format="string"/>
<attr name="cpv_progressTextSuffix" format="string"/>
<attr name="cpv_innerBackgroundColor" format="color"/>
<attr name="cpv_progressStyle" format="enum">
<enum name="Normal" value="0"/>
<enum name="FillInner" value="1"/>
<enum name="FillInnerArc" value="2"/>
</attr>
<attr name="cpv_innerProgressColor" format="color"/>
<attr name="cpv_innerPadding" format="dimension"/>
<attr name="cpv_outerColor" format="color"/>
<attr name="cpv_outerSize" format="dimension"/>
<attr name="cpv_reachCapRound" format="boolean"/>
</declare-styleable>
<declare-styleable name="CircleImageView">
<attr name="type" format="enum">
<enum name="none" value="0" />
<enum name="circle" value="1" />
<enum name="rect_rounded" value="2" />
</attr>
<attr name="borderColor" format="color" />
<attr name="borderWidth" format="dimension" />
<attr name="rectRoundRadius" format="dimension" />
</declare-styleable>
</resources>
@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="transition_movie_img">transition_movie_img</string>
<string name="transition_news_img">transition_news_img</string>
<string name="transition_book_img">transition_book_img</string>
</resources>