添加项目

This commit is contained in:
zhanglei
2025-06-30 14:12:14 +08:00
commit 8a60755b60
2374 changed files with 198527 additions and 0 deletions
+1
View File
@@ -0,0 +1 @@
/build
+70
View File
@@ -0,0 +1,70 @@
plugins {
id 'com.android.library'
id 'kotlin-android'
id 'kotlin-kapt'
id 'org.jetbrains.kotlin.android'
}
apply plugin: 'kotlin-android'
android {
compileSdk rootProject.ext.compileSdkVersion
defaultConfig {
minSdk rootProject.ext.minSdkVersion
targetSdk rootProject.ext.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
debug {
minifyEnabled false // 开启混淆
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
dataBinding true
}
namespace 'com.btpj.lib_base'
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.google.android.gms:play-services-ads-identifier:18.0.1'
api project(path: ':rsalibrary')
configurations {
all*.exclude group: 'com.google.guava', module: 'listenablefuture'
}
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.3.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5'
//androidx
implementation rootProject.ext.androidxLibs
// ViewModel and LiveData
implementation rootProject.ext.jetpackLibs
/* annotationProcessor */
kapt rootProject.ext.annotationProcessorLibs
/*网络请求相关*/
implementation rootProject.ext.networkLibs
/*Glide*/
implementation rootProject.ext.glideLibs
/*AgentWeb*/
implementation rootProject.ext.agentWebLibs
// 其他包
implementation rootProject.ext.commonLibs
}
+148
View File
@@ -0,0 +1,148 @@
# 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 ##################
# 代码混淆压缩比,在0~7之间,默认为5,一般不做修改
-optimizationpasses 5
# 混合时不使用大小写混合,混合后的类名为小写
-dontusemixedcaseclassnames
# 指定不去忽略非公共库的类
-dontskipnonpubliclibraryclasses
# 这句话能够使我们的项目混淆后产生映射文件
# 包含有类名->混淆后类名的映射关系
-verbose
# 指定不去忽略非公共库的类成员
-dontskipnonpubliclibraryclassmembers
# 不做预校验,preverify是proguard的四个步骤之一,Android不需要preverify,去掉这一步能够加快混淆速度
-dontpreverify
# 忽略警告
-ignorewarnings
# 保留Annotation不混淆
-keepattributes *Annotation*,InnerClasses
# 避免混淆泛型
-keepattributes Signature
# 抛出异常时保留代码行号
-keepattributes SourceFile,LineNumberTable
# 指定混淆是采用的算法,后面的参数是一个过滤器
# 这个过滤器是谷歌推荐的算法,一般不做更改
-optimizations !code/simplification/cast,!field/*,!class/merging/*
############### 对于一些基本指令的添加end #########################
############### Android开发中一些需要保留的公共部分start ##################
# 保留我们使用的四大组件,自定义的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
# 保留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.**
# 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.**
# 保留R下面的资源
-keep class **.R$* {*;}
# 保留本地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);
}
# 保留Parcelable序列化类不被混淆
-keep class * implements android.os.Parcelable { public static final android.os.Parcelable$Creator *;}
# 保留Serializable序列化的类不被混淆
-keepnames class * implements java.io.Serializable
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
!static !transient <fields>;
!private <fields>;
!private <methods>;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
# 对于带有回调函数的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
# 所有实体类不能混淆 model文件夹下的所有实体类不能混淆
-keep class com.btpj.lib_base.data.bean.** {*;}
-keep class com.btpj.wanandroid.data.** {*;}
############### Android开发中一些需要保留的公共部分end ##################
############### 第三方库中的混淆规则start ##############################
# BaseRecyclerViewAdapterHelper混淆
-keep public class * extends com.chad.library.adapter.base.viewholder.BaseViewHolder
-keep public class * extends com.chad.library.adapter.base.viewholder.BaseDataBindingHolder
-keepclassmembers class com.chad.library.adapter.base.viewholder.BaseDataBindingHolder {
public void *(android.view.View);
}
-keepclassmembers class * extends com.chad.library.adapter.base.viewholder.BaseDataBindingHolder {
public void *(android.view.View);
}
# Glide混淆
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep class * extends com.bumptech.glide.module.AppGlideModule {
<init>(...);
}
-keep public enum com.bumptech.glide.load.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
-keep class com.bumptech.glide.load.data.ParcelFileDescriptorRewinder$InternalRewinder {
*** rewind();
}
# bugly混淆
-dontwarn com.tencent.bugly.**
-keep public class com.tencent.bugly.**{*;}
@@ -0,0 +1,22 @@
package com.btpj.lib_base
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.btpj.lib_base.test", appContext.packageName)
}
}
+8
View File
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
</manifest>
@@ -0,0 +1,50 @@
package com.btpj.lib_base
import android.app.Application
import android.content.Context
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelStore
import androidx.lifecycle.ViewModelStoreOwner
import com.btpj.lib_base.data.local.DataStoreManager
import com.btpj.lib_base.data.local.IpManager
import kotlin.properties.Delegates
/**
* Application基类
*
* @author nanfeifei 2022/3/21
*/
open class BaseApp : Application(), ViewModelStoreOwner {
private lateinit var mAppViewModelStore: ViewModelStore
private var mFactory: ViewModelProvider.Factory? = null
companion object {
var appContext: Context by Delegates.notNull()
}
override fun onCreate() {
super.onCreate()
IpManager.initialize(this)
DataStoreManager.initialize(this)
appContext = applicationContext
mAppViewModelStore = ViewModelStore()
}
/** 获取一个全局的ViewModel */
fun getAppViewModelProvider(): ViewModelProvider {
return ViewModelProvider(this, getAppFactory())
}
private fun getAppFactory(): ViewModelProvider.Factory {
if (mFactory == null) {
mFactory = ViewModelProvider.AndroidViewModelFactory.getInstance(this)
}
return mFactory as ViewModelProvider.Factory
}
override fun getViewModelStore(): ViewModelStore {
return mAppViewModelStore
}
}
@@ -0,0 +1,296 @@
package com.btpj.lib_base.base
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Bundle
import android.view.View
import android.widget.TextView
import androidx.appcompat.app.ActionBar
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.btpj.lib_base.BR
import com.btpj.lib_base.R
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_HIDE
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_SHOW
import com.btpj.lib_base.event.LoginEvent
import com.btpj.lib_base.ext.hideLoading
import com.btpj.lib_base.ext.showLoading
import com.btpj.lib_base.utils.LogUtil
import com.btpj.lib_base.utils.StatusBarUtil
import com.btpj.lib_base.utils.ToastUtil
import com.gyf.immersionbar.ktx.immersionBar
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.HttpException
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
/**
* 封装了ViewModel和DataBinding的Activity基类
*
* @author nanfeifei 2021/11/23
*/
abstract class BaseVMBActivity<VM : BaseViewModel, B : ViewDataBinding>(private val contentViewResId: Int) :
AppCompatActivity(), View.OnClickListener {
lateinit var mViewModel: VM
lateinit var mBinding: B
var toolbar: Toolbar? = null
/**
* 重写getResources()方法,让APP的字体不受系统设置字体大小影响
*/
override fun getResources(): Resources? {
val config = Configuration()
config.setToDefaults()
createConfigurationContext(config)
return super.getResources()
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
EventBus.getDefault().register(this)
initViewModel()
initDataBinding()
initImmersionBar()
createObserve()
initView(savedInstanceState)
bindEvent()
initData()
mViewModel.init()
}
open fun initImmersionBar() {
setTransparentStatusBar(transparentStatusBar(), statusBarDarkFont())
initToolBar()
}
/**
*
* @param isTransparent
* @param isStatusBarDarkFont
*/
open fun setTransparentStatusBar(isTransparent: Boolean, isStatusBarDarkFont: Boolean) {
immersionBar {
// reset()
titleBar(getToolBar())
if (isTransparent) {
statusBarColor(android.R.color.transparent)
.fitsSystemWindows(false) //解决状态栏和布局重叠问题
} else {
statusBarColor(R.color.theme_color)
.fitsSystemWindows(true) //解决状态栏和布局重叠问题
}
if (isStatusBarDarkFont) {
statusBarDarkFont(
true,
0.2f
) //原理:如果当前设备支持状态栏字体变色,会设置状态栏字体为黑色,如果当前设备不支持状态栏字体变色,会使当前状态栏加上透明度,否则不执行透明度
} else {
statusBarDarkFont(false, 0.2f)
}
}
}
private fun initToolBar() {
toolbar = getToolBar()
toolbar?.title = ""
toolbar?.navigationIcon = getDrawable(R.drawable.icon_back)
setSupportActionBar(toolbar)
val actionBar: ActionBar? = supportActionBar
actionBar?.setDisplayHomeAsUpEnabled(true)
if (transparentStatusBar()) {
fitTransparentStatusBar(toolbar)
}
toolbar?.setNavigationOnClickListener {
onBackEvent()
// finish()
}
}
fun setToolBarRightText(text: CharSequence){
var tvRight = findViewById<TextView>(R.id.tv_right)
tvRight?.let {
tvRight.visibility = View.VISIBLE
tvRight.text = text
tvRight.setOnClickListener(this)
}
}
fun fitTransparentStatusBar(view: View?){
view?.let {
var statusHeight = StatusBarUtil.getStatusBarHeight(this@BaseVMBActivity)
it.setPadding(0, statusHeight,
0, 0)
var height: Int = it.layoutParams?.height ?: 0
it.layoutParams.height = height + statusHeight
}
}
override fun onBackPressed() {
super.onBackPressed()
}
open fun onBackEvent() {
super.onBackPressed()
}
open fun getToolBar(): Toolbar? {
return findViewById<Toolbar>(R.id.toolbar_lay)
}
/**
* 是否是透明状态栏
*/
open fun transparentStatusBar(): Boolean {
return false
}
/** 状态栏是否是深色*/
open fun statusBarDarkFont(): Boolean {
return true
}
/** ViewModel初始化 */
@Suppress("UNCHECKED_CAST")
open fun initViewModel() {
// 这里利用反射获取泛型中第一个参数ViewModel
val type: Class<VM> =
(this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<VM>
mViewModel = ViewModelProvider(this)[type]
}
/** DataBinding初始化 */
private fun initDataBinding() {
mBinding = DataBindingUtil.setContentView(this, contentViewResId)
mBinding.apply {
// 需绑定lifecycleOwner到activity,xml绑定的数据才会随着liveData数据源的改变而改变
lifecycleOwner = this@BaseVMBActivity
setVariable(BR.viewModel, mViewModel)
}
}
/** View相关初始化 */
abstract fun initView(savedInstanceState: Bundle?)
/**
* 事件监听
*/
protected abstract fun bindEvent()
/**
* 初始化数据
*/
open fun initData() {}
fun showToast(message: String?) {
if (message != null && message.isNotEmpty()) {
ToastUtil.showShort(
this@BaseVMBActivity, message)
}
}
/** 提供编写LiveData监听逻辑的方法 */
open fun createObserve() {
mViewModel.apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED){
toastMessage.collect{ message ->
message?.let {
ToastUtil.showShort(
this@BaseVMBActivity, message)
}
}
}
}
loadingDialog.observe(this@BaseVMBActivity){
when(it){
LOADING_STATE_SHOW -> {
showLoading()
}
LOADING_STATE_HIDE -> {
hideLoading()
}
}
}
// 全局服务器请求错误监听
exception.observe(this@BaseVMBActivity) {
requestError(it.message)
LogUtil.e("Network error${it.message}")
when (it) {
is SocketTimeoutException -> ToastUtil.showShort(
this@BaseVMBActivity,
getString(R.string.request_time_out)
)
is ConnectException, is UnknownHostException -> ToastUtil.showShort(
this@BaseVMBActivity,
getString(R.string.network_error)
)
is HttpException -> {
if(it.code() != 401){
ToastUtil.showShort(
this@BaseVMBActivity, it.message ?: getString(R.string.response_error)
)
}
}
is IOException -> {}
else -> ToastUtil.showShort(
this@BaseVMBActivity, it.message ?: getString(R.string.response_error)
)
}
}
// 全局服务器返回的错误信息监听
errorResponse.observe(this@BaseVMBActivity) {
requestError(it?.message)
it?.message?.run {
ToastUtil.showShort(this@BaseVMBActivity, this)
}
}
}
}
/** 提供一个请求错误的方法,用于像关闭加载框,显示错误布局之类的 */
open fun requestError(msg: String?) {
mViewModel.loadingDialog.value = LOADING_STATE_HIDE
}
fun addClickViews(vararg views: View) {
for (view in views) {
view.setOnClickListener(this)
}
}
override fun onDestroy() {
super.onDestroy()
hideLoading()
EventBus.getDefault().unregister(this)
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: LoginEvent) {
loginStateChange(event.isLogin)
}
/**
* 登录状态发生变化
*/
open fun loginStateChange(isLogin: Boolean) {
}
override fun onClick(v: View?) {
}
}
@@ -0,0 +1,229 @@
package com.btpj.lib_base.base
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.fragment.app.Fragment
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.btpj.lib_base.BR
import com.btpj.lib_base.R
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_HIDE
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_SHOW
import com.btpj.lib_base.event.LoginEvent
import com.btpj.lib_base.ext.hideLoading
import com.btpj.lib_base.ext.showLoading
import com.btpj.lib_base.utils.LogUtil
import com.btpj.lib_base.utils.ToastUtil
import kotlinx.coroutines.launch
import org.greenrobot.eventbus.EventBus
import org.greenrobot.eventbus.Subscribe
import org.greenrobot.eventbus.ThreadMode
import retrofit2.HttpException
import java.io.IOException
import java.lang.reflect.ParameterizedType
import java.net.ConnectException
import java.net.SocketTimeoutException
import java.net.UnknownHostException
/**
* 封装了ViewModel和DataBinding的Fragment基类
*
* @author LTP 2021/11/23
*/
abstract class BaseVMBFragment<VM : BaseViewModel, B : ViewDataBinding>(private val contentViewResId: Int) :
Fragment(), View.OnClickListener {
/** 是否第一次加载 */
private var mIsFirstLoading = true
protected lateinit var mViewModel: VM
protected lateinit var mBinding: B
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
mBinding = DataBindingUtil.inflate(inflater, contentViewResId, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().register(this)
}
mIsFirstLoading = true
initViewModel()
initView(view, savedInstanceState)
setupDataBinding()
createObserve()
bindEvent()
initData()
mViewModel.init()
}
fun setToolBarRightText(text: CharSequence){
var tvRight = mBinding.root.findViewById<TextView>(R.id.tv_right)
tvRight?.let {
tvRight.visibility = View.VISIBLE
tvRight.text = text
tvRight.setOnClickListener(this)
}
}
fun setToolBarRightImage(imageRes: Int){
var ivRight = mBinding.root.findViewById<ImageView>(R.id.iv_right)
ivRight?.let {
ivRight.visibility = View.VISIBLE
ivRight.setImageResource(imageRes)
ivRight.setOnClickListener(this)
}
}
/** ViewModel初始化 */
@Suppress("UNCHECKED_CAST")
open fun initViewModel() {
// 这里利用反射获取泛型中第一个参数ViewModel
val type: Class<VM> =
(this.javaClass.genericSuperclass as ParameterizedType).actualTypeArguments[0] as Class<VM>
mViewModel = ViewModelProvider(this)[type]
}
/** DataBinding相关设置 */
private fun setupDataBinding() {
mBinding.apply {
// 需绑定lifecycleOwner到Fragment,xml绑定的数据才会随着liveData数据源的改变而改变
lifecycleOwner = viewLifecycleOwner
setVariable(BR.viewModel, mViewModel)
}
}
/** View相关初始化 */
abstract fun initView(view: View, savedInstanceState: Bundle?)
/**
* 事件监听
*/
protected abstract fun bindEvent()
/**
* 初始化数据
*/
open fun initData() {}
override fun onResume() {
super.onResume()
if (lifecycle.currentState == Lifecycle.State.STARTED && mIsFirstLoading) {
lazyLoadData()
mIsFirstLoading = false
}
}
/** 数据懒加载 */
open fun lazyLoadData() {}
/** 提供编写LiveData监听逻辑的方法 */
open fun createObserve() {
mViewModel.apply {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.CREATED){
toastMessage.collect{ message ->
message?.let {
ToastUtil.showShort(
requireContext(), message)
}
}
}
}
loadingDialog.observe(viewLifecycleOwner){
when(it){
LOADING_STATE_SHOW -> {
showLoading()
}
LOADING_STATE_HIDE -> {
hideLoading()
}
}
}
// 全局服务器请求错误监听
exception.observe(viewLifecycleOwner) {
requestError(it.message)
LogUtil.e("Network error${it.message}")
when (it) {
is SocketTimeoutException -> ToastUtil.showShort(
requireContext(),
getString(R.string.request_time_out)
)
is ConnectException, is UnknownHostException -> ToastUtil.showShort(
requireContext(),
getString(R.string.network_error)
)
is HttpException -> {
if(it.code() != 401){
ToastUtil.showShort(
requireContext(), it.message ?: getString(R.string.response_error)
)
}
}
is IOException -> {}
else -> ToastUtil.showShort(
requireContext(), it.message ?: getString(R.string.response_error)
)
}
}
// 全局服务器返回的错误信息监听
errorResponse.observe(viewLifecycleOwner) {
requestError(it?.message)
it?.message?.run {
ToastUtil.showShort(requireContext(), this)
}
}
}
}
/** 提供一个请求错误的方法,用于像关闭加载框之类的 */
open fun requestError(msg: String? = null) {
mViewModel.loadingDialog.value = LOADING_STATE_HIDE
}
override fun onDestroyView() {
mBinding == null
super.onDestroyView()
}
override fun onDestroy() {
super.onDestroy()
if (!EventBus.getDefault().isRegistered(this)) {
EventBus.getDefault().unregister(this)
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
fun onEvent(event: LoginEvent) {
loginStateChange(event.isLogin)
}
open fun loginStateChange(isLogin: Boolean) {
}
fun addClickViews(vararg views: View) {
for (view in views) {
view.setOnClickListener(this)
}
}
override fun onClick(v: View?) {
}
}
@@ -0,0 +1,28 @@
package com.btpj.lib_base.base
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.btpj.lib_base.data.bean.ApiResponse
import kotlinx.coroutines.flow.MutableStateFlow
/**
* ViewModel基类
* @author LTP 2021/11/23
*/
abstract class BaseViewModel : ViewModel() {
companion object{
const val LOADING_STATE_SHOW = 1
const val LOADING_STATE_HIDE = 2
}
/** 加载框控制 */
var loadingDialog = MutableLiveData<Int>()
/** 请求异常(服务器请求失败,譬如:服务器连接超时等) */
val exception = MutableLiveData<Exception>()
/** 请求服务器返回错误(服务器请求成功但status错误,譬如:登录过期等) */
val errorResponse = MutableLiveData<ApiResponse<*>?>()
var toastMessage = MutableStateFlow<String?>(null)
/** 界面启动时要进行的初始化逻辑,如网络请求,数据初始化等 */
abstract fun init()
}
@@ -0,0 +1,206 @@
package com.btpj.lib_base.base.adapter
import android.view.View
import android.widget.CompoundButton
import androidx.annotation.LayoutRes
import com.btpj.lib_base.base.adapter.BaseCheckRecycleViewAdapter.CheckItem
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.entity.MultiItemEntity
import com.chad.library.adapter.base.viewholder.BaseViewHolder
/**
* 封装adapter
*
* @param <T>
</T> */
abstract class BaseCheckRecycleViewAdapter<T : CheckItem, VH : BaseViewHolder>
@JvmOverloads constructor(@LayoutRes private val layoutResId: Int, data: MutableList<T>? = null
) : BaseQuickAdapter<T , VH>(layoutResId, data) {
/**
* 判定是否已激活选择模式
*/
var enabledCheckMode //激活选择模式
= false
/**
* 是否是单选模式
*/
/**
* 设置单选模式,默认是复选模式
*/
var singleMode //单选模式
= false
/**
* 单选模式选中后是否可取消选中项
* @param isCancel true为可取消,false为不可取消(必须有一项被选中)
*/
var singleModeIsCanCancel //单选模式选中后是否可取消选中项
= false
var currentCheckedPosition = -1
private var secondCheckedPosition = -1
/**
* 处理复选框
*/
fun handleCompoundButton(compoundButton: CompoundButton, t: T) {
compoundButton.isChecked = t!!.checked
compoundButton.visibility =
if (enabledCheckMode) View.VISIBLE else View.GONE
}
/**
* 激活选择模式
*/
fun enableCheckMode() {
if (enabledCheckMode) {
return
}
enabledCheckMode = true
notifyDataSetChanged()
}
/**
* 取消选择模式
*/
fun cancelCheckMode() {
if (!enabledCheckMode) {
return
}
enabledCheckMode = false
for (item in data) {
item!!.checked = false
}
notifyDataSetChanged()
}
/**
* 点击了某一项
*
* @return true:已经激活了选择模式并且设置成功;false:尚未激活选择模式并且设置失败
*/
fun clickItem(position: Int, isSecond: Boolean): Boolean {
var position = position
return if (enabledCheckMode) {
if (position < data.size) {
if (singleMode) {
if (isSecond) {
if (secondCheckedPosition == -1) {
val item: T = data[position]
item!!.checked = true
} else if (secondCheckedPosition == position) {
if (singleModeIsCanCancel) {
val item: T = data.get(position)
item!!.checked = !item.checked
}
} else {
if (currentCheckedPosition < data.size) {
data[currentCheckedPosition].checked = false
}
data.get(position).checked = true
}
secondCheckedPosition = position
} else {
secondCheckedPosition = -1
if (currentCheckedPosition == -1) {
val item: T = data[position]
item!!.checked = !item.checked
} else if (currentCheckedPosition == position) {
if (singleModeIsCanCancel) {
val item: T = data.get(position)
item!!.checked = !item.checked
}
} else {
if (currentCheckedPosition < data.size) {
data[currentCheckedPosition].checked = false
}
data[position].checked = true
}
currentCheckedPosition = position
}
} else {
val item: T = data.get(position)
item!!.checked = !item.checked
}
notifyDataSetChanged()
}
true
} else {
false
}
}
/**
* 全选
*
* @return true:已经激活了选择模式并且设置成功;false:尚未激活选择模式并且设置失败
*/
fun checkAll(checked: Boolean): Boolean {
return if (enabledCheckMode) {
for (i in 0 until data.size) {
val item: T = data[i]
item!!.checked = checked
}
notifyDataSetChanged()
if (!checked) {
currentCheckedPosition = -1
secondCheckedPosition = -1
}
true
} else {
false
}
}
/**
* 获取选中的项
*/
val checkedItems: List<T>
get() {
val checkedItems: MutableList<T> = ArrayList()
for (item in data) {
if (item!!.checked) {
checkedItems.add(item)
}
}
return checkedItems
}
/**
* 获取集合中选中的项
*/
fun getCheckedItems(list: List<T>): List<T> {
val checkedItems: MutableList<T> = ArrayList()
for (item in list) {
if (item!!.checked) {
checkedItems.add(item)
}
}
return checkedItems
}
/**
* 删除选中的项
*/
fun deleteCheckedItems(): List<T> {
val checkedItems: MutableList<T> = ArrayList()
val iterator: MutableIterator<T> = data.iterator()
var item: T
while (iterator.hasNext()) {
item = iterator.next()
if (item!!.checked) {
checkedItems.add(item)
iterator.remove()
}
}
notifyDataSetChanged()
currentCheckedPosition = -1
secondCheckedPosition = -1
return checkedItems
}
interface CheckItem : MultiItemEntity {
var checked: Boolean
}
}
@@ -0,0 +1,24 @@
package com.btpj.lib_base.base.adapter
import android.view.ViewGroup
import androidx.annotation.LayoutRes
import androidx.databinding.ViewDataBinding
import com.chad.library.adapter.base.BaseQuickAdapter
import com.chad.library.adapter.base.util.getItemView
import com.chad.library.adapter.base.viewholder.BaseDataBindingHolder
/**
* @author nanfeifei
* @time 2023/5/5 14:07
* @description 基于第三方库BaseRecyclerViewAdapterHelper拓展使用DataBinding的Adapter
*/
abstract class BaseDataBindingAdapter<T, BD : ViewDataBinding>
@JvmOverloads constructor(@LayoutRes private val layoutResId: Int, data: MutableList<T>? = null
) : BaseQuickAdapter<T, BaseDataBindingHolder<BD>>(layoutResId, data) {
override fun onCreateDefViewHolder(
parent: ViewGroup,
viewType: Int
): BaseDataBindingHolder<BD> {
return BaseDataBindingHolder(parent.getItemView(layoutResId))
}
}
@@ -0,0 +1,16 @@
package com.btpj.lib_base.data.bean
/**
* 接口返回外层封装实体
*
* @author LTP 2022/3/22
*/
data class ApiResponse<T>(
var result: T?,
var code: Int,
val message: String?,
val success: Boolean,
val timestamp: Long,
val ok: Boolean,
val data: T?
)
@@ -0,0 +1,16 @@
package com.btpj.lib_base.data.bean
/**
* 分页实体
*
* @author LTP 2022/3/22
*/
data class PageResponse<T>(
val curPage: Int,
val datas: List<T>,
val offset: Int,
val over: Boolean,
val pageCount: Int,
val size: Int,
val total: Int
)
@@ -0,0 +1,221 @@
package com.btpj.lib_base.data.local
import android.content.Context
import android.util.Log
import com.btpj.lib_base.utils.DataStoreUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
object DataStoreManager {
private const val TAG = "CommonSPUtils"
private const val KEY_TOKEN = "token"
private const val KEY_OPEN_APP = "openApp"
private const val KEY_PRIVACY_POLICY = "isAgree"
private const val KEY_PRIVACY_AGREEMENT_URL = "privacyAgreementUrl"
private const val KEY_USER_AGREEMENT_URL = "userAgreementUrl"
private const val KEY_USER_ID = "userId"
private const val KEY_USER_INFO = "userInfo"
private const val KEY_USER_INFO2 = "userInfo2"
private const val KEY_FCM_TOKEN = "fcmToken"
private const val KEY_PRIVACY = "privacy"
private const val KEY_APP_INSTANCE_ID = "appInstanceId"
private const val KEY_USER_LAW_URL = "userLawUrl"
private lateinit var dataStore: DataStoreUtils
fun initialize(context: Context?) {
if (context == null) {
Log.w(TAG, "initialize: context is null")
return
}
context?.apply {
dataStore = DataStoreUtils.init(this)
}
}
/** 保存弹窗隐私隐私是否同意 */
fun savePrivacyState(isAgree: Boolean) {
CoroutineScope(Dispatchers.IO).launch {
dataStore?.putData(KEY_PRIVACY, isAgree)
}
}
/** 是否弹窗已同意隐私协议 */
fun isPrivacyState(): Boolean {
return if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_PRIVACY, false)
} else {
return false
}
}
/**
* 已打开APP,保持状态
*/
fun setOpenApp(openApp: Boolean){
CoroutineScope(Dispatchers.IO).launch {
dataStore?.putData(KEY_OPEN_APP, openApp)
}
}
/**
* 是否是第一次打开APP
*/
fun isOpenApp(): Boolean{
return if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_OPEN_APP, false)
}else{
return false
}
}
/** 保存隐私隐私是否同意 */
fun saveAgreePrivacyPolicyStatus(isAgree: Boolean){
CoroutineScope(Dispatchers.IO).launch {
dataStore?.putData(KEY_PRIVACY_POLICY, isAgree)
}
}
/** 是否已同意隐私协议 */
fun isAgreePrivacyPolicyStatus(): Boolean {
return if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_PRIVACY_POLICY, false)
}else{
return false
}
}
/** 保持Token信息 */
fun saveToken(token: String){
dataStore?.putSyncData(KEY_TOKEN, token)
}
/** 获取Token信息 */
fun getToken(): String{
var token = if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_TOKEN, "")
}else{
""
}
return token
}
/** 保持FcmToken信息 */
fun saveFcmToken(fcmToken: String){
dataStore?.putSyncData(KEY_FCM_TOKEN, fcmToken)
}
/** 获取FcmToken信息 */
fun getFcmToken(): String{
var fcmToken = if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_FCM_TOKEN, "")
}else{
""
}
return fcmToken
}
/** 保持FcmToken信息 */
fun saveAppInstanceId(appInstanceId: String){
dataStore?.putSyncData(KEY_APP_INSTANCE_ID, appInstanceId)
}
/** 获取FcmToken信息 */
fun getAppInstanceId(): String{
var appInstanceId = if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_APP_INSTANCE_ID, "")
}else{
""
}
return appInstanceId
}
/** 保存User信息 */
fun saveUserInfo(userInfo: String){
dataStore?.putSyncData(KEY_USER_INFO, userInfo)
}
/** 保存备份User信息 */
fun saveUserInfo2(userInfo: String) {
dataStore?.putSyncData(KEY_USER_INFO2, userInfo)
}
/** 获取User信息 */
fun getUserInfo(): String{
var userInfo = if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_USER_INFO, "")
}else{
""
}
return userInfo
}/** 获取User信息 */
fun getUserInfo2(): String{
var userInfo = if (DataStoreManager::dataStore.isInitialized) {
dataStore.getSyncData(KEY_USER_INFO2, "")
}else{
""
}
return userInfo
}
fun isLogin(): Boolean {
return getToken().isNotEmpty()
}
/**
* 保存隐私协议地址
*/
fun savePrivacyAgreementUrl(url: String){
dataStore?.putSyncData(KEY_PRIVACY_AGREEMENT_URL, url)
}
/**
* 获取隐私协议地址
*/
fun getPrivacyAgreementUrl(): String? {
return if(DataStoreManager::dataStore.isInitialized){
dataStore.getSyncData(KEY_PRIVACY_AGREEMENT_URL, "")
}else{
""
}
}
/**
* 保存用户协议地址
*/
fun saveUserAgreementUrl(url: String){
dataStore?.putSyncData(KEY_USER_AGREEMENT_URL, url)
}
/**
* 获取用户协议地址
*/
fun getUserAgreementUrl(): String? {
return if (DataStoreManager::dataStore.isInitialized){
dataStore?.getSyncData(KEY_USER_AGREEMENT_URL, "")
}else{
""
}
}
/**
* 保存用户协议地址
*/
fun savePermissionAgreementUrl(url: String){
dataStore?.putSyncData(KEY_USER_AGREEMENT_URL, url)
}
/**
* 获取用户协议地址
*/
fun getPermissionAgreementUrl(): String? {
return if (DataStoreManager::dataStore.isInitialized){
dataStore?.getSyncData(KEY_USER_AGREEMENT_URL, "")
}else{
""
}
}
fun saveLawUrl(url: String) {
dataStore?.putSyncData(KEY_USER_LAW_URL, url)
}
fun getLawUrl(): String? {
return if (DataStoreManager::dataStore.isInitialized) {
dataStore?.getSyncData(KEY_USER_LAW_URL, "")
} else {
""
}
}
fun saveUserId(userId: Int){
dataStore?.putSyncData(KEY_USER_ID, userId)
}
fun getUserId(): Int? {
return if (DataStoreManager::dataStore.isInitialized){
dataStore?.getSyncData(KEY_USER_ID, 0)
}else{
return 0
}
}
}
@@ -0,0 +1,119 @@
package com.btpj.lib_base.data.local
import android.content.Context
import android.util.Log
import com.btpj.lib_base.utils.DataStoreUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
/**
* 服务器地址管理
* 这个项目用不到,放在这里提供一个模板
*
* @author LTP 2022/4/8
*/
object IpManager {
private const val TAG = "IpManager"
/** 常用的IP */
private const val DEBUG_DEFAULT_IP_ADDRESS_REMOTE = "http://cqyt.dev.yg.dt.io/" // 开发环境
private const val TEST_DEFAULT_IP_ADDRESS_REMOTE = "https://starry.test.icdat.cn/" // 测试环境
// private const val TEST_DEFAULT_IP_ADDRESS_REMOTE = "https://quarry-api.yg.icdat.cn/" // 测试环境
private const val PRODUCT_DEFAULT_IP_ADDRESS_REMOTE = "https://health-api.tzgl.shuziweidao.com/" // 线上环境
private const val DEBUG_H5_ADDRESS_REMOTE = "http://cms.dev.yg.dt.io" // 开发环境
private const val TEST_H5_ADDRESS_REMOTE = "https://starry.out.icdat.cn" // 测试环境
private const val PRODUCT_H5_ADDRESS_REMOTE = "https://console.shuziweidao.com" // 线上环境
private const val KEY_IP_SET = "data_ip_set"
private const val KEY_DEFAULT_IP_AND_PORT = "data_default_ip_and_port"
private lateinit var dataStore: DataStoreUtils
val baseUrlType: BaseUrlType = BaseUrlType.PRODUCT
fun initialize(context: Context?) {
if (context == null) {
Log.w(TAG, "initialize: context is null")
return
}
context.apply {
dataStore = DataStoreUtils.init(this)
}
}
enum class BaseUrlType(val type: Int) {
DEBUG(1),
TEST(2),
PRODUCT(3)
}
private fun getBaseUrl(baseUrlType: BaseUrlType = BaseUrlType.DEBUG): String{
return when(baseUrlType){
BaseUrlType.DEBUG -> {
DEBUG_DEFAULT_IP_ADDRESS_REMOTE
}
BaseUrlType.TEST -> {
TEST_DEFAULT_IP_ADDRESS_REMOTE
}
BaseUrlType.PRODUCT -> {
PRODUCT_DEFAULT_IP_ADDRESS_REMOTE
}
}
}
fun getH5BaseUrl(baseUrlType: BaseUrlType = BaseUrlType.DEBUG): String {
return when (baseUrlType) {
BaseUrlType.DEBUG -> {
DEBUG_H5_ADDRESS_REMOTE
}
BaseUrlType.TEST -> {
TEST_H5_ADDRESS_REMOTE
}
BaseUrlType.PRODUCT -> {
PRODUCT_H5_ADDRESS_REMOTE
}
}
}
/**
* 保存默认IP
*
* @param ip 要保存为默认的IP
*/
fun saveDefaultIP(ip: String) {
dataStore.putSyncData(KEY_DEFAULT_IP_AND_PORT, ip)
}
/** 获取默认IP */
fun getDefaultIP(): String {
var ip= getBaseUrl(baseUrlType)
CoroutineScope(Dispatchers.Unconfined).launch {
val flowDefaultIp = dataStore.getData(KEY_DEFAULT_IP_AND_PORT, ip)
flowDefaultIp.collect{
ip = it
}
}
return ip
}
/** 存储使用过的IP集 */
fun saveIPSet(ipSet: MutableSet<String>) {
dataStore.putSyncData(KEY_IP_SET, ipSet)
}
/** 获取使用过的IP集 */
fun getIPSet(): Flow<Set<String>> {
return dataStore.getData(
KEY_IP_SET, setOf(
PRODUCT_DEFAULT_IP_ADDRESS_REMOTE
)
)
}
fun getImageBaseUrl(): String{
return getDefaultIP() + "file/show/"
}
}
@@ -0,0 +1,71 @@
package com.btpj.lib_base.data.network
import com.btpj.lib_base.data.bean.ApiResponse
import com.btpj.lib_base.http.BaseRepository
import com.btpj.lib_base.http.RetrofitManager
import com.btpj.lib_base.http.api.OtherAPi
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import okhttp3.ResponseBody
object OtherRepository : BaseRepository(), OtherAPi {
private val service by lazy { RetrofitManager.getService(OtherAPi::class.java) }
override suspend fun getOther(url: String): ApiResponse<JsonElement> {
return apiCall { service.getOther(url) }
}
// override suspend fun getBlock(): ApiResponse<Boolean> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getBlockList(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getCoupon(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getFans(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getLike(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getRegister(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getVideoBlock(): ApiResponse<String> {
// return apiCall { service.getBlock() }
// }
//
// override suspend fun getVideoDown(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoIn(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoInfo() {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoLike(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoList(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoOut(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
//
// override suspend fun getVideoUpload(): ApiResponse<String> {
// TODO("Not yet implemented")
// }
}
@@ -0,0 +1,5 @@
package com.btpj.lib_base.event
class LoginEvent(var isLogin: Boolean) {
}
@@ -0,0 +1,80 @@
package com.btpj.lib_base.ext
import androidx.lifecycle.viewModelScope
import com.btpj.lib_base.base.BaseViewModel
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_HIDE
import com.btpj.lib_base.base.BaseViewModel.Companion.LOADING_STATE_SHOW
import com.btpj.lib_base.data.bean.ApiResponse
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
/**
* BaseViewModel的一些扩展方法
*
* @author nanfeifie 2022/3/22
*/
/**
* 启动协程,封装了viewModelScope.launch
*
* @param showDialog 是否显示加载框
* @param tryBlock try语句运行的函数
* @param catchBlock catch语句运行的函数,可以用来做一些网络异常等的处理,默认空实现
* @param finallyBlock finally语句运行的函数,可以用来做一些资源回收等,默认空实现
*/
fun BaseViewModel.launch(
showDialog: Boolean,
tryBlock: suspend CoroutineScope.() -> Unit,
catchBlock: suspend CoroutineScope.() -> Unit = {},
finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
// 默认是执行在主线程,相当于launch(Dispatchers.Main)
viewModelScope.launch {
try {
if(showDialog){
loadingDialog.value = LOADING_STATE_SHOW
}
tryBlock()
} catch (e: Exception) {
exception.value = e
catchBlock()
} finally {
finallyBlock()
}
}
}
fun BaseViewModel.launch(
tryBlock: suspend CoroutineScope.() -> Unit,
catchBlock: suspend CoroutineScope.() -> Unit = {},
finallyBlock: suspend CoroutineScope.() -> Unit = {}
) {
launch(true, tryBlock, catchBlock, finallyBlock)
}
/**
* 请求结果处理
*
* @param response ApiResponse
* @param successBlock 服务器请求成功返回成功码的执行回调,默认空实现
* @param errorBlock 服务器请求成功返回错误码的执行回调,默认返回false的空实现,函数返回值true:拦截统一错误处理,false:不拦截
*/
suspend fun <T> BaseViewModel.handleRequest(
response: ApiResponse<T>,
successBlock: suspend CoroutineScope.(response: ApiResponse<T>) -> Unit = {},
errorBlock: suspend CoroutineScope.(response: ApiResponse<T>) -> Boolean = { false }
) {
coroutineScope {
if(response.success||response.ok){
loadingDialog.value = LOADING_STATE_HIDE
successBlock(response)
}else{
// when (response.code) {
// else -> { // 服务器返回的其他错误码
if (!errorBlock(response)) {
// 只有errorBlock返回false不拦截处理时,才去统一提醒错误提示
errorResponse.value = response
}
// }
}
}
}
@@ -0,0 +1,34 @@
package com.btpj.lib_base.ext
import android.widget.ImageView
import androidx.databinding.BindingAdapter
import com.btpj.lib_base.widgets.TitleLayout
/**
* DataBinding的自定义属性
* @author nanfeifie 2022/4/2
*/
@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(url: String) {
load(url)
}
/**
* ImageView设置圆形图片
* @author LTP 2022/4/2
*/
@BindingAdapter("circleImageUrl")
fun ImageView.setCircleImageUrl(url: String?) {
loadCircle(url)
}
/**
* 这里使用DataBinding的自定义属性方式而不是直接用TitleLayout的app:titleText
* 是因为TitleLayout里定义的setTitleText方法返回值是TitleLayout对象而不是void
* 不想去改TitleLayout里定义的setTitleText方法所以就用DataBinding的自定义属性
*/
@BindingAdapter("titleText")
fun TitleLayout.setTitle(titleText: String?) {
this.setTitleText(titleText ?: "")
}
@@ -0,0 +1,42 @@
package com.btpj.lib_base.ext
import android.annotation.SuppressLint
import android.text.Html
import android.text.Spanned
import com.baileren.rsalibrary.RSACipherStrategy
import com.google.gson.GsonBuilder
import java.util.regex.Pattern
/**
* String扩展类
* @author nanfeifie 2022/3/25
*/
fun String.toHtml(@SuppressLint("InlinedApi") flag: Int = Html.FROM_HTML_MODE_LEGACY): Spanned {
return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.N) {
Html.fromHtml(this, flag)
} else {
Html.fromHtml(this)
}
}
/** 将对象转为JSON字符串 */
fun Any?.toJson(): String {
return GsonBuilder().create().toJson(this)
}
/** 将对象转为key加密之后的字符串 */
fun Any?.toEncryptJson(): String{
return GsonBuilder()
.create().toJson(this)
}
fun <T> String?.jsonToBean(clazz: Class<T>): T {
return GsonBuilder()
.create().fromJson(this, clazz)
}
fun String.isMobile(): Boolean {
var pattern= Pattern.compile("^1[3-9]\\d{9}\$")
return pattern.matcher(this).matches()
}
fun String.encrypt(publicKey: String): String{
var rsaCipherStrategy = RSACipherStrategy()
return rsaCipherStrategy.encrypt(publicKey, this)
}
@@ -0,0 +1,365 @@
package com.btpj.lib_base.ext
import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.annotation.DrawableRes
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.RecyclerView
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.customview.customView
import com.afollestad.materialdialogs.customview.getCustomView
import com.afollestad.materialdialogs.lifecycle.lifecycleOwner
import com.btpj.lib_base.R
import com.btpj.lib_base.data.local.IpManager
import com.btpj.lib_base.utils.ScreenUtil
import com.btpj.lib_base.utils.ScreenUtil.dp2px
import com.bumptech.glide.Glide
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.CircleCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.request.RequestOptions
import com.google.android.material.bottomnavigation.BottomNavigationView
import java.io.File
fun addImageBaseUrl(url: String?): String?{
if (url != null) {
if(url.startsWith("http")){
return url
}
}else {
return null
}
return IpManager.getImageBaseUrl() + url.replace("\\","/")
}
/**
* ImageView利用Glide加载图片
* @param url 图片url(可远程可本地)
* @param showPlaceholder 是否展示placeholder,默认为true
*/
fun ImageView.load(url: String, showPlaceholder: Boolean = true, @DrawableRes defaultResId: Int = 0) {
if (showPlaceholder) {
val options = RequestOptions().transform()
if (defaultResId == 0) {
options.placeholder(R.drawable.ic_default)
.error(R.drawable.ic_default)
}
Glide.with(context).load(addImageBaseUrl(url))
.apply(options)
.into(this)
} else {
Glide.with(context).load(addImageBaseUrl(url))
.into(this)
}
}
/**
* ImageView利用Glide加载图片
* @param url 图片url(可远程可本地)
* @param showPlaceholder 是否展示placeholder,默认为true
*/
fun ImageView.load(file: File, showPlaceholder: Boolean = true, @DrawableRes defaultResId: Int = 0) {
Glide.with(context).load(file)
.transition(DrawableTransitionOptions.withCrossFade(500))
.into(this)
}
/**
* ImageView利用Glide加载图片
* @param resourceId 本地图片资源Id
* @param showPlaceholder 是否展示placeholder,默认为false
*/
fun ImageView.load(@DrawableRes resourceId: Int, showPlaceholder: Boolean = false, @DrawableRes defaultResId: Int = 0) {
Glide.with(context).load(resourceId)
.into(this)
}
/**
* ImageView利用Glide加载圆形图片
* @param url 图片url(可远程可本地)
*/
fun ImageView.loadCircle(url: String?, @DrawableRes defaultResId: Int = 0) {
val options = RequestOptions
.bitmapTransform(CircleCrop())
if (defaultResId == 0) {
options.placeholder(R.drawable.ic_default_head_img)
.error(R.drawable.ic_default_head_img)
}
Glide.with(context).load(addImageBaseUrl(url))
.apply(options)
.into(this)
}
/**
* ImageView利用Glide加载圆形图片
* @param url 图片url(可远程可本地)
*/
fun ImageView.loadCircle(file: File, @DrawableRes defaultResId: Int = 0) {
val options = RequestOptions
.bitmapTransform(CircleCrop())
if (defaultResId == 0) {
options.placeholder(R.drawable.ic_default_head_img)
.error(R.drawable.ic_default_head_img)
}
Glide.with(context).load(file)
.apply(options)
.into(this)
}
/**
* ImageView利用Glide加载圆形图片
* @param resourceId 本地图片资源Id
*/
fun ImageView.loadCircle(@DrawableRes resourceId: Int) {
Glide.with(context).load(resourceId)
// .placeholder(R.drawable.ic_default_img)
.apply(RequestOptions.bitmapTransform(CircleCrop()))
.into(this)
}
/**
* 加载图片到ImageView
* @param imageUrl 图片地址
* @param imageView View
* @param placeholder 占位图
*/
fun ImageView.loadRoundedImage(
url: String?,
roundingRadius: Float,
@DrawableRes defaultResId: Int = 0
) {
//设置图片圆角角度
val roundedCorners = RoundedCorners(dp2px(roundingRadius))
val multiTransformation: MultiTransformation<Bitmap> = MultiTransformation(
CenterCrop(), roundedCorners
)
val options = RequestOptions
.bitmapTransform(multiTransformation)
if (defaultResId == 0) {
options.placeholder(R.drawable.ic_default)
.error(R.drawable.ic_default)
}
Glide.with(context)
.load(addImageBaseUrl(url))
.apply(options)
.into(this)
}
/**
* 加载图片到ImageView
* @param imageUrl 图片地址
* @param imageView View
* @param placeholder 占位图
*/
fun ImageView.loadRoundedImage(
file: File,
roundingRadius: Float
) {
//设置图片圆角角度
val roundedCorners = RoundedCorners(dp2px(roundingRadius))
val multiTransformation: MultiTransformation<Bitmap> = MultiTransformation(
CenterCrop(), roundedCorners
)
val options = RequestOptions
.bitmapTransform(multiTransformation)
Glide.with(context)
.load(file)
.apply(options)
.into(this)
}
/**
* 加载图片到ImageView
* @param imageUrl 图片地址
* @param imageView View
* @param placeholder 占位图
*/
fun ImageView.loadRoundedImage(
imageRes: Int,
roundingRadius: Float
) {
//设置图片圆角角度
val roundedCorners = RoundedCorners(dp2px(roundingRadius))
val multiTransformation: MultiTransformation<Bitmap> = MultiTransformation(
CenterCrop(), roundedCorners
)
val options = RequestOptions
.bitmapTransform(multiTransformation)
if (imageRes != 0) {
Glide.with(context)
.load(imageRes)
.apply(options)
.into(this)
}
}
/**
* SwipeRefreshLayout设置加载主题颜色
* @author LTP 2022/3/24
*/
fun SwipeRefreshLayout.initColors() {
setColorSchemeResources( R.color.theme_color
)
}
/**
* RecyclerView列表为空时的显示视图
*/
fun RecyclerView.getEmptyView(message: String = context.getString(R.string.list_is_empty)): View {
return LayoutInflater.from(context)
.inflate(R.layout.layout_empty, parent as ViewGroup, false).apply {
findViewById<TextView>(R.id.tv_empty).text = message
// findViewById<TextView>(R.id.tv_empty).setOnClickListener {
// refreshCall.invoke()
// }
}
}
/**
* 初始化普通的toolbar 只设置标题
*
* @param titleStr 标题
*/
fun Toolbar.initTitle(titleStr: String = "") {
title = titleStr
}
/**
* 初始化返回键
*
* @param backImg 返回键资源路径
* @param onBack 返回事件
*/
fun Toolbar.initClose(
backImg: Int = R.drawable.ic_back,
onBack: () -> Unit
) {
setNavigationIcon(backImg)
setNavigationOnClickListener { onBack() }
}
/**
* Activity上显示AlertDialog
*
* @param message AlertDialog内容信息
* @param title AlertDialog标题,默认为 "温馨提示"
* @param positiveButtonText AlertDialog右侧按键内容 默认为 "确定"
* @param positiveAction AlertDialog点击右侧按键的行为 默认是空方法
* @param negativeButtonText AlertDialog左侧按键内容 默认为 "取消"
* @param negativeAction AlertDialog点击左侧按键的行为 默认是空方法
*/
fun AppCompatActivity.showDialog(
message: String,
title: String = "Tips",
positiveButtonText: String = "Confirm",
positiveAction: () -> Unit = {},
negativeButtonText: String = "Cancel",
negativeAction: () -> Unit = {}
) {
MaterialDialog(this)
.cancelable(true)
.lifecycleOwner(this)
.show {
title(text = title)
message(text = message)
positiveButton(text = positiveButtonText) { positiveAction.invoke() }
negativeButton(text = negativeButtonText) { negativeAction.invoke() }
}
}
/**
* Fragment上显示AlertDialog
*
* @param message AlertDialog内容信息
* @param title AlertDialog标题,默认为 "温馨提示"
* @param positiveButtonText AlertDialog右侧按键内容 默认为 "确定"
* @param positiveAction AlertDialog点击右侧按键的行为 默认是空方法
* @param negativeButtonText AlertDialog左侧按键内容 默认为 "取消"
* @param negativeAction AlertDialog点击左侧按键的行为 默认是空方法
*/
fun Fragment.showDialog(
message: String,
title: String = "Tips",
positiveButtonText: String = "Confirm",
positiveAction: () -> Unit = {},
negativeButtonText: String = "Cancel",
negativeAction: () -> Unit = {}
) {
MaterialDialog(requireContext())
.cancelable(true)
.lifecycleOwner(viewLifecycleOwner)
.show {
title(text = title)
message(text = message)
positiveButton(text = positiveButtonText) { positiveAction.invoke() }
negativeButton(text = negativeButtonText) { negativeAction.invoke() }
}
}
/** 加载框 */
@SuppressLint("StaticFieldLeak")
private var mLoadingDialog: MaterialDialog? = null
/** 打开加载框 */
fun AppCompatActivity.showLoading(message: String = "加载中") {
if (!this.isFinishing) {
if (mLoadingDialog == null) {
mLoadingDialog = MaterialDialog(this)
.cancelable(true)
.cancelOnTouchOutside(false)
.cornerRadius(6f)
.customView(R.layout.dialog_loading)
.maxWidth(literal = ScreenUtil.dp2px(120f))
.lifecycleOwner(this)
mLoadingDialog?.getCustomView()?.run {
this.findViewById<TextView>(R.id.tv_loadingMsg).text = message
}
}
mLoadingDialog?.show()
}
}
/** 打开加载框 */
fun Fragment.showLoading(message: String = "加载中") {
if (!this.isRemoving&& this.isAdded) {
if (mLoadingDialog == null) {
mLoadingDialog = MaterialDialog(requireContext())
.cancelable(true)
.cancelOnTouchOutside(false)
.cornerRadius(6f)
.customView(R.layout.dialog_loading)
.maxWidth(literal = ScreenUtil.dp2px(120f))
.lifecycleOwner(this)
mLoadingDialog?.getCustomView()?.run {
this.findViewById<TextView>(R.id.tv_loadingMsg).text = message
}
}
mLoadingDialog?.show()
}
}
/** 隐藏Loading加载框 */
fun hideLoading() {
mLoadingDialog?.cancel()
mLoadingDialog = null
}
/**
* 处理BottomNavigationView中的tab长按出现toast的问题
*
* @param ids tab项的id集
*/
fun BottomNavigationView.clearLongClickToast(ids: MutableList<Int>) {
val bottomNavigationView: ViewGroup = getChildAt(0) as ViewGroup
for (position in 0 until ids.size) {
bottomNavigationView.getChildAt(position).findViewById<View>(ids[position])
.setOnLongClickListener { true }
}
}
@@ -0,0 +1,18 @@
package com.btpj.lib_base.http
import com.btpj.lib_base.data.bean.ApiResponse
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
/**
* Repository数据仓库基类,主要用于协程的调用
*
* @author LTP 2022/3/23
*/
open class BaseRepository {
suspend fun <T> apiCall(api: suspend () -> ApiResponse<T>): ApiResponse<T> {
return withContext(Dispatchers.IO) {
api.invoke() }
}
}
@@ -0,0 +1,94 @@
package com.btpj.lib_base.http
import com.btpj.lib_base.BaseApp.Companion.appContext
import com.btpj.lib_base.data.local.IpManager
import com.btpj.lib_base.http.interceptor.CustomSignInterceptor
import com.btpj.lib_base.http.interceptor.logInterceptor
import com.btpj.lib_base.utils.LogUtil
import com.franmontiel.persistentcookiejar.PersistentCookieJar
import com.franmontiel.persistentcookiejar.cache.SetCookieCache
import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
import okhttp3.Authenticator
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.RequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit
/**
* Retrofit管理类
*
* @author nanfeifei 2022/3/21
*/
object RetrofitManager {
/** 请求超时时间 */
private const val TIME_OUT_SECONDS = 1
/** 请求cookie */
val cookieJar: PersistentCookieJar by lazy {
PersistentCookieJar(
SetCookieCache(),
SharedPrefsCookiePersistor(appContext)
)
}
/** 请求根地址 */
val BASE_URL = IpManager.getDefaultIP()
/** OkHttpClient相关配置 */
private val client: OkHttpClient
get() = OkHttpClient.Builder()
.addInterceptor(CustomSignInterceptor())
// 请求过滤器
.addInterceptor(logInterceptor)
.authenticator(Authenticator.JAVA_NET_AUTHENTICATOR)
// .authenticator(object : Authenticator{
// override fun authenticate(route: Route?, response: Response): Request? {
// println("Authenticating for response: $response")
// println("Challenges: " + response.challenges())
// val credential: String = Credentials.basic("jesse", "password1")
// return response.request.newBuilder()
// .header("Authorization", credential)
// .build();
// }
// })
//设置缓存配置,缓存最大10M,设置了缓存之后可缓存请求的数据到data/data/包名/cache/net_cache目录中
// .cache(Cache(File(appContext.cacheDir, "net_cache"), 10 * 1024 * 1024))
// //添加缓存拦截器 可传入缓存天数
// .addInterceptor(CacheInterceptor(30))
// 请求超时时间
.connectTimeout(TIME_OUT_SECONDS.toLong(), TimeUnit.MINUTES)
.readTimeout(TIME_OUT_SECONDS.toLong(), TimeUnit.MINUTES)
.readTimeout(TIME_OUT_SECONDS.toLong(), TimeUnit.MINUTES)
.cookieJar(cookieJar)
.build()
/**
* Retrofit相关配置
*/
private fun initRetrofit(client: OkHttpClient, baseUrl: String?): Retrofit{
return Retrofit.Builder()
.client(client)
// 使用Moshi更适合Kotlin
.addConverterFactory(GsonConverterFactory.create())
.baseUrl(baseUrl ?: BASE_URL)
.build()
}
private val retrofit: Retrofit by lazy {
initRetrofit(client, BASE_URL)
}
fun <T> getService(serviceClass: Class<T>, baseUrl: String? = null): T {
LogUtil.d(BASE_URL)
if(retrofit!= null && baseUrl.isNullOrEmpty()){
return retrofit.create(serviceClass)
}
return initRetrofit(client, baseUrl).create(serviceClass)
}
fun String.toRequestBody(): RequestBody {
return toRequestBody("application/json; charset=utf-8".toMediaTypeOrNull())
}
}
@@ -0,0 +1,86 @@
package com.btpj.lib_base.http.api
import com.btpj.lib_base.data.bean.ApiResponse
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import okhttp3.ResponseBody
import retrofit2.http.Headers
import retrofit2.http.POST
import retrofit2.http.Url
interface OtherAPi {
@Headers("Content-Type: application/json")
@POST
suspend fun getOther(@Url url: String)
: ApiResponse<JsonElement>
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/block")
// suspend fun getBlock()
// : ApiResponse<Boolean>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/block/list")
// suspend fun getBlockList()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/coupon")
// suspend fun getCoupon()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/fans")
// suspend fun getFans()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/like")
// suspend fun getLike()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/customer/register")
// suspend fun getRegister()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/block")
// suspend fun getVideoBlock()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/down")
// suspend fun getVideoDown()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/in")
// suspend fun getVideoIn()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/info")
// suspend fun getVideoInfo()
//
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/like")
// suspend fun getVideoLike()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/list")
// suspend fun getVideoList()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/out")
// suspend fun getVideoOut()
// : ApiResponse<String>
//
// @Headers("Content-Type: application/json")
// @POST("/customer/video/upload")
// suspend fun getVideoUpload()
// : ApiResponse<String>
}
@@ -0,0 +1,78 @@
/*
* Copyright (C) 2015 Square, Inc.
*
* 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.btpj.lib_base.http.factory;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.reflect.TypeToken;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import okhttp3.RequestBody;
import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;
/**
* A {@linkplain Converter.Factory converter} which uses Gson for JSON.
*
* <p>Because Gson is so flexible in the types it supports, this converter assumes that it can
* handle all types. If you are mixing JSON serialization with something else (such as protocol
* buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this
* instance} last to allow the other converters a chance to see their types.
*/
public final class GsonConverterFactory extends Converter.Factory {
/**
* Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and
* decoding from JSON (when no charset is specified by a header) will use UTF-8.
*/
public static GsonConverterFactory create() {
return create();
}
/**
* Create an instance using {@code gson} for conversion. Encoding to JSON and decoding from JSON
* (when no charset is specified by a header) will use UTF-8.
*/
@SuppressWarnings("ConstantConditions") // Guarding public API nullability.
public static GsonConverterFactory create(Gson gson) {
if (gson == null) throw new NullPointerException("gson == null");
return new GsonConverterFactory(gson);
}
private final Gson gson;
private GsonConverterFactory(Gson gson) {
this.gson = gson;
}
@Override
public Converter<ResponseBody, ?> responseBodyConverter(
Type type, Annotation[] annotations, Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonResponseBodyConverter<>(gson, adapter);
}
@Override
public Converter<?, RequestBody> requestBodyConverter(
Type type,
Annotation[] parameterAnnotations,
Annotation[] methodAnnotations,
Retrofit retrofit) {
TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
return new GsonRequestBodyConverter<>(gson, adapter);
}
}
@@ -0,0 +1,38 @@
package com.btpj.lib_base.http.factory;
import com.google.gson.Gson;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.nio.charset.Charset;
import okhttp3.MediaType;
import okhttp3.RequestBody;
import okio.Buffer;
import retrofit2.Converter;
public final class GsonRequestBodyConverter<T> implements Converter<T, RequestBody> {
private static final MediaType MEDIA_TYPE = MediaType.get("application/json; charset=UTF-8");
private static final Charset UTF_8 = Charset.forName("UTF-8");
private final Gson gson;
private final TypeAdapter<T> adapter;
public GsonRequestBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public RequestBody convert(T value) throws IOException {
Buffer buffer = new Buffer();
Writer writer = new OutputStreamWriter(buffer.outputStream(), UTF_8);
JsonWriter jsonWriter = gson.newJsonWriter(writer);
adapter.write(jsonWriter, value);
jsonWriter.close();
return RequestBody.create(MEDIA_TYPE, buffer.readByteString());
}
}
@@ -0,0 +1,39 @@
package com.btpj.lib_base.http.factory;
import com.google.gson.Gson;
import com.google.gson.JsonElement;
import com.google.gson.JsonIOException;
import com.google.gson.JsonParser;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import java.io.IOException;
import okhttp3.ResponseBody;
import retrofit2.Converter;
public final class GsonResponseBodyConverter<T> implements Converter<ResponseBody, T> {
private final Gson gson;
private final TypeAdapter<T> adapter;
public GsonResponseBodyConverter(Gson gson, TypeAdapter<T> adapter) {
this.gson = gson;
this.adapter = adapter;
}
@Override
public T convert(ResponseBody value) throws IOException {
JsonReader jsonReader = gson.newJsonReader(value.charStream());
try {
JsonElement parse = new JsonParser().parse(jsonReader);
T result = adapter.fromJsonTree(parse);
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {
throw new JsonIOException("JSON document was not fully consumed.");
}
return result;
} finally {
value.close();
}
}
}
@@ -0,0 +1,228 @@
/*
* Copyright (C) 2017 zhouyou(478319399@qq.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.btpj.lib_base.http.interceptor
import com.orhanobut.logger.Logger
import okhttp3.*
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.MultipartBody.Part.Companion.createFormData
import okhttp3.RequestBody.Companion.toRequestBody
import okio.Buffer
import java.io.IOException
import java.io.UnsupportedEncodingException
import java.net.URLDecoder
import java.net.URLEncoder
import java.util.*
/**
*
* 描述:动态拦截器
* 主要功能是针对参数:<br></br>
* 1.可以获取到全局公共参数和局部参数,统一进行签名sign<br></br>
* 2.可以自定义动态添加参数,类似时间戳timestamp是动态变化的,token(登录了才有),参数签名等<br></br>
* 3.参数值是经过UTF-8编码的<br></br>
* 4.默认提供询问是否动态签名(签名需要自定义),动态添加时间戳等<br></br>
* 作者: nanfeifei<br></br>
* 日期: 2017/5/3 15:32 <br></br>
* 版本: v1.0<br></br>
*/
abstract class BaseCustomDynamicInterceptor : Interceptor {
private var httpUrl: HttpUrl? = null
@Throws(IOException::class)
override fun intercept(chain: Interceptor.Chain): Response {
var request: Request = chain.request()
var newBuilder: Request.Builder = request.newBuilder()
getHttpUrl(request.url)?.let { newBuilder = newBuilder.url(it) }
request = dynamicHeader(newBuilder).build()
if (request.method == "GET") {
httpUrl = parseUrl(request.url.toUrl().toString()).toHttpUrlOrNull()
request = addGetParamsSign(request)
} else if (request.method == "POST") {
httpUrl = request.url
request = addPostParamsSign(request)
}
var response = chain.proceed(request)
return response
}
private fun getHttpUrl(httpUrl: HttpUrl): HttpUrl? {
var oldUri = httpUrl.toUri()
Logger.d(oldUri.path)
return httpUrl
}
//get 添加签名和公共动态参数
@Throws(UnsupportedEncodingException::class)
private fun addGetParamsSign(request: Request): Request {
var request = request
var httpUrl: HttpUrl = request.url
val newBuilder: HttpUrl.Builder = httpUrl.newBuilder()
//获取原有的参数
val nameSet: Set<String> = httpUrl.queryParameterNames
val nameList = ArrayList<String>()
nameList.addAll(nameSet)
val oldparams = TreeMap<String, String>()
for (i in nameList.indices) {
val value: String = if (httpUrl.queryParameterValues(nameList[i]) != null
&& httpUrl.queryParameterValues(nameList[i]).isNotEmpty()
)
httpUrl.queryParameterValues(nameList[i])[0].toString() else ""
oldparams[nameList[i]] = value
}
val nameKeys = listOf(nameList).toString()
//拼装新的参数
val newParams = dynamic(oldparams)
for ((key, value) in newParams) {
val urlValue = URLEncoder.encode(value, Charsets.UTF_8.name())
//原来的URl: https://xxx.xxx.xxx/app/chairdressing/skinAnalyzePower/skinTestResult?appId=10101
if (!nameKeys.contains(key)) { //避免重复添加
newBuilder.addQueryParameter(key, urlValue)
}
}
httpUrl = newBuilder.build()
request = request.newBuilder().url(httpUrl).build()
return request
}
//post 添加签名和公共动态参数
@Throws(UnsupportedEncodingException::class)
private fun addPostParamsSign(request: Request): Request {
var request = request
if (request.body is FormBody) {
val bodyBuilder = FormBody.Builder()
var formBody: FormBody? = request.body as FormBody?
//原有的参数
val oldparams = TreeMap<String, String>()
if (formBody != null) {
for (i in 0 until formBody.size) {
oldparams[formBody.encodedName(i)] = formBody.encodedValue(i)
}
}
//拼装新的参数
val newParams = dynamic(oldparams)
//Logc.i("======post请求参数===========");
for ((key, value1) in newParams) {
val value = URLDecoder.decode(value1, Charsets.UTF_8.name())
bodyBuilder.addEncoded(key, value)
//Logc.i(entry.getKey() + " -> " + value);
}
formBody = bodyBuilder.build()
request = request.newBuilder().post(formBody).build()
} else if (request.body is MultipartBody) {
var multipartBody: MultipartBody? = request.body as MultipartBody?
val bodyBuilder: MultipartBody.Builder =
MultipartBody.Builder().setType(MultipartBody.FORM)
val oldparts: List<MultipartBody.Part> = multipartBody?.parts ?: ArrayList()
//拼装新的参数
val newparts: MutableList<MultipartBody.Part> = ArrayList<MultipartBody.Part>()
newparts.addAll(oldparts)
val oldparams = TreeMap<String, String>()
val newParams = dynamic(oldparams)
for ((key, value) in newParams) {
val part: MultipartBody.Part = createFormData(key, value)
newparts.add(part)
}
for (part in newparts) {
bodyBuilder.addPart(part)
}
multipartBody = bodyBuilder.build()
request = request.newBuilder().post(multipartBody).build()
} else if (isPlainJson(request.body!!.contentType())) {
var oldJsonStr = bodyToString(request)
val postJsonStr = dynamicJson(oldJsonStr)
val requestBody: RequestBody =
postJsonStr.toRequestBody(request.body!!.contentType())
request = request.newBuilder().post(requestBody).build()
}
return request
}
//解析前:https://xxx.xxx.xxx/app/chairdressing/skinAnalyzePower/skinTestResult?appId=10101
//解析后:https://xxx.xxx.xxx/app/chairdressing/skinAnalyzePower/skinTestResult
private fun parseUrl(url: String): String {
var url = url
if ("" != url && url.contains("?")) { // 如果URL不是空字符串
url = url.substring(0, url.indexOf('?'))
}
return url
}
/**
* 将提交内容转化为字符串
* @param request
* @return
*/
private fun bodyToString(request: Request): String? {
try {
val copy = request.newBuilder().build()
val buffer = Buffer()
copy.body!!.writeTo(buffer)
return buffer.readUtf8()
} catch (e: Exception) {
e.printStackTrace()
}
return null
}
/**
* 动态处理Header
*
* @param builder
* @return 返回新的参数集合
*/
abstract fun dynamicHeader(builder: Request.Builder): Request.Builder
/**
* 动态处理参数(不包含Json形式)
*
* @param dynamicMap
* @return 返回新的参数集合
*/
abstract fun dynamic(dynamicMap: TreeMap<String, String>?): TreeMap<String, String>
/**
* 动态处理参数(Json形式)
*
* @param json
* @return 返回新的参数集合
*/
abstract fun dynamicJson(json: String?): String
companion object {
/**
* 判断提交内容是不是json格式
* @param mediaType
* @return
*/
fun isPlainJson(mediaType: MediaType?): Boolean {
if (mediaType == null) return false
var subtype = mediaType.subtype
if (subtype != null) {
subtype = subtype.lowercase(Locale.getDefault())
if (subtype.contains("json")) //
return true
}
return false
}
}
}
@@ -0,0 +1,39 @@
package com.btpj.lib_base.http.interceptor
import com.btpj.lib_base.BaseApp.Companion.appContext
import com.btpj.lib_base.utils.NetworkUtil
import okhttp3.CacheControl
import okhttp3.Interceptor
import okhttp3.Response
/**
* 缓存拦截器,用于无网情况下传递header直接拉取之前缓存的数据
* @param day 缓存天数
*
* @author nanfeifei 2022/4/14
*/
class CacheInterceptor(private var day: Int = 7) : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
var request = chain.request()
if (!NetworkUtil.isNetworkAvailable(appContext)) {
request = request.newBuilder()
.cacheControl(CacheControl.FORCE_CACHE)
.build()
}
val response = chain.proceed(request)
if (!NetworkUtil.isNetworkAvailable(appContext)) {
val maxAge = 60 * 60
response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, max-age=$maxAge")
.build()
} else {
val maxStale = 60 * 60 * 24 * day
response.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "public, only-if-cached, max-stale=$maxStale")
.build()
}
return response
}
}
@@ -0,0 +1,82 @@
/*
* Copyright (C) 2017 zhouyou(478319399@qq.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.btpj.lib_base.http.interceptor
import android.text.TextUtils
import com.btpj.lib_base.data.local.DataStoreManager
import com.btpj.lib_base.ext.toJson
import com.btpj.lib_base.utils.LogUtil
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import okhttp3.Request
import java.util.TreeMap
/**
*
* 描述:对参数进行签名、添加token、时间戳处理的拦截器
* 主要功能说明:<br></br>
* 因为参数签名没办法统一,签名的规则不一样,签名加密的方式也不同有MD5、BASE64等等,只提供自己能够扩展的能力。<br></br>
* 作者: nanfeifei<br></br>
* 日期: 2017/5/4 15:21 <br></br>
* 版本: v1.0<br></br>
*/
class CustomSignInterceptor : BaseCustomDynamicInterceptor() {
companion object{
const val BODY_NO_ENCODE = "bodyNoEncode"
}
override fun dynamicHeader(builder: Request.Builder): Request.Builder {
// builder.addHeader("Content-Type", "application/json")
builder.addHeader("X-Access-Token", DataStoreManager.getToken())
return builder
}
override fun dynamic(dynamicMap: TreeMap<String, String>?): TreeMap<String, String> {
//dynamicMap:是原有的全局参数+局部参数
// dynamicMap?.set("marketId".asEncode(), marketId.toString()) //示例
return dynamicMap!! //dynamicMap:是原有的全局参数+局部参数+新增的动态参数
}
override fun dynamicJson(json: String?): String {
var jsonObj: Any = if(TextUtils.isEmpty(json)){
JsonObject()
}else{
JsonParser.parseString(json).asJsonObject
}
json?.let { LogUtil.d(it) }
if(jsonObj is JsonObject){
// jsonObj.addProperty("marketId".asEncode(), marketId) //示例
return if(jsonObj.has(BODY_NO_ENCODE)){
jsonObj.remove(BODY_NO_ENCODE)
encrypt(jsonObj.toJson(), false)
}else{
jsonObj.remove(BODY_NO_ENCODE)
encrypt(jsonObj.toJson(), true)
}
}else{
return json!!
}
}
/**
* @param requestJson 请求字符串
* @param isBodySign Body是否加密,无要求直接返回即可
*/
private fun encrypt(requestJson: String, isBodySign: Boolean): String {
LogUtil.d(requestJson)
return requestJson
}
}
@@ -0,0 +1,16 @@
package com.btpj.lib_base.http.interceptor
import com.btpj.lib_base.BuildConfig
import com.btpj.lib_base.utils.LogUtil
import okhttp3.logging.HttpLoggingInterceptor
/**
* okhttp 日志拦截器
* @author nanfeifei 2022/3/21
*/
val logInterceptor = HttpLoggingInterceptor(object : HttpLoggingInterceptor.Logger {
override fun log(message: String) {
// 使用自己的日志工具接管
LogUtil.d(message)
}
}).setLevel(if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC)
@@ -0,0 +1,27 @@
package com.btpj.lib_base.utils
import android.content.Context
import android.os.Build
/**
* APP常用工具类,包括获取版本号等
*
* @author nanfeifei 2022/4/12
*/
object AppUtil {
/** 获取版本号名称 */
fun getAppVersionName(context: Context): String {
val packageInfo =
context.applicationContext.packageManager.getPackageInfo(context.packageName, 0)
return packageInfo.versionName
}
/** 获取版本号 */
fun getAppVersionCode(context: Context): Long {
val packageInfo =
context.applicationContext.packageManager.getPackageInfo(context.packageName, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) packageInfo.longVersionCode
else packageInfo.versionCode.toLong()
}
}
@@ -0,0 +1,117 @@
package com.btpj.lib_base.utils
import android.content.Context
import android.os.Environment
import androidx.appcompat.app.AppCompatActivity
import java.io.File
import java.math.BigDecimal
object CacheUtil {
/**
* 获取APP缓存大小
*/
fun getTotalCacheSize(context: Context): String {
var cacheSize = getFolderSize(context.cacheDir)
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
cacheSize += getFolderSize(context.externalCacheDir)
}
return getFormatSize(cacheSize.toDouble())
}
/**
* 清除缓存
*/
fun clearAllCache(activity: AppCompatActivity?) {
activity?.let {
deleteDir(it.cacheDir)
if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
if (it.externalCacheDir == null) {
ToastUtil.showLong(activity, "Cache clearing failure")
}
return
}
it.externalCacheDir?.let { file ->
if (deleteDir(file)) {
ToastUtil.showLong(activity, "Clearing cache succeeded")
}
}
}
}
}
/**
* 删除文件
*
* @param file File
*/
private fun deleteDir(file: File): Boolean {
if (file.isDirectory) {
val children = file.list()
for (i in children.indices) {
val success = deleteDir(File(file, children[i]))
if (!success) {
return false
}
}
}
return file.delete()
}
/**
* 获取文件
* Context.getExternalFilesDir() --> SDCard/Android/data/你的应用的包名/files/
* 目录,一般放一些长时间保存的数据
* Context.getExternalCacheDir() -->
* SDCard/Android/data/你的应用包名/cache/目录,一般存放临时缓存数据
*/
fun getFolderSize(file: File?): Long {
var size: Long = 0
file?.run {
try {
val fileList = listFiles()
for (i in fileList.indices) {
// 如果下面还有文件
size += if (fileList[i].isDirectory) {
getFolderSize(fileList[i])
} else {
fileList[i].length()
}
}
} catch (e: Exception) {
e.printStackTrace()
}
}
return size
}
/**
* 格式化缓存单位
*/
fun getFormatSize(size: Double): String {
val kiloByte = size / 1024
if (kiloByte < 1) {
return size.toString() + "B"
}
val megaByte = kiloByte / 1024
if (megaByte < 1) {
val result1 = BigDecimal(kiloByte.toString())
return result1.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "KB"
}
val gigaByte = megaByte / 1024
if (gigaByte < 1) {
val result2 = BigDecimal(megaByte.toString())
return result2.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "MB"
}
val teraBytes = gigaByte / 1024
if (teraBytes < 1) {
val result3 = BigDecimal(gigaByte.toString())
return result3.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "GB"
}
val result4 = BigDecimal(teraBytes)
return result4.setScale(2, BigDecimal.ROUND_HALF_UP).toPlainString() + "TB"
}
@@ -0,0 +1,31 @@
package com.btpj.lib_base.utils
import android.graphics.Color
import java.util.*
/**
* 一些额外的工具类,不好分类的那种
*
* @author LTP 2022/4/7
*/
object CommonUtil {
/** 判断String是否为空或空串,主要提供给xml中dataBinding用 */
@JvmStatic
fun isEmpty(str: String?): Boolean {
return str?.isEmpty() ?: true
}
/** 获取随机rgb颜色值 */
fun randomColor(): Int {
Random().run {
//0-190, 如果颜色值过大,就越接近白色,就看不清了,所以需要限定范围
val red = nextInt(190)
val green = nextInt(190)
val blue = nextInt(190)
//使用rgb混合生成一种新的颜色,Color.rgb生成的是一个int数
return Color.rgb(red, green, blue)
}
}
}
@@ -0,0 +1,272 @@
package com.btpj.lib_base.utils
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.*
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.io.IOException
/**
*
* 异步获取数据
* [getData] [readBooleanFlow] [readFloatFlow] [readIntFlow] [readLongFlow] [readStringFlow]
* 同步获取数据
* [getSyncData] [readBooleanData] [readFloatData] [readIntData] [readLongData] [readStringData]
*
* 异步写入数据
* [putData] [saveBooleanData] [saveFloatData] [saveIntData] [saveLongData] [saveStringData]
* 同步写入数据
* [putSyncData] [saveSyncBooleanData] [saveSyncFloatData] [saveSyncIntData] [saveSyncLongData] [saveSyncStringData]
*
* 异步清除数据
* [clear]
* 同步清除数据
* [clearSync]
*
* 描述:DataStore 工具类
*
*/
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "HearthExpertClients")
object DataStoreUtils {
private lateinit var dataStore: DataStore<Preferences>
/**
* init Context
* @param context Context
*/
fun init(context: Context): DataStoreUtils {
dataStore = context.dataStore
return this
}
@Suppress("UNCHECKED_CAST")
fun <U> getSyncData(key: String, default: U): U {
val res = when (default) {
is Long -> readLongData(key, default)
is String -> readStringData(key, default)
is Int -> readIntData(key, default)
is Boolean -> readBooleanData(key, default)
is Float -> readFloatData(key, default)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
return res as U
}
@Suppress("UNCHECKED_CAST")
fun <U> getData(key: String, default: U): Flow<U> {
val data = when (default) {
is Long -> readLongFlow(key, default)
is String -> readStringFlow(key, default)
is Int -> readIntFlow(key, default)
is Boolean -> readBooleanFlow(key, default)
is Float -> readFloatFlow(key, default)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
return data as Flow<U>
}
suspend fun <U> putData(key: String, value: U) {
when (value) {
is Long -> saveLongData(key, value)
is String -> saveStringData(key, value)
is Int -> saveIntData(key, value)
is Boolean -> saveBooleanData(key, value)
is Float -> saveFloatData(key, value)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
}
fun <U> putSyncData(key: String, value: U) {
when (value) {
is Long -> saveSyncLongData(key, value)
is String -> saveSyncStringData(key, value)
is Int -> saveSyncIntData(key, value)
is Boolean -> saveSyncBooleanData(key, value)
is Float -> saveSyncFloatData(key, value)
else -> throw IllegalArgumentException("This type can be saved into DataStore")
}
}
private fun readBooleanFlow(key: String, default: Boolean = false): Flow<Boolean> =
dataStore.data
.catch {
//当读取数据遇到错误时,如果是 `IOException` 异常,发送一个 emptyPreferences 来重新使用
//但是如果是其他的异常,最好将它抛出去,不要隐藏问题
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[booleanPreferencesKey(key)] ?: default
}
private fun readBooleanData(key: String, default: Boolean = false): Boolean {
var value = false
runBlocking {
dataStore.data.first {
value = it[booleanPreferencesKey(key)] ?: default
true
}
}
return value
}
private fun readIntFlow(key: String, default: Int = 0): Flow<Int> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[intPreferencesKey(key)] ?: default
}
fun readIntData(key: String, default: Int = 0): Int {
var value = 0
runBlocking {
dataStore.data.first {
value = it[intPreferencesKey(key)] ?: default
true
}
}
return value
}
private fun readStringFlow(key: String, default: String = ""): Flow<String> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[stringPreferencesKey(key)] ?: default
}
private fun readStringData(key: String, default: String = ""): String {
var value = ""
runBlocking {
dataStore.data.first {
value = it[stringPreferencesKey(key)] ?: default
true
}
}
return value
}
private fun readFloatFlow(key: String, default: Float = 0f): Flow<Float> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[floatPreferencesKey(key)] ?: default
}
private fun readFloatData(key: String, default: Float = 0f): Float {
var value = 0f
runBlocking {
dataStore.data.first {
value = it[floatPreferencesKey(key)] ?: default
true
}
}
return value
}
private fun readLongFlow(key: String, default: Long = 0L): Flow<Long> =
dataStore.data
.catch {
if (it is IOException) {
it.printStackTrace()
emit(emptyPreferences())
} else {
throw it
}
}.map {
it[longPreferencesKey(key)] ?: default
}
private fun readLongData(key: String, default: Long = 0L): Long {
var value = 0L
runBlocking {
dataStore.data.first {
value = it[longPreferencesKey(key)] ?: default
true
}
}
return value
}
suspend fun saveBooleanData(key: String, value: Boolean) {
dataStore.edit { mutablePreferences ->
mutablePreferences[booleanPreferencesKey(key)] = value
}
}
private fun saveSyncBooleanData(key: String, value: Boolean) =
runBlocking { saveBooleanData(key, value) }
private suspend fun saveIntData(key: String, value: Int) {
dataStore.edit { mutablePreferences ->
mutablePreferences[intPreferencesKey(key)] = value
}
}
private fun saveSyncIntData(key: String, value: Int) = runBlocking { saveIntData(key, value) }
private suspend fun saveStringData(key: String, value: String) {
dataStore.edit { mutablePreferences ->
mutablePreferences[stringPreferencesKey(key)] = value
}
}
private fun saveSyncStringData(key: String, value: String) = runBlocking { saveStringData(key, value) }
private suspend fun saveFloatData(key: String, value: Float) {
dataStore.edit { mutablePreferences ->
mutablePreferences[floatPreferencesKey(key)] = value
}
}
private fun saveSyncFloatData(key: String, value: Float) = runBlocking { saveFloatData(key, value) }
private suspend fun saveLongData(key: String, value: Long) {
dataStore.edit { mutablePreferences ->
mutablePreferences[longPreferencesKey(key)] = value
}
}
private fun saveSyncLongData(key: String, value: Long) = runBlocking { saveLongData(key, value) }
suspend fun clear() {
dataStore.edit {
it.clear()
}
}
fun clearSync() {
runBlocking {
dataStore.edit {
it.clear()
}
}
}
}
@@ -0,0 +1,741 @@
package com.btpj.lib_base.utils
import android.text.TextUtils
import java.text.ParseException
import java.text.ParsePosition
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Date
import java.util.GregorianCalendar
import java.util.Locale
/**
* 时间处理工具类
*
* @author nanfeifei 2017/9/4
*/
object DateUtil {
/**
* 获得当前年
*
* @return 当前年
*/
val nowYear: String
get() {
val currentTime = Date()
val dateString =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(currentTime)
return dateString.substring(0, 4)
}
/**
* 获得当前月
*
* @return 当前月
*/
val nowMonth: String
get() {
val currentTime = Date()
val dateString =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(currentTime)
return dateString.substring(5, 7)
}
/**
* 获得当前日
*
* @return 当前日
*/
val nowDay: String
get() {
val currentTime = Date()
val dateString =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(currentTime)
return dateString.substring(8, 10)
}
/**
* 获得当前时
*
* @return 当前时
*/
val nowHour: String
get() {
val currentTime = Date()
val dateString =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(currentTime)
return dateString.substring(11, 13)
}
/**
* 获得当前分
*
* @return 当前分
*/
val nowMinute: String
get() {
val currentTime = Date()
val dateString =
SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(currentTime)
return dateString.substring(14, 16)
}
/**
* 获取yyyy-MM-dd型生日时间的年
*
* @param dayStr yyyy-MM-dd型生日时间
* @return yyyy-MM-dd型生日时间的年
*/
fun getBirthdayYear(dayStr: String?): Int {
return try {
dayStr!!.split("-")[0].toInt()
} catch (e: Exception) {
nowYear.toInt()
}
}
/**
* 获取yyyy-MM-dd型生日时间的月
*
* @param dayStr yyyy-MM-dd型生日时间
* @return yyyy-MM-dd型生日时间的月
*/
fun getBirthdayMonth(dayStr: String?): Int {
return try {
dayStr!!.split("-")[1].toInt() - 1
} catch (e: Exception) {
nowMonth.toInt() - 1
}
}
/**
* 获取yyyy-MM-dd型生日时间的日
*
* @param dayStr yyyy-MM-dd型生日时间
* @return yyyy-MM-dd型生日时间的日
*/
fun getBirthdayDay(dayStr: String?): Int {
return try {
dayStr!!.split("-")[2].toInt()
} catch (e: Exception) {
nowDay.toInt()
}
}
/**
* 将短日期转为长日期(例如2018-9-8变成2018-09-08
*
* @param dateStr 短日期 (如2018-9-8)
* @return 长日期 (如2018-09-08)
*/
fun shortDateStrToLong(dateStr: String): String {
val strArray = dateStr.split("-".toRegex(), 3).toTypedArray()
val year = strArray[0]
val month = if (strArray[1].toInt() > 9) strArray[1] else "0${strArray[1]}"
val day = if (strArray[2].toInt() > 9) strArray[2] else "0${strArray[2]}"
return "$year-$month-$day"
}
/**
* 获取当前时间(yyyy-MM-dd)
*
* @return yyyy-MM-dd
*/
fun getNowDayString(): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
}
/**
* 将时间对象转换为年月日时间格式的字符串 yyyy-MM-dd
*
* @param dateDate 时间对象
* @return 中时间格式的字符串 yyyy-MM-dd
*/
fun dateToStrDay(dateDate: Date): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(dateDate)
}
/**
* 获取现在时间
*
* @return 返回字符串格式 yyyy-MM-dd HH:mm:ss
*/
val nowStringDateLong: String
get() = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(Date())
/**
* 获取以现在时间命名的文件名
*
* @return 返回字符串文件名 yyyyMMddHHmmss
*/
val nowDateFileName: String
get() {
val formatter = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault())
return formatter.format(Date())
}
/**
* 获取现在时间
*
* @return 返回短时间字符串格式yyyy-MM-dd
*/
val nowStringDateShort: String
get() = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(Date())
/**
* 获取现在时间 小时:分;秒 HH:mm:ss
*
* @return 返回短时间字符串格式HH:mm:ss
*/
val nowTimeShort: String
get() = SimpleDateFormat("HH:mm:ss", Locale.getDefault()).format(Date())
/**
* 得到现在时间
*
* @return 字符串 yyyyMMdd HHmmss
*/
val stringToday: String
get() {
val currentTime = Date()
val formatter = SimpleDateFormat("yyyyMMdd HHmmss", Locale.getDefault())
return formatter.format(currentTime)
}
/**
* 获取指定时间值(long)对应的日期yyyy-MM-dd
*
* @param currentTimeMills 指定时间值(long)
* @return 指定时间值(long)对应的日期
*/
fun getShortDateStr(currentTimeMills: Long): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(currentTimeMills)
}
/**
* 获取指定时间值(long)对应的日期MM-dd
*
* @param currentTimeMills 指定时间值(long)
* @return 指定时间值(long)对应的日期
*/
fun getMonthDayStr(currentTimeMills: Long): String {
return SimpleDateFormat("MM-dd", Locale.getDefault()).format(currentTimeMills)
}
/**
* 获取指定时间值(long)对应的日期yyyy-MM
*
* @param currentTimeMills 指定时间值(long)
* @return 指定时间值(long)对应的日期
*/
fun getDateYearMonthStr(currentTimeMills: Long): String {
return SimpleDateFormat("yyyy-MM", Locale.getDefault()).format(currentTimeMills)
}
/**
* 获取指定时间值(long)对应的日期yyyy年MM月
*
* @param currentTimeMills 指定时间值(long)
* @return 指定时间值(long)对应的日期
*/
fun getDateYearMonthChineseStr(currentTimeMills: Long): String {
return SimpleDateFormat("yyyy年MM月", Locale.getDefault()).format(currentTimeMills)
}
/**
* 获取指定时间值(long)对应的日期yyyy-MM-dd HH:mm:ss
*
* @param timeMills 指定时间值(long)
* @return 指定时间值(long)对应的日期
*/
@JvmStatic
fun getLongDateStr(timeMills: Long?): String {
if (timeMills == null) {
return ""
}
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(timeMills)
}
/**
* 将时间对象转换为长时间格式的字符串 yyyy-MM-dd HH:mm:ss
*
* @param dateDate Date时间对象
* @return 时间格式为 yyyy-MM-dd HH:mm:ss 的字符串
*/
fun dateToStrLong(dateDate: Date): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(dateDate)
}
/**
* 将中文日期改为短时间格式的字符串 yyyy-MM
* @param dateStr yyyy年MM月
*/
fun chineseDateToStrShortMonth(dateStr: String): String{
var date = SimpleDateFormat("yyyy年MM月", Locale.getDefault()).parse(dateStr)
return SimpleDateFormat("yyyy-MM", Locale.getDefault()).format(date)
}
/**
* 将时间对象转换为中时间格式的字符串 yyyy-MM-dd HH:mm
*
* @param dateDate 时间对象
* @return 中时间格式的字符串 yyyy-MM-dd HH:mm
*/
fun dateToStrMiddle(dateDate: Date): String {
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).format(dateDate)
}
/**
* 将时间对象转换为短时间格式的字符串 yyyy-MM-dd
*
* @param dateDate 时间对象
* @return 短时间格式的字符串 yyyy-MM-dd
*/
fun dateToStrShort(dateDate: Date): String {
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(dateDate)
}
/**
* 将时间对象转换为短时间格式的字符串 yyyy年MM月dd日
*
* @param date 时间戳
* @return 短时间格式的字符串 yyyy年MM月dd日
*/
fun dateToStrShortChinese(date: Long?): String {
if (date == null) {
return ""
}
return SimpleDateFormat("yyyy年MM月dd日", Locale.getDefault()).format(Date(date))
}
/**
* 获取指定时间值(long)对应的年份yyyy
*
* @param dateDate 时间对象
* @return 对应的年份yyyy
*/
fun dateToStrYear(dateDate: Date): String {
return SimpleDateFormat("yyyy", Locale.getDefault()).format(dateDate)
}
/**
* 将时间对象转换为HH:mm时间格式的字符串
*
* @param dateDate 时间对象
* @return 短时间格式的字符串 HH:mm
*/
fun dateToStrHour(dateDate: Date): String {
return SimpleDateFormat("HH:mm").format(dateDate)
}
/**
* 将时间对象转换为HH:mm时间格式的字符串
*
* @param date 时间戳
* @return 短时间格式的字符串 HH:mm
*/
fun dateToStrHour(date: Long?): String {
if (date == null) {
return ""
}
return SimpleDateFormat("HH:mm").format(Date(date))
}
/**
* 获取一个月的最后一天
*
* @param dateStr 日期
* @return 日期所在月的最后一天
*/
fun getEndDateOfMonth(dateStr: String): String {// yyyy-MM-dd
val strArray = dateStr.split("-".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
var str = strArray[0] + "-" + strArray[1] + "-"
val month = strArray[1]
val mon = Integer.parseInt(month)
str += if (mon == 1 || mon == 3 || mon == 5 || mon == 7 || mon == 8 || mon == 10 || mon == 12) {
"31"
} else if (mon == 4 || mon == 6 || mon == 9 || mon == 11) {
"30"
} else {
if (isLeapYear(dateStr)) {
"29"
} else {
"28"
}
}
return str
}
/**
* 将长时间格式字符串yyyy-MM-dd HH:mm:ss 转换为时间
*
* @param strDate yyyy-MM-dd HH:mm:ss
* @return Date类型的时间
*/
fun strToDateLong(strDate: String?): Date {
val pos = ParsePosition(0)
return SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).parse(strDate, pos)
}
/**
* 将长时间格式字符串转换为时间 yyyy-MM-dd HH:mm:ss
*
* @param strDate
* @return
*/
fun strToDateMiddle(strDate: String): Date {
val pos = ParsePosition(0)
return SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault()).parse(strDate, pos)
}
/**
* 将短时间格式字符串转换为时间 yyyy-MM-dd
*
* @param strDate yyyy-MM-dd的短时间格式
* @return 短时间格式字符串转换的时间
*/
fun strToDateShort(strDate: String): Date {
val pos = ParsePosition(0)
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(strDate, pos)
}
/**
* 获取与当前时间(yyyy-MM-dd HH:mm:ss)的间隔毫秒值
*
* @param dateStr 要比较的时间String (yyyy-MM-dd HH:mm:ss)
*/
fun getMinutesSpaceFromNow(dateStr: String): Long {
val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).parse(dateStr)
return date.time - Date().time
}
/**
* 两个时间之间的间隔天数
*
* @param date1 yyyy-MM-dd
* @param date2 yyyy-MM-dd
* @return 间隔天数
*/
fun getDaySpace(date1: String?, date2: String?): Long {
if (date1 == null || "" == date1) {
return 0
}
if (date2 == null || "" == date2) {
return 0
}
return try {
// 转换为标准时间
val date = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date1)
val myDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(date2)
(date.time - myDate.time) / (24 * 60 * 60 * 1000)
} catch (e: Exception) {
e.printStackTrace()
0
}
}
/**
* 修改时间格式(例:2018-12-04 19:08:03 改为 12-04 1908)
*
* @param timeStr 要修改的时间
*/
fun long2MiddleDateStr(timeStr: String?): String {
return when {
timeStr.isNullOrEmpty() -> "--"
timeStr.length > 7 -> timeStr.substring(5, 16)
else -> timeStr
}
}
/**
* 将长时间格式变为短时间格式(例:2018-12-04 19:08:03 改为 2018-12-04)
*
* @param timeStr 要修改的时间 yyyy-MM-dd HH:mm:ss
*
* @return 修改后的时间yyyy-MM-dd(报错返回本身或null返回"--"
*/
fun long2ShortDateStr(timeStr: String?): String {
return try {
timeStr!!.substring(0, 10)
} catch (e: Exception) {
timeStr ?: "--"
}
}
/**
* 获取时间特殊显示,当为当天时显示HH:mm,昨天是显示昨天,否则显示日期(收发文列表中使用)
*
* @param dateStr yyyy-MM-dd HH:mm:ss的字符串时间格式
* @return 当为当天时显示HH:mm,昨天是显示昨天,否则显示具体日期
*/
fun getTimeFromNow(dateStr: String): String {
return if (dateStr.length > 16) {
val nowDateStr = nowStringDateShort
val date = dateStr.substring(0, 10)
when {
getDaySpace(nowDateStr, date) == 0L -> // 当天显示HH:mm即可
dateStr.substring(11, 16)
getDaySpace(nowDateStr, date) == 1L -> "昨天"
else -> date
}
} else {
dateStr
}
}
/**
* 根据用户传入的时间表示格式,返回当前时间的格式 如果是yyyyMMdd,注意字母y不能大写。
*
* @param format yyyyMMddhhmmss
* @return
*/
fun getUserDate(format: String): String {
val currentTime = Date()
val formatter = SimpleDateFormat(format, Locale.getDefault())
return formatter.format(currentTime)
}
/**
* 得到二个日期间的间隔天数
*
* @param startDateStr 开始时间 yyyy-MM-dd
* @param endDateStr 结束时间 yyyy-MM-dd
*
* @return 间格的天数(null表示时间格式不正确)
*/
fun getTwoDay(startDateStr: String, endDateStr: String): Int? {
val day: Int
try {
val startDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(startDateStr)
val endDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).parse(endDateStr)
day = ((endDate.time - startDate.time) / (24 * 60 * 60 * 1000)).toInt()
} catch (e: Exception) {
return null
}
return day
}
/**
* 获取某年某月是否含某一天如果含则返回此天不含则返回当月的最后一天
*
* @param year 某年
* @param month 某月
* @param day 检测所含的天
* @return 获取某年某月是否含某一天如果含则返回此天不含则返回当月的最后一天
*/
fun getCurrentDayInEveryMonth(year: Int, month: Int, day: Int): Int {
return try {
var endDateOfMonth = getEndDateOfMonth("$year-$month-$day")
endDateOfMonth =
endDateOfMonth.substring(endDateOfMonth.length - 2, endDateOfMonth.length)
if (day > 0 && day < Integer.parseInt(endDateOfMonth)) {
day
} else {
Integer.parseInt(endDateOfMonth)
}
} catch (e: NumberFormatException) {
e.printStackTrace()
1
}
}
/**
* 得到一个时间延后或前移几天的时间,nowdate(dd-MM-yyyy)为时间,delay为前移或后延的天数
* 印度格式使用需注意
*/
fun getNextDay(nowDate: String, delay: String): String {
return try {
val mDate: String
val d = strToDateShort(nowDate)
val myTime = d.time / 1000 + Integer.parseInt(delay) * 24 * 60 * 60
d.time = myTime * 1000
mDate = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(d)
mDate
} catch (e: Exception) {
""
}
}
/**
* 功能:<br></br> 距离现在几天的时间是多少
* 获得一个时间字符串,格式为:dd-MM-yyyy
* day 如果为整数,表示未来时间
* 如果为负数,表示过去时间
* 印度格式,使用需注意
*
* @author Tony
* @version 2016年11月29日 上午11:02:56 <br></br>
*/
fun getFromNow(day: Int): String {
val date = Date()
val dateTime = date.time / 1000 + day * 24 * 60 * 60
date.time = dateTime * 1000
return SimpleDateFormat("dd-MM-yyyy", Locale.getDefault()).format(date)
}
/**
* 判断是否润年
*
* @param ddate
* @return
*/
fun isLeapYear(ddate: String): Boolean {
val d = strToDateShort(ddate)
val gc = Calendar.getInstance() as GregorianCalendar
gc.time = d
val year = gc.get(Calendar.YEAR)
return year % 400 == 0 || year % 4 == 0 && year % 100 != 0
}
/**
* 返回当前日期所在周或所在周后weekOffSet周的日期集合
*
* @param weekOffSet 周偏移,上周为-1,本周为0,下周为1,以此类推
*/
fun getWeekList(weekOffSet: Int): ArrayList<String> {
val dateList: ArrayList<String> = ArrayList()
// Locale.FRANCE是由于法国第一天是周一最后一天是周日
val calendar = Calendar.getInstance(Locale.FRANCE)
val format = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
calendar.time = Date()
calendar.add(Calendar.WEEK_OF_YEAR, weekOffSet)
calendar.set(Calendar.DAY_OF_WEEK, Calendar.MONDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.TUESDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.WEDNESDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.THURSDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.FRIDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.SATURDAY)
dateList.add(format.format(calendar.time))
calendar.set(Calendar.DAY_OF_WEEK, Calendar.SUNDAY)
dateList.add(format.format(calendar.time))
return dateList
}
/**
* 返回当前日期前12个月或月份集合,包含当月
*
*/
fun getFirstMonthList(): ArrayList<String> {
val dateList: ArrayList<String> = ArrayList()
// Locale.FRANCE是由于法国第一天是周一最后一天是周日
val calendar = Calendar.getInstance()
val format = SimpleDateFormat("yyyy年MM月", Locale.getDefault())
calendar.time = Date()
//如果是31号通过month-1得到的值为本月1号,故设置成1号
calendar.set(Calendar.DAY_OF_MONTH, 1)
for(index in 0 until 11){
if (index > 0){
calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1)
}
var date = format.format(calendar.time)
dateList.add(date)
}
return dateList
}
/**
* 获取yyyy-MM-dd类型的时间字符串在日历上显示的日期(如2017-08-02显示为2
*
* @param dateStr yyyy-MM-dd类型的时间字符串
*/
fun getDayInCalendar(dateStr: String): String {
return try {
val day = dateStr.substring(dateStr.length - 2, dateStr.length)
if (day.substring(0, 1) == "0") day.substring(1, 2) else day
} catch (e: Exception) {
""
}
}
/**
* 根据一个日期,返回是星期几的字符串
*
* @param date 日期 yyyy-MM-dd
* @return 对应的星期几
*/
fun getWeek(date: String): String {
// 再转换为时间
val d = strToDateShort(date)
val c = Calendar.getInstance()
c.time = d
return SimpleDateFormat("EEEE", Locale.getDefault()).format(c.time)
}
fun getWeekStr(sdate: String): String {
var str: String
str = getWeek(sdate)
when (str) {
"1" -> str = "Sunday"
"2" -> str = "Monday"
"3" -> str = "Tuesday"
"4" -> str = "Wednesday"
"5" -> str = "Thursday"
"6" -> str = "Friday"
"7" -> str = "Saturday"
}
return str
}
fun getWeekStrChinese(week: String?): String {
if(TextUtils.isEmpty(week)){
return ""
}
var str: String = ""
when (week) {
"1" -> str = "周一"
"2" -> str = "周二"
"3" -> str = "周三"
"4" -> str = "周四"
"5" -> str = "周五"
"6" -> str = "周六"
"7" -> str = "周日"
}
return str
}
/**
* 获取上个月的今天
*
* @return yyyy-MM-dd
*/
fun getLastMonthDayDateString(): String {
val calendar = Calendar.getInstance()
calendar.time = Date()
calendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH) - 1) // 设置为上个月
return SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(calendar.time)
}
fun dateDiff(startTime: Long?, endTime: Long?): String {
if(startTime == null || endTime == null){
return ""
}
val nd = (1000 * 24 * 60 * 60).toLong() // 一天的毫秒数
val nh = (1000 * 60 * 60).toLong() // 一小时的毫秒数
val nm = (1000 * 60).toLong() // 一分钟的毫秒数
val ns: Long = 1000 // 一秒钟的毫秒数
val diff: Long
try {
// 获得两个时间的毫秒时间差异
diff = endTime - startTime
val hour = diff / nh // 计算差多少小时
val min = diff % nh / nm // 计算差多少分钟
val sec = diff % nh % nm / ns // 计算差多少秒
// val hourStr = if (hour < 9) "0$hour" else hour.toString() + ""
// val minStr = if (min < 9) "0$min" else min.toString() + ""
// val secStr = if (sec < 9) "0$sec" else sec.toString() + ""
return if(hour > 0){
""+hour+"小时"+min+""+sec+""
}else if(min > 0){
""+min+""+sec+""
}else{
""+sec+""
}
} catch (e: ParseException) {
e.printStackTrace()
}
return ""
}
}
@@ -0,0 +1,257 @@
package com.btpj.lib_base.utils
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.SystemClock
import android.telephony.*
import android.util.Log
import com.btpj.lib_base.BaseApp.Companion.appContext
import com.google.android.gms.ads.identifier.AdvertisingIdClient
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStream
import java.io.InputStreamReader
import java.net.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object DeviceInfoUtils {
/**
* 获取设备名称
*/
fun getDeviceModel(): String {
return Build.MODEL
}
/**
* 调用时需要先申请ACCESS_FINE_LOCATION,需注意
*/
suspend fun getDbm(): Int {
try {
var dbm: Int
val mTelephonyManager =
appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
// val cellInfoList: List<CellInfo>? = mTelephonyManager.allCellInfo
// if (cellInfoList != null) {
// for (cellInfo in cellInfoList) {
// if (cellInfo is CellInfoCdma) {
// return cellInfo.cellSignalStrength.dbm
// }
// if (cellInfo is CellInfoGsm) {
// return cellInfo.cellSignalStrength.dbm
// }
// if (cellInfo is CellInfoLte) {
// return cellInfo.cellSignalStrength.dbm
// }
// if (cellInfo is CellInfoWcdma) {
// return cellInfo.cellSignalStrength.dbm
// }
//
// }
// }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
var dbmSAsync = CoroutineScope(Dispatchers.Default).async {
suspendCoroutine<Int> { dbmSAsync ->
mTelephonyManager.registerTelephonyCallback(
appContext.mainExecutor,
object : TelephonyCallback(),
TelephonyCallback.SignalStrengthsListener {
override fun onSignalStrengthsChanged(signalStrength: SignalStrength) {
var cellSignalStrengths = signalStrength.cellSignalStrengths
if (!cellSignalStrengths.isNullOrEmpty()) {
dbm = cellSignalStrengths[0].dbm
mTelephonyManager.unregisterTelephonyCallback(this)
dbmSAsync.resume(dbm)
}else{
dbmSAsync.resume(0)
}
}
})
}
}
return dbmSAsync.await()
} else {
var dbmAsync = CoroutineScope(Dispatchers.Default).async {
suspendCoroutine<Int> { dbmAsync ->
mTelephonyManager.listen(object : PhoneStateListener() {
override fun onSignalStrengthChanged(asu: Int) {
super.onSignalStrengthChanged(asu)
dbm = -113 + 2 * asu
mTelephonyManager.listen(this, PhoneStateListener.LISTEN_NONE)
dbmAsync.resume(dbm)
}
}, PhoneStateListener.LISTEN_CALL_STATE)
}
}
return dbmAsync.await()
}
} catch (e: Exception) {
LogUtil.e("getDbm", e.message ?: e.toString())
return 0
}
return 0
}
@SuppressLint("MissingPermission")
fun getDbmNeedPermission(): Int {
try {
var dbm: Int
val mTelephonyManager =
appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val cellInfoList: List<CellInfo?> = mTelephonyManager.allCellInfo
if (cellInfoList != null) {
for (cellInfo in cellInfoList) {
if (cellInfo is CellInfoCdma) {
return cellInfo.cellSignalStrength.dbm
}
if (cellInfo is CellInfoGsm) {
return cellInfo.cellSignalStrength.dbm
}
if (cellInfo is CellInfoLte) {
return cellInfo.cellSignalStrength.dbm
}
if (cellInfo is CellInfoWcdma) {
return cellInfo.cellSignalStrength.dbm
}
}
}
} catch (e: Exception) {
LogUtil.e("getDbm", e.message ?: e.toString())
return 0
}
return 0
}
/**
* 判断是否包含SIM卡
*
* @return 状态
*/
private fun hasSimCard(context: Context): Boolean {
val telMgr: TelephonyManager =
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
val simState: Int = telMgr.simState
var result = true
when (simState) {
TelephonyManager.SIM_STATE_ABSENT, TelephonyManager.SIM_STATE_UNKNOWN -> result =
false // 没有SIM卡
}
return result
}
//获取 GAID
suspend fun getGAID(): String {
var gaid = ""
gaid = withContext(Dispatchers.IO) {
var adInfo: AdvertisingIdClient.Info? = null
try {
adInfo = AdvertisingIdClient.getAdvertisingIdInfo(appContext)
} catch (e: IOException) {
// Unrecoverable error connecting to Google Play services (e.g.,
// the old version of the service doesn't support getting AdvertisingId).
Log.e("getGAID", "IOException")
} catch (e: GooglePlayServicesNotAvailableException) {
// Google Play services is not available entirely.
Log.e("getGAID", "GooglePlayServicesNotAvailableException")
} catch (e: Exception) {
Log.e("getGAID", "Exception:$e")
// Encountered a recoverable error connecting to Google Play services.
}
adInfo?.id ?: ""
}
return gaid
}
fun getMac(): String? {
return MacUtil.getMac()
}
fun getSerialNumber(): String {
// val tm: TelephonyManager? =
// appContext.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?
return Build.SERIAL
}
fun getElapsedRealtime(): Long {
return SystemClock.elapsedRealtime()
}
suspend fun getIP(): String {
// try {
// val en = NetworkInterface.getNetworkInterfaces()
// while (en.hasMoreElements()) {
// val intf = en.nextElement()
// val enumIpAddr = intf.inetAddresses
// while (enumIpAddr.hasMoreElements()) {
// val inetAddress = enumIpAddr.nextElement()
// if (!inetAddress.isLoopbackAddress && inetAddress is Inet4Address) {
// // return inetAddress.getAddress().toString();
// return inetAddress.hostAddress.toString()
// }
// }
// }
// } catch (ex: SocketException) {
// Log.e("BaseScanTvDeviceClient", "获取本机IP false =" + ex.toString())
// }
var ip = withContext(Dispatchers.IO) {
getNetIp()
}
return ip
}
/**
* 获取外网IP地址
* @return
*/
fun getNetIp(): String {
var line: String? = ""
var infoUrl: URL? = null
var inStream: InputStream? = null
try {
infoUrl = URL("http://whatismyip.akamai.com/")
val connection = infoUrl.openConnection()
val httpConnection = connection as HttpURLConnection
val responseCode = httpConnection.responseCode
if (responseCode == HttpURLConnection.HTTP_OK) {
inStream = httpConnection.inputStream
val reader = BufferedReader(InputStreamReader(inStream, "utf-8"))
val strber = java.lang.StringBuilder()
do {
line = reader.readLine()
if (line != null) {
strber.append(line)
} else {
break
}
} while (true)
inStream.close()
connection.disconnect()
line = strber.substring(0)
}
} catch (e: MalformedURLException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
} catch (e: Exception) {
e.printStackTrace()
} finally {
if (line == null) {
return ""
}
return line
}
}
}
@@ -0,0 +1,31 @@
package com.btpj.lib_base.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import java.lang.reflect.Type
class GsonUtil {
private var gson: Gson = GsonBuilder().create()
companion object {
private var gsonUtil: GsonUtil? = null
get() {
if (field == null) {
field = GsonUtil()
}
return field
}
fun get(): GsonUtil {
return gsonUtil!!
}
}
fun strToBean(json: String, typeOfT: Type): Any{
return gson.fromJson(json, typeOfT)
}
fun beanToStr(src: Any, typeOfT: Type): String{
return gson.toJson(src, typeOfT)
}
}
@@ -0,0 +1,161 @@
package com.btpj.lib_base.utils
import android.app.Activity
import android.content.Context
import android.util.Log
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
/**
* 键盘相关工具类,借鉴https://github.com/vondear/RxTools
*
* @author nanfeifei
*/
object KeyboardUtil {
/**
* 避免输入法面板遮挡
*
* 在manifest.xml中activity中设置
*
* android:windowSoftInputMode="stateVisible|adjustResize"
*/
/**
* 动态隐藏软键盘
*
* @param activity activity
*/
fun hideSoftInput(activity: Activity) {
val view = activity.window.peekDecorView()
if (view != null) {
val inputManger =
activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManger.hideSoftInputFromWindow(view.windowToken, 0)
}
}
/**
* 点击隐藏软键盘
*
* @param activity
* @param view
*/
fun hideKeyboard(activity: Activity, view: View) {
val imm = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(view.windowToken, 0)
}
/**
* 动态隐藏软键盘
*
* @param context 上下文
* @param edit 输入框
*/
fun hideSoftInput(context: Context, edit: EditText) {
edit.clearFocus()
val inputManger =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManger.hideSoftInputFromWindow(edit.windowToken, 0)
}
/**
* 点击屏幕空白区域隐藏软键盘(方法1)
*
* 在onTouch中处理,未获焦点则隐藏
*
* 参照以下注释代码
*/
fun clickBlankArea2HideSoftInput0() {
Log.i("tips", "U should copy the following code.")
/*
@Override
public boolean onTouchEvent (MotionEvent event){
if (null != this.getCurrentFocus()) {
InputMethodManager mInputMethodManager = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
return mInputMethodManager.hideSoftInputFromWindow(this.getCurrentFocus().getWindowToken(), 0);
}
return super.onTouchEvent(event);
}
*/
}
/**
* 点击屏幕空白区域隐藏软键盘(方法2)
*
* 根据EditText所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘
*
* 需重写dispatchTouchEvent
*
* 参照以下注释代码
*/
fun clickBlankArea2HideSoftInput1() {
Log.i("tips", "U should copy the following code.")
/*
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
View v = getCurrentFocus();
if (isShouldHideKeyboard(v, ev)) {
hideKeyboard(v.getWindowToken());
}
}
return super.dispatchTouchEvent(ev);
}
// 根据EditText所在坐标和用户点击的坐标相对比,来判断是否隐藏键盘
private boolean isShouldHideKeyboard(View v, MotionEvent event) {
if (v != null && (v instanceof EditText)) {
int[] l = {0, 0};
v.getLocationInWindow(l);
int left = l[0],
top = l[1],
bottom = top + v.getHeight(),
right = left + v.getWidth();
return !(event.getX() > left && event.getX() < right
&& event.getY() > top && event.getY() < bottom);
}
return false;
}
// 获取InputMethodManager,隐藏软键盘
private void hideKeyboard(IBinder token) {
if (token != null) {
InputMethodManager im = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
im.hideSoftInputFromWindow(token, InputMethodManager.HIDE_NOT_ALWAYS);
}
}
*/
}
/**
* 动态显示软键盘
*
* @param context 上下文
* @param edit 输入框
*/
fun showSoftInput(context: Context, edit: EditText) {
edit.isFocusable = true
edit.isFocusableInTouchMode = true
edit.requestFocus()
val inputManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.showSoftInput(edit, 0)
}
/**
* 切换键盘显示与否状态
*
* @param context 上下文
* @param edit 输入框
*/
fun toggleSoftInput(context: Context, edit: EditText) {
edit.isFocusable = true
edit.isFocusableInTouchMode = true
edit.requestFocus()
val inputManager =
context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.toggleSoftInput(InputMethodManager.SHOW_FORCED, 0)
}
}
@@ -0,0 +1,139 @@
package com.btpj.lib_base.utils
import android.util.Log
/**
* 日志打印工具类
*
* @author nanfeifei 2018/3/26
*/
object LogUtil {
/** 是否是调试状态,即是否打印日志 */
private var isDebug = true
private var tag = "BTPJ"
/**
* 设置调试状态(以便实现是否打印日志,可以在application的onCreate函数里面初始化)
*
* @param isDebug true: 调试状态即打印所有日志
* false: 上线状态即关闭所有日志的打印
*/
fun isDebug(isDebug: Boolean) {
this.isDebug = isDebug
}
/**
* 设置打印日志的Tag
*
* @param tag 打印日志的Tag
*/
fun setTag(tag: String) {
this.tag = tag
}
/**
* 打印VERBOSE类型的日志
*
* @param tag 打印的Tag
* @param msg 打印的信息
*/
fun v(tag: String, msg: String) {
if (isDebug) {
Log.v(tag, msg)
}
}
/**
* 打印VERBOSE类型的日志
*
* @param msg 打印的信息
*/
fun v(msg: String) {
v(msg)
}
/**
* 打印DEBUG类型的日志
*
* @param tag 打印的Tag
* @param msg 打印的信息
*/
fun d(tag: String, msg: String) {
if (isDebug) {
Log.d(tag, msg)
}
}
/**
* 打印DEBUG类型的日志
*
* @param msg 打印的信息
*/
fun d(msg: String) {
d(tag, msg)
}
/**
* 打印INFO类型的日志
*
* @param tag 打印的Tag
* @param msg 打印的信息
*/
fun i(tag: String, msg: String) {
if (isDebug) {
Log.i(tag, msg)
}
}
/**
* 打印INFO类型的日志
*
* @param msg 打印的信息
*/
fun i(msg: String) {
i(tag, msg)
}
/**
* 打印WARN类型的日志
*
* @param tag 打印的Tag
* @param msg 打印的信息
*/
fun w(tag: String, msg: String) {
if (isDebug) {
Log.w(tag, msg)
}
}
/**
* 打印WARN类型的日志
*
* @param msg 打印的信息
*/
fun w(msg: String) {
w(tag, msg)
}
/**
* 打印ERROR类型的日志
*
* @param tag 打印的Tag
* @param msg 打印的信息
*/
fun e(tag: String, msg: String) {
if (isDebug) {
Log.e(tag, msg)
}
}
/**
* 打印ERROR类型的日志
*
* @param msg 打印的信息
*/
fun e(msg: String) {
e(tag, msg)
}
}
@@ -0,0 +1,107 @@
package com.btpj.lib_base.utils
import android.annotation.SuppressLint
import android.content.Context
import android.net.wifi.WifiInfo
import android.net.wifi.WifiManager
import android.os.Build
import com.btpj.lib_base.BaseApp.Companion.appContext
import java.io.IOException
import java.io.InputStreamReader
import java.io.LineNumberReader
import java.net.NetworkInterface
import java.util.*
object MacUtil {
/**
* Android 6.0 之前(不包括6.0)获取mac地址
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
* @param context * @return
*/
@SuppressLint("MissingPermission")
private fun getMacDefault(context: Context?): String? {
var mac = "0"
if (context == null) {
return mac
}
val wifi: WifiManager = context.getSystemService(Context.WIFI_SERVICE) as WifiManager
var info: WifiInfo? = null
try {
info = wifi.connectionInfo
} catch (e: Exception) {
e.printStackTrace()
}
if (info == null) {
return null
}
mac = info.macAddress
return mac
}
/**
* Android 6.0-Android 7.0 获取mac地址
*/
private fun getMacAddress(): String? {
var macSerial: String? = null
var str = "0"
try {
val pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address")
val ir = InputStreamReader(pp.inputStream)
val input = LineNumberReader(ir)
while (null != str) {
str = input.readLine()
if (str != null) {
macSerial = str.trim { it <= ' ' } //去空格
break
}
}
} catch (ex: IOException) {
// 赋予默认值
ex.printStackTrace()
}
return macSerial
}
/**
* Android 7.0之后获取Mac地址
* 遍历循环所有的网络接口,找到接口是 wlan0
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
* @return
*/
private fun getMacFromHardware(): String? {
try {
val all: List<NetworkInterface> =
Collections.list(NetworkInterface.getNetworkInterfaces())
for (nif in all) {
if (!nif.name.equals("wlan0", true)) continue
val macBytes: ByteArray = nif.hardwareAddress ?: return null
val res1 = StringBuilder()
for (b in macBytes) {
res1.append(String.format("%02X:", b))
}
if (res1.isNotEmpty()) {
res1.deleteCharAt(res1.length - 1)
}
return res1.toString()
}
} catch (ex: Exception) {
ex.printStackTrace()
}
return null
}
fun getMac(): String? {
var mac: String? = "0"
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
mac = getMacDefault(appContext)
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
mac = getMacAddress()
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mac = getMacFromHardware()
}
if (mac == null || mac == "") {
mac = "0"
}
return mac
}
}
@@ -0,0 +1,47 @@
package com.btpj.lib_base.utils
import android.content.Context
import android.content.Context.WIFI_SERVICE
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import androidx.core.content.ContextCompat.getSystemService
import com.btpj.lib_base.BaseApp.Companion.appContext
/**
* 网络工具类
*
* @author nanfeifei 2022/4/14
*/
object NetworkUtil {
/**
* 网络是否可用
*
* @param context Context
* @return 网络是否可用
*/
fun isNetworkAvailable(context: Context): Boolean {
val manager =
context.applicationContext.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
return manager.activeNetworkInfo?.isAvailable == true
}
/**
* 是否连接Wifi
*
* @param context
* @return boolean
*/
fun isWifi(context: Context): Boolean {
val connectivityManager = context
.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetInfo = connectivityManager.activeNetworkInfo
return activeNetInfo?.type == ConnectivityManager.TYPE_WIFI
}
fun getWifiNum(): Int{
val wifiManager: WifiManager = appContext.getSystemService(WIFI_SERVICE) as WifiManager
var scanResults = wifiManager.scanResults
return scanResults.size
}
}
@@ -0,0 +1,110 @@
package com.btpj.lib_base.utils
import android.app.Activity
import android.content.Context
import android.content.res.Resources
import android.util.DisplayMetrics
import android.util.TypedValue
/**
* 屏幕、尺寸相关工具类
*
* @author nanfeifei 2018/8/6
*/
object ScreenUtil {
/**
* dp转px,也可以使用resources.getDimension(R.dimen.xxx).toInt()
*
* @param dpVal 要转换的dp值
*
* @return dp转换为px后的值
*/
fun dp2px(dpVal: Float): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, Resources.getSystem().displayMetrics).toInt()
}
/**
* sp转px
*
* @param spVal 要转换的sp值
*
* @return sp转换为px后的值
*/
fun sp2px(spVal: Float): Int {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, spVal, Resources.getSystem().displayMetrics).toInt()
}
/**
* px转dp
*
* @param pxVal 要转换的px值
*
* @return px转换为dp后的值
*/
fun px2dp(pxVal: Float): Float {
return pxVal / Resources.getSystem().displayMetrics.density
}
/**
* px转sp
*
* @param context Context
* @param pxVal 要转换的px值
*
* @return px转换为sp后的值
*/
fun px2sp(pxVal: Float): Float {
return pxVal / Resources.getSystem().displayMetrics.scaledDensity
}
/**
* 获取屏幕的宽度(px)
*
* @param context Context
*/
fun getScreenWidth(): Int {
return Resources.getSystem().displayMetrics.widthPixels
}
/**
* 获取屏幕的高度(px)
*
* @param context Context
*/
fun getScreenHeight(): Int {
return Resources.getSystem().displayMetrics.heightPixels
}
/**
* 获取屏幕的屏幕密度
*
* @param context Context
*/
fun getScreenDensity(): Float {
return Resources.getSystem().displayMetrics.density
}
/**
* 通过反射,获取包含虚拟键的整体屏幕高度
*
* @return 包含虚拟键的整体屏幕高度
*/
fun getScreenRealHeight(activity: Activity): Int {
var dpi = 0
val display = activity.windowManager.defaultDisplay
val dm = DisplayMetrics()
val c: Class<*>
try {
c = Class.forName("android.view.Display")
val method = c.getMethod("getRealMetrics", DisplayMetrics::class.java)
method.invoke(display, dm)
dpi = dm.heightPixels
} catch (e: Exception) {
e.printStackTrace()
}
return dpi
}
}
@@ -0,0 +1,84 @@
package com.btpj.lib_base.utils
import android.app.Activity
import android.content.Context
import android.os.Build
import android.view.View
import android.view.WindowManager
import androidx.annotation.ColorInt
import com.btpj.lib_base.R
/**
* 屏幕相关工具类
* 1.设置沉浸式任务栏
*
* @author nanfeifei 16/9/21.
*/
object StatusBarUtil {
/**
* 模拟沉浸式状态栏,本质上是通过设置状态栏的颜色,可设置为与toolbar相同达到沉浸式的效果
*
* @param activity 要设置的Activity
*/
fun setImmersionStatus(activity: Activity) {
// 透明状态栏
activity.window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
activity.window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
activity.window.statusBarColor = activity.resources.getColor(R.color.purple_500)
}
/**
* 设置无状态栏,直接干掉顶部的状态栏,但要注意例如一些actionbar会自动顶到最上方需要适配
*
* @param activity 要设置的Activity
*/
fun setNoStatus(activity: Activity) {
// 透明状态栏
activity.window.addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS)
}
/**
* 设置状态栏颜色
*
* @param activity 需要设置的activity
* @param color 状态栏颜色值
*/
fun setStatusBarColor(activity: Activity, @ColorInt color: Int) {
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
}
/**
* 设置Android6.0上状态栏的字体颜色为黑色
*
* @param activity Activity
*/
fun setStatusBarLightMode(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility =
View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR
}
}
/**
* 设置Android6.0上状态栏的字体颜色为黑色
*
* @param activity Activity
*/
fun setStatusBarDarkMode(activity: Activity) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
activity.window.decorView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
}
}
/**
* 获取手机状态栏的高度
*/
fun getStatusBarHeight(context: Context): Int {
val resourceId = context.resources.getIdentifier("status_bar_height", "dimen", "android")
return context.resources.getDimensionPixelSize(resourceId)
}
}
@@ -0,0 +1,69 @@
package com.btpj.lib_base.utils
import android.content.Context
import android.view.Gravity
import android.widget.Toast
import androidx.annotation.StringRes
/**
* Toast封装工具类
* 注:不知咋回事,设置Toast为静态LeakCanary就报内存泄漏,即使设置成context.applicationContext
*
* @author nanfeifei 2018/3/26
*/
object ToastUtil {
/**
* 显示短时间的Toast
*
* @param context Context
* @param msg 显示的消息
*/
fun showShort(context: Context, msg: String) {
if(msg.isNullOrEmpty()){
return
}
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT).show()
}
fun showShort(context: Context, @StringRes resId: Int) {
Toast.makeText(context.applicationContext, resId, Toast.LENGTH_SHORT).show()
}
/**
* 显示长时间的Toast
*
* @param context Context
* @param msg 显示的消息
*/
fun showLong(context: Context, msg: String) {
if(msg.isNullOrEmpty()){
return
}
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_LONG).show()
}
/**
* 居中显示短时间的Toast
*
* @param context Context
* @param msg 显示的消息
*/
fun showShortInCenter(context: Context, msg: String) {
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_SHORT).apply {
setGravity(Gravity.CENTER, 0, 0)
show()
}
}
/**
* 居中显示短时间的Toast
*
* @param context Context
* @param msg 显示的消息
*/
fun showLongInCenter(context: Context, msg: String) {
Toast.makeText(context.applicationContext, msg, Toast.LENGTH_LONG).apply {
setGravity(Gravity.CENTER, 0, 0)
show()
}
}
}
@@ -0,0 +1,421 @@
package com.btpj.lib_base.widgets
import android.annotation.SuppressLint
import android.content.Context
import android.util.AttributeSet
import android.view.Gravity
import android.view.View
import android.view.ViewGroup
import com.btpj.lib_base.R
import kotlin.math.max
import kotlin.math.min
/**
* 流式布局
*
* @author nanfeifei 2018/5/4
*/
class FlowLayout(context: Context, attrs: AttributeSet?) : ViewGroup(context, attrs) {
/** 水平间距 */
private var mChildHorizontalSpacing = 0
/** 垂直间距 */
private var mChildVerticalSpacing = 0
/** 对齐方式,目前支持 [Gravity.CENTER_HORIZONTAL], [Gravity.LEFT] 和 [Gravity.RIGHT] */
private var mGravity: Int = 0
private var mMaxMode = LINES
private var mMaximum = Integer.MAX_VALUE
companion object {
private const val LINES = 0
private const val NUMBER = 1
}
/** 每一行的item数目,下标表示行下标,在onMeasured的时候计算得出,供onLayout去使用 */
private lateinit var mItemNumberInEachLine: IntArray
/** 每一行的item的宽度和(包括item直接的间距),下标表示行下标 */
private lateinit var mWidthSumInEachLine: IntArray
/** onMeasure过程中实际参与measure的子View个数 */
private var measuredChildCount: Int = 0
init {
val typeArray = context.obtainStyledAttributes(attrs, R.styleable.FlowLayout)
mChildHorizontalSpacing =
typeArray.getDimensionPixelSize(R.styleable.FlowLayout_childHorizontalSpacing, 0)
mChildVerticalSpacing =
typeArray.getDimensionPixelSize(R.styleable.FlowLayout_childVerticalSpacing, 0)
mGravity = typeArray.getInteger(R.styleable.FlowLayout_android_gravity, Gravity.START)
val maxLines = typeArray.getInt(R.styleable.FlowLayout_android_maxLines, -1)
if (maxLines >= 0) {
setMaxLines(maxLines)
}
val maxNumber = typeArray.getInt(R.styleable.FlowLayout_maxNumber, -1)
if (maxNumber >= 0) {
setMaxNumber(maxNumber)
}
typeArray.recycle()
}
@SuppressLint("DrawAllocation", "SwitchIntDef")
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val widthSpecMode = MeasureSpec.getMode(widthMeasureSpec)
val widthSpecSize = MeasureSpec.getSize(widthMeasureSpec)
val heightSpecMode = MeasureSpec.getMode(heightMeasureSpec)
val heightSpecSize = MeasureSpec.getSize(heightMeasureSpec)
var maxLineHeight = 0
var resultWidth: Int
val resultHeight: Int
val count = childCount
mItemNumberInEachLine = IntArray(count)
mWidthSumInEachLine = IntArray(count)
var lineIndex = 0
// 若FlowLayout指定了MATCH_PARENT或固定宽度,则需要使子View换行
if (widthSpecMode == MeasureSpec.EXACTLY) {
resultWidth = widthSpecSize
measuredChildCount = 0
// 下一个子View的position
var childPositionX = paddingLeft
var childPositionY = paddingTop
// 子View的Right最大可达到的x坐标
val childMaxRight = widthSpecSize - paddingRight
for (i in 0 until count) {
if (mMaxMode == NUMBER && measuredChildCount >= mMaximum) {
// 超出最多数量,则不再继续
break
} else if (mMaxMode == LINES && lineIndex >= mMaximum) {
// 超出最多行数,则不再继续
break
}
val child = getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
val childLayoutParams = child.layoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
childLayoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
childLayoutParams.height
)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
val childWidth = child.measuredWidth
maxLineHeight = max(maxLineHeight, child.measuredHeight)
// 需要换行
if (childPositionX + childWidth > childMaxRight) {
// 如果换行后超出最大行数,则不再继续
if (mMaxMode == LINES) {
if (lineIndex + 1 >= mMaximum) {
break
}
}
// 后面每次加item都会加上一个space,这样的话每行都会为最后一个item多加一次space,所以在这里减一次
mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing
lineIndex++ // 换行
childPositionX = paddingLeft // 下一行第一个item的x
childPositionY += maxLineHeight + mChildVerticalSpacing // 下一行第一个item的y
}
mItemNumberInEachLine[lineIndex]++
mWidthSumInEachLine[lineIndex] += childWidth + mChildHorizontalSpacing
childPositionX += childWidth + mChildHorizontalSpacing
measuredChildCount++
}
// 如果最后一个item不是刚好在行末(即lineCount最后没有+1,也就是mWidthSumInEachLine[lineCount]非0),则要减去最后一个item的space
if (mWidthSumInEachLine.isNotEmpty() && mWidthSumInEachLine[lineIndex] > 0) {
mWidthSumInEachLine[lineIndex] -= mChildHorizontalSpacing
}
resultHeight = when (heightSpecMode) {
MeasureSpec.UNSPECIFIED -> childPositionY + maxLineHeight + paddingBottom
MeasureSpec.AT_MOST -> min(
childPositionY + maxLineHeight + paddingBottom,
heightSpecSize
)
else -> heightSpecSize
}
} else {
// 不计算换行,直接一行铺开
resultWidth = paddingLeft + paddingRight
measuredChildCount = 0
for (i in 0 until count) {
if (mMaxMode == NUMBER) {
// 超出最多数量,则不再继续
if (measuredChildCount > mMaximum) {
break
}
} else if (mMaxMode == LINES) {
// 超出最大行数,则不再继续
if (1 > mMaximum) {
break
}
}
val child = getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
val childLayoutParams = child.layoutParams
val childWidthMeasureSpec = getChildMeasureSpec(
widthMeasureSpec,
paddingLeft + paddingRight,
childLayoutParams.width
)
val childHeightMeasureSpec = getChildMeasureSpec(
heightMeasureSpec,
paddingTop + paddingBottom,
childLayoutParams.height
)
child.measure(childWidthMeasureSpec, childHeightMeasureSpec)
resultWidth += child.measuredWidth
maxLineHeight = max(maxLineHeight, child.measuredHeight)
measuredChildCount++
}
if (measuredChildCount > 0) {
resultWidth += mChildHorizontalSpacing * (measuredChildCount - 1)
}
resultHeight = maxLineHeight + paddingTop + paddingBottom
if (mItemNumberInEachLine.isNotEmpty()) {
mItemNumberInEachLine[lineIndex] = count
}
if (mWidthSumInEachLine.isNotEmpty()) {
mWidthSumInEachLine[0] = resultWidth
}
}
setMeasuredDimension(resultWidth, resultHeight)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
val width = right - left
// 按照不同gravity使用不同的布局,默认是left
when (mGravity and Gravity.HORIZONTAL_GRAVITY_MASK) {
Gravity.START -> layoutWithGravityLeft(width)
Gravity.END -> layoutWithGravityRight(width)
Gravity.CENTER_HORIZONTAL -> layoutWithGravityCenterHorizontal(width)
else -> layoutWithGravityLeft(width)
}
}
/**
* 将子View靠左布局
*/
private fun layoutWithGravityLeft(parentWidth: Int) {
val childMaxRight = parentWidth - paddingRight
var childPositionX = paddingLeft
var childPositionY = paddingTop
var lineHeight = 0
val childCount = childCount
val childCountToLayout = min(childCount, measuredChildCount)
for (i in 0 until childCountToLayout) {
val child = getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
val childWidth = child.measuredWidth
val childHeight = child.measuredHeight
lineHeight = max(lineHeight, childHeight)
if (childPositionX + childWidth > childMaxRight) {
childPositionX = paddingLeft
childPositionY += lineHeight + mChildVerticalSpacing
lineHeight = 0
}
child.layout(
childPositionX,
childPositionY,
childPositionX + childWidth,
childPositionY + childHeight
)
childPositionX += childWidth + mChildHorizontalSpacing
}
// 如果布局的子View少于childCount,则表示有一些子View不需要布局
if (measuredChildCount < childCount) {
for (i in measuredChildCount until childCount) {
val child = getChildAt(i)
if (child.visibility == View.GONE) {
continue
}
child.layout(0, 0, 0, 0)
}
}
}
/**
* 将子View居中布局
*/
private fun layoutWithGravityCenterHorizontal(parentWidth: Int) {
var nextChildIndex = 0
var nextChildPositionX: Int
var nextChildPositionY = paddingTop
var lineHeight = 0
// 遍历每一行
for (i in mItemNumberInEachLine.indices) {
// 如果这一行已经没item了,则退出循环
if (mItemNumberInEachLine[i] == 0) {
break
}
if (nextChildIndex > measuredChildCount - 1) {
break
}
// 遍历该行内的元素,布局每个元素
nextChildPositionX =
(parentWidth - paddingLeft - paddingRight - mWidthSumInEachLine[i]) / 2 + paddingLeft // 子 View 的最小 x 值
for (j in nextChildIndex until nextChildIndex + mItemNumberInEachLine[i]) {
val childView = getChildAt(j)
if (childView.visibility == View.GONE) {
continue
}
val childWidth = childView.measuredWidth
val childHeight = childView.measuredHeight
childView.layout(
nextChildPositionX,
nextChildPositionY,
nextChildPositionX + childWidth,
nextChildPositionY + childHeight
)
lineHeight = max(lineHeight, childHeight)
nextChildPositionX += childWidth + mChildHorizontalSpacing
}
// 一行结束了,整理一下,准备下一行
nextChildPositionY += lineHeight + mChildVerticalSpacing
nextChildIndex += mItemNumberInEachLine[i]
lineHeight = 0
}
val childCount = childCount
if (measuredChildCount < childCount) {
for (i in measuredChildCount until childCount) {
val childView = getChildAt(i)
if (childView.visibility == View.GONE) {
continue
}
childView.layout(0, 0, 0, 0)
}
}
}
/**
* 将子View靠右布局
*/
private fun layoutWithGravityRight(parentWidth: Int) {
var nextChildIndex = 0
var nextChildPositionX: Int
var nextChildPositionY = paddingTop
var lineHeight = 0
// 遍历每一行
for (i in mItemNumberInEachLine.indices) {
// 如果这一行已经没item了,则退出循环
if (mItemNumberInEachLine[i] == 0) {
break
}
if (nextChildIndex > measuredChildCount - 1) {
break
}
// 遍历该行内的元素,布局每个元素
nextChildPositionX =
parentWidth - paddingRight - mWidthSumInEachLine[i] // 初始值为子 View 的最小 x 值
for (j in nextChildIndex until nextChildIndex + mItemNumberInEachLine[i]) {
val childView = getChildAt(j)
if (childView.visibility == View.GONE) {
continue
}
val childWidth = childView.measuredWidth
val childHeight = childView.measuredHeight
childView.layout(
nextChildPositionX,
nextChildPositionY,
nextChildPositionX + childWidth,
nextChildPositionY + childHeight
)
lineHeight = max(lineHeight, childHeight)
nextChildPositionX += childWidth + mChildHorizontalSpacing
}
// 一行结束了,整理一下,准备下一行
nextChildPositionY += lineHeight + mChildVerticalSpacing
nextChildIndex += mItemNumberInEachLine[i]
lineHeight = 0
}
val childCount = childCount
if (measuredChildCount < childCount) {
for (i in measuredChildCount until childCount) {
val childView = getChildAt(i)
if (childView.visibility == View.GONE) {
continue
}
childView.layout(0, 0, 0, 0)
}
}
}
/**
* 设置子 View 的对齐方式,目前支持 [Gravity.CENTER_HORIZONTAL], [Gravity.LEFT] 和 [Gravity.RIGHT]
*/
fun setGravity(gravity: Int) {
if (mGravity != gravity) {
mGravity = gravity
requestLayout()
}
}
fun getGravity(): Int {
return mGravity
}
/**
* 获取最多可显示的行数
*
* @return 没有限制时返回-1
*/
fun getMaxLines(): Int {
return if (mMaxMode == LINES) mMaximum else -1
}
/**
* 设置最多可显示的行数
*
* @param maxLines 最多可显示的行数
*/
fun setMaxLines(maxLines: Int) {
mMaximum = maxLines
mMaxMode = LINES
requestLayout()
}
/**
* 获取最多可显示的子View个数
*/
fun getMaxNumber(): Int {
return if (mMaxMode == NUMBER) mMaximum else -1
}
/**
* 设置最多可显示的子View个数
*
* @param maxNumber 最多可显示的子View个数
*/
fun setMaxNumber(maxNumber: Int) {
mMaximum = maxNumber
mMaxMode = NUMBER
requestLayout()
}
}
@@ -0,0 +1,259 @@
package com.btpj.lib_base.widgets
import android.app.Activity
import android.content.Context
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.databinding.DataBindingUtil
import com.btpj.lib_base.R
import com.btpj.lib_base.databinding.LayoutTitleBinding
import com.btpj.lib_base.utils.ScreenUtil
/**
* 封装的Title标题栏
* 比Toolbar更好用,当然没有Toolbar那么强大,不过通常的功能均能更好的满足,不满足的再用Toolbar就行了
*
* @author nanfeifei 16/9/19.
*/
class TitleLayout(context: Context, attrs: AttributeSet) : ConstraintLayout(context, attrs) {
private var mBinding: LayoutTitleBinding
init {
// 自定义TitleLayout的相关属性
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.TitleLayout)
val titleBackgroundColor = typedArray.getColor(
R.styleable.TitleLayout_titleBackgroundColor,
ContextCompat.getColor(context, R.color.purple_500)
)
val backIconRes =
typedArray.getResourceId(R.styleable.TitleLayout_backIconRes, R.drawable.ic_back)
val isShowBack = typedArray.getBoolean(R.styleable.TitleLayout_isShowBack, true)
val titleTextColor = typedArray.getColor(
R.styleable.TitleLayout_titleTextColor,
ContextCompat.getColor(context, R.color._ffffff)
)
val titleTextSize = typedArray.getDimensionPixelSize(
R.styleable.TitleLayout_titleTextSize,
ScreenUtil.sp2px(20f)
)
val titleText = typedArray.getString(R.styleable.TitleLayout_titleText)
typedArray.recycle()
// 自定义TitleLayout的布局
mBinding = DataBindingUtil.inflate(
LayoutInflater.from(context),
R.layout.layout_title,
this,
true
)
// 设置TitleLayout的背景色
mBinding.clTitleBar.setBackgroundColor(titleBackgroundColor)
// TitleBar的返回键
mBinding.ivBack.apply {
visibility = if (isShowBack) View.VISIBLE else View.GONE
setImageResource(backIconRes)
setOnClickListener { (context as Activity).onBackPressed() }
}
// TitleBar的标题文本
mBinding.tvTitleText.apply {
setTextColor(titleTextColor)
setTextSize(TypedValue.COMPLEX_UNIT_PX, titleTextSize.toFloat())
text = titleText
isSelected = true
}
}
/**
* 设置返回键图标
*
* @param resId 返回键图标Id
*/
fun setBackIcon(resId: Int): TitleLayout {
mBinding.ivBack.setImageResource(resId)
return this
}
/**
* 设置Title背景色
*
* @param titleBackgroundColor Title背景色
*/
fun setTitleBackgroundColor(titleBackgroundColor: Int): TitleLayout {
mBinding.clTitleBar.setBackgroundColor(titleBackgroundColor)
return this
}
/**
* 设置Title左侧的返回键是否显示
*
* @param isVisible Title左侧的返回键是否显示
*/
fun setBackVisible(isVisible: Boolean): TitleLayout {
mBinding.ivBack.visibility = if (isVisible) View.VISIBLE else View.GONE
return this
}
/**
* 设置Title中间的标题文本名
*
* @param titleText Title中间的标题文本名
*/
fun setTitleText(titleText: String): TitleLayout {
mBinding.tvTitleText.text = titleText
return this
}
/**
* 设置Title中间的标题文本颜色
*
* @param titleTextColor Title中间的标题文本颜色
*/
fun setTitleTextColor(titleTextColor: Int): TitleLayout {
mBinding.tvTitleText.setTextColor(titleTextColor)
return this
}
/**
* 设置Title中间的标题文本大小
*
* @param titleTextSize Title中间的标题文本大小
*/
fun setTitleTextSize(titleTextSize: Int): TitleLayout {
mBinding.tvTitleText.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleTextSize.toFloat())
return this
}
/**
* 设置Title右测的TextView编辑菜单
*
* @param text Title右测的TextView编辑菜单文本
* @param onClickListener 菜单点击回调
*/
fun setRightView(text: String, onClickListener: OnClickListener): TitleLayout {
mBinding.apply {
ivMenu.visibility = View.GONE
tvMenu.apply {
visibility = View.VISIBLE
this.text = text
setOnClickListener(onClickListener)
}
}
return this
}
/**
* 设置Title右测的TextView编辑菜单
*
* @param rightViewBackground Title右测的TextView背景色
*/
fun setRightViewBackground(rightViewBackground: Int): TitleLayout {
mBinding.apply {
ivMenu.visibility = View.GONE
tvMenu.apply {
visibility = View.VISIBLE
setBackgroundColor(rightViewBackground)
}
}
return this
}
/**
* 设置Title右测的TextView编辑菜单
*
* @param text Title右测的TextView编辑菜单文本
* @param textColor Title右测的TextView编辑菜单文本颜色
* @param onClickListener 菜单点击回调
*/
fun setRightView(text: String, textColor: Int, onClickListener: OnClickListener): TitleLayout {
mBinding.apply {
ivMenu.visibility = View.GONE
.apply {
tvMenu.apply {
visibility = View.VISIBLE
this.text = text
setTextColor(textColor)
setOnClickListener(onClickListener)
}
}
}
return this
}
/**
* 设置Title右测的TextView编辑菜单
*
* @param text Title右测的TextView编辑菜单文本
*/
fun setRightView(text: String): TitleLayout {
mBinding.apply {
ivMenu.visibility = View.GONE
.apply {
tvMenu.apply {
visibility = View.VISIBLE
this.text = text
}
}
}
return this
}
/**
* 设置Title右测的ImageView编辑菜单
*
* @param imageRes Title右测的ImageView编辑菜单ImageViewResource
* @param onClickListener 菜单点击回调
*/
fun setRightView(imageRes: Int, onClickListener: OnClickListener): TitleLayout {
mBinding.apply {
tvMenu.visibility = View.GONE
ivMenu.apply {
visibility = View.VISIBLE
setImageResource(imageRes)
setOnClickListener(onClickListener)
}
}
return this
}
/**
* 设置Title右测的ImageView编辑菜单
*
* @param imageRes Title右测的ImageView编辑菜单ImageViewResource
*/
fun setRightView(imageRes: Int): TitleLayout {
mBinding.apply {
tvMenu.visibility = View.GONE
ivMenu.apply {
visibility = View.VISIBLE
setImageResource(imageRes)
}
}
return this
}
/**
* 设置Title右侧的菜单上的提示红点是否显示
*
* @param isVisible 红点是否显示
*/
fun setRedViewVisible(isVisible: Boolean): TitleLayout {
mBinding.viewRed.visibility = if (isVisible) View.VISIBLE else View.INVISIBLE
return this
}
/**
* 设置左键的点击事件
*/
fun setLeftOnclick(l: OnClickListener): TitleLayout {
mBinding.ivBack.setOnClickListener(l)
return this
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<solid android:color="@color/_ff4a57" />
<size
android:width="2dp"
android:height="2dp" />
</shape>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#ffffff"
android:pathData="M477.87,157.87c17.07,-17.07 17.07,-42.67 0,-59.73s-42.67,-17.07 -59.73,0L40.53,465.07C14.93,490.67 14.93,531.2 40.53,554.67c0,0 125.87,123.73 379.73,371.2 17.07,17.07 44.8,17.07 59.73,0 17.07,-17.07 17.07,-44.8 0,-59.73L115.2,512l362.67,-354.13z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="@color/_eeeeee"
android:pathData="M853.34,106.66a128,128 0,0 1,128 128v554.69a128,128 0,0 1,-128 128L170.66,917.34a128,128 0,0 1,-128 -128L42.66,234.66a128,128 0,0 1,128 -128h682.69zM698.88,516.35l-2.37,2.98 -111.04,149.89a117.34,117.34 0,0 1,-153.73 31.33l-4.48,-2.78 -83.62,-54.37a53.34,53.34 0,0 0,-66.88 7.1l-2.56,2.75 -152.8,176.96c10.98,13.22 27.2,21.95 45.5,23.04l3.74,0.1h682.69a64,64 0,0 0,63.87 -60.26l0.13,-3.74v-155.97l-143.26,-122.78a53.34,53.34 0,0 0,-75.2 5.76zM853.34,170.66L170.66,170.66a64,64 0,0 0,-63.87 60.26l-0.13,3.74v514.66l119.14,-137.89a117.34,117.34 0,0 1,148.22 -24.48l4.51,2.82 83.62,54.34c22.59,14.72 52.42,10.14 69.66,-10.11l2.24,-2.88 111.04,-149.86a117.34,117.34 0,0 1,164.13 -24.45l3.3,2.56 3.2,2.62 101.6,87.07L917.31,234.66a64,64 0,0 0,-60.22 -63.87l-3.74,-0.13zM277.34,320a64,64 0,1 1,0 128,64 64,0 0,1 0,-128z" />
</vector>
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:width="64dp"
android:height="64dp"
android:viewportWidth="1024"
android:viewportHeight="1024">
<path
android:fillColor="#FF999999"
android:pathData="M968.35,519.52v422.96c0,15.58 -11.13,26.71 -26.71,26.71L80.14,969.19c-15.58,0 -28.94,-11.13 -28.94,-26.71L51.2,519.52h291.62c6.68,35.62 22.26,71.24 51.2,97.95C425.18,648.64 467.48,664.22 512,664.22s84.59,-17.81 117.98,-46.75c26.71,-26.71 44.52,-60.1 51.2,-97.95h287.17zM658.92,470.55c-13.36,0 -22.26,11.13 -22.26,24.49 0,66.78 -55.65,122.43 -122.43,122.43S391.79,561.82 391.79,495.04c0,-13.36 -11.13,-24.49 -22.26,-24.49L93.5,470.55l146.92,-133.57L785.81,336.98L930.5,470.55L658.92,470.55zM1010.64,472.78L814.75,292.46c-4.45,-6.68 -13.36,-8.9 -22.26,-8.9L231.51,283.56c-8.9,0 -17.81,4.45 -22.26,8.9L11.13,472.78C4.45,479.45 0,490.58 0,499.49v440.77c0,44.52 35.62,80.14 80.14,80.14h861.49c44.52,0 80.14,-35.62 80.14,-80.14L1021.77,499.49c2.23,-8.9 -4.45,-20.03 -11.13,-26.71zM589.91,768.85c2.23,11.13 11.13,15.58 31.17,13.36 33.39,-6.68 48.97,-2.23 57.88,-4.45 8.9,-4.45 11.13,-17.81 -4.45,-22.26 -15.58,-6.68 -33.39,2.22 -35.62,-2.23 -2.23,-2.23 4.45,-8.9 17.81,-11.13 15.58,-4.45 31.17,-4.45 28.94,-15.58 -4.45,-15.58 -26.71,-17.81 -44.52,-13.36 -22.26,2.23 -55.65,28.94 -51.2,55.65zM380.66,710.97c-15.58,-4.45 -37.84,-2.23 -44.52,13.36 -2.23,8.9 13.36,11.13 28.94,15.58 11.13,2.23 17.81,8.9 17.81,11.13 -2.23,4.45 -20.03,-4.45 -35.62,2.23 -15.58,6.68 -13.36,20.03 -4.45,22.26 8.9,4.45 24.49,0 57.88,4.45 17.81,2.23 28.94,-2.23 31.17,-13.36 4.45,-24.49 -28.94,-51.2 -51.2,-55.65zM512,831.18c-31.17,0 -55.65,24.49 -55.65,55.65 0,6.68 4.45,11.13 11.13,11.13s11.13,-4.45 11.13,-11.13c0,-17.81 15.58,-33.39 33.39,-33.39 20.03,0 33.39,15.58 33.39,33.39 0,6.68 4.45,11.13 11.13,11.13s11.13,-4.45 11.13,-11.13c0,-31.17 -24.49,-55.65 -55.65,-55.65zM456.35,889.06v-2.23,2.23z"
tools:ignore="VectorPath" />
<path
android:fillColor="#FF999999"
android:pathData="M968.35,519.52v422.96c0,15.58 -11.13,26.71 -26.71,26.71L80.14,969.19c-15.58,0 -28.94,-11.13 -28.94,-26.71L51.2,519.52h291.62c6.68,35.62 22.26,71.24 51.2,97.95C425.18,648.64 467.48,664.22 512,664.22s84.59,-17.81 117.98,-46.75c26.71,-26.71 44.52,-60.1 51.2,-97.95h287.17zM658.92,470.55c-13.36,0 -22.26,11.13 -22.26,24.49 0,66.78 -55.65,122.43 -122.43,122.43S391.79,561.82 391.79,495.04c0,-13.36 -11.13,-24.49 -22.26,-24.49L93.5,470.55l146.92,-133.57L785.81,336.98L930.5,470.55L658.92,470.55zM1010.64,472.78L814.75,292.46c-4.45,-6.68 -13.36,-8.9 -22.26,-8.9L231.51,283.56c-8.9,0 -17.81,4.45 -22.26,8.9L11.13,472.78C4.45,479.45 0,490.58 0,499.49v440.77c0,44.52 35.62,80.14 80.14,80.14h861.49c44.52,0 80.14,-35.62 80.14,-80.14L1021.77,499.49c2.23,-8.9 -4.45,-20.03 -11.13,-26.71zM589.91,768.85c2.23,11.13 11.13,15.58 31.17,13.36 33.39,-6.68 48.97,-2.23 57.88,-4.45 8.9,-4.45 11.13,-17.81 -4.45,-22.26 -15.58,-6.68 -33.39,2.22 -35.62,-2.23 -2.23,-2.23 4.45,-8.9 17.81,-11.13 15.58,-4.45 31.17,-4.45 28.94,-15.58 -4.45,-15.58 -26.71,-17.81 -44.52,-13.36 -22.26,2.23 -55.65,28.94 -51.2,55.65zM380.66,710.97c-15.58,-4.45 -37.84,-2.23 -44.52,13.36 -2.23,8.9 13.36,11.13 28.94,15.58 11.13,2.23 17.81,8.9 17.81,11.13 -2.23,4.45 -20.03,-4.45 -35.62,2.23 -15.58,6.68 -13.36,20.03 -4.45,22.26 8.9,4.45 24.49,0 57.88,4.45 17.81,2.23 28.94,-2.23 31.17,-13.36 4.45,-24.49 -28.94,-51.2 -51.2,-55.65zM512,831.18c-31.17,0 -55.65,24.49 -55.65,55.65 0,6.68 4.45,11.13 11.13,11.13s11.13,-4.45 11.13,-11.13c0,-17.81 15.58,-33.39 33.39,-33.39 20.03,0 33.39,15.58 33.39,33.39 0,6.68 4.45,11.13 11.13,11.13s11.13,-4.45 11.13,-11.13c0,-31.17 -24.49,-55.65 -55.65,-55.65zM456.35,889.06v-2.23,2.23zM727.93,193.67c-4.45,-4.45 -6.68,-11.13 -6.68,-15.58 0,-6.68 2.23,-13.36 6.68,-17.81l84.59,-86.82c8.9,-8.9 24.49,-8.9 33.39,0s8.9,24.49 0,33.39l-84.59,86.82c-4.45,4.45 -8.9,6.68 -15.58,6.68 -6.68,0 -13.36,-2.23 -17.81,-6.68m-231.51,-31.17c-4.45,-4.45 -6.68,-11.13 -6.68,-17.81l-2.23,-120.21C487.51,11.13 498.64,0 509.77,0c13.36,0 24.49,8.9 24.49,22.26l2.23,120.21c0,8.9 -4.45,17.81 -13.36,22.26 -8.9,4.45 -17.81,2.23 -26.71,-2.23m-244.87,35.62l-89.04,-82.36c-8.9,-8.9 -8.9,-24.49 0,-33.39 8.9,-8.9 24.49,-8.9 33.39,0l89.04,82.36c8.9,8.9 8.9,24.49 0,33.39 -8.9,8.9 -24.49,8.9 -33.39,0"
tools:ignore="VectorPath" />
</vector>
@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView 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="wrap_content"
app:cardBackgroundColor="@color/_ffffff"
app:cardCornerRadius="6dp">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="100dp"
android:layout_gravity="center"
android:gravity="center"
android:orientation="vertical">
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_marginLeft="24dp"
android:layout_marginRight="24dp"
android:indeterminateTint="@color/theme_color"
android:indeterminateTintMode="src_atop" />
<TextView
android:id="@+id/tv_loadingMsg"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:gravity="center"
android:text="@string/request_web"
android:textColor="#333"
android:textSize="16sp" />
</LinearLayout>
</androidx.cardview.widget.CardView>
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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"
android:paddingBottom="100dp">
<TextView
android:id="@+id/tv_empty"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="36dp"
android:gravity="center"
android:text="@string/list_is_empty"
android:textColor="@color/_666666"
android:textSize="14sp"
app:drawableTopCompat="@drawable/image_default_empty" />
</FrameLayout>
@@ -0,0 +1,94 @@
<?xml version="1.0" encoding="utf-8"?><!--通用的TitleBar布局 -->
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools">
<data>
<!-- 这个布局加入dataBinding纯粹是为了让BR生成viewModel的Variable
以便BaseVMBActivity中调用mBinding.setVariable(BR.viewModel, mViewModel)
而不用实际使用的Activity(继承自BaseVMBActivity)每次都调用一遍-->
<variable
name="viewModel"
type="com.btpj.lib_base.base.BaseViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_titleBar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
android:background="@color/purple_500">
<ImageView
android:id="@+id/iv_back"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@null"
android:paddingStart="10dp"
android:paddingEnd="30dp"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_back" />
<ImageView
android:id="@+id/iv_menu"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@null"
android:paddingStart="20dp"
android:paddingEnd="15dp"
android:src="@android:drawable/ic_menu_add"
android:textColor="@color/_ffffff"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/iv_back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_back" />
<TextView
android:id="@+id/tv_menu"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="?android:attr/selectableItemBackground"
android:gravity="center"
android:maxEms="7"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:singleLine="true"
android:textColor="@color/_ffffff"
android:textSize="14sp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/iv_back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_back"
tools:text="Edit" />
<TextView
android:id="@+id/tv_titleText"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:ellipsize="marquee"
android:gravity="center"
android:marqueeRepeatLimit="marquee_forever"
android:paddingStart="40dp"
android:paddingEnd="28dp"
android:singleLine="true"
android:textColor="@color/_ffffff"
android:textSize="20sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
tools:text="Title" />
<View
android:id="@+id/view_red"
android:layout_width="10dp"
android:layout_height="10dp"
android:layout_marginEnd="14dp"
android:layout_marginBottom="14dp"
android:background="@drawable/bg_circle_red"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="@+id/iv_back"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/iv_back" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
+27
View File
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!--自定义的标题栏相关属性-->
<declare-styleable name="TitleLayout">
<!--标题栏背景颜色(默认蓝色)-->
<attr name="titleBackgroundColor" format="color" />
<!--是否显示标题栏左侧的返回键(默认显示)-->
<attr name="isShowBack" format="boolean" />
<!--标题栏左侧的返回键的图标-->
<attr name="backIconRes" format="reference" />
<!--标题栏标题文本颜色(默认白色)-->
<attr name="titleTextColor" format="color" />
<!--标题栏标题文本大小-->
<attr name="titleTextSize" format="dimension" />
<!--标题栏标题文本-->
<attr name="titleText" format="string" />
</declare-styleable>
<!--FlowLayout流式布局-->
<declare-styleable name="FlowLayout">
<attr name="android:gravity" />
<attr name="android:maxLines" format="integer" />
<attr name="childHorizontalSpacing" format="dimension" />
<attr name="childVerticalSpacing" format="dimension" />
<attr name="maxNumber" format="integer" />
</declare-styleable>
</resources>
+36
View File
@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="colorPrimary">@color/white</color>
<color name="colorPrimaryDark">@color/white</color>
<color name="colorAccent">#FF6121</color>
<color name="theme_color">#54eccb</color>
<color name="theme_bg">#EFEFEF</color>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="_333333">#333333</color>
<color name="_3700B3">#3700B3</color>
<color name="_4cd2f5">#4CD2F5</color>
<color name="_6200EE">#6200EE</color>
<color name="_666666">#666666</color>
<color name="_669900">#669900</color>
<color name="_797979">#797979</color>
<color name="_84749C">#84749C</color>
<color name="_999999">#999999</color>
<color name="_a0a0a0">#a0a0a0</color>
<color name="_cccccc">#cccccc</color>
<color name="_d8d8d8">#d8d8d8</color>
<color name="_e91e63">#e91e63</color>
<color name="_eeeeee">#eeeeee</color>
<color name="_f0f0f0">#f0f0f0</color>
<color name="_f5f5f5">#f5f5f5</color>
<color name="_ff4a57">#ff4a57</color>
<color name="_ffffff">#ffffff</color>
</resources>
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<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="title_bar_height">48dp</dimen>
</resources>
+6
View File
@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="toolbar_lay" type="id"/>
<item name="tv_right" type="id"/>
<item name="iv_right" type="id"/>
</resources>
+7
View File
@@ -0,0 +1,7 @@
<resources>
<string name="list_is_empty">暂无数据</string>
<string name="response_error">网络连接异常</string>
<string name="network_error">网络连接异常</string>
<string name="request_time_out">网络请求超时</string>
<string name="request_web">加载中…</string>
</resources>
+3
View File
@@ -0,0 +1,3 @@
<resources>
</resources>
@@ -0,0 +1,20 @@
package com.btpj.lib_base
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
// var str = CustomerEncryptUtil.decode("A1BDBDB9F3E6E6F8F8FCE7FBFAF1E7FDFFE7FCF1F3FBF9F9F9FB")
// print(str)
// var str2 = CustomerEncryptUtil.decode("488EE17C1EA2A84A769746DCEDF0DEB31193DC46B7FE825493652A380FDB1A89DD8A382A4D3BE001E0751F89BA444B5916D023C0F7D4A944DC4FD8A95809B758")
// print(str2)
// var list = mutableListOf<Int>(10, 20, 30)
// print("list contains="+list.contains(50)+" ")
}
}