feat:同步迁移utils、core模块

feat:集成网络调试工具
feat:初步搭建ExoPlayer-待完善
This commit is contained in:
Max
2023-11-02 16:25:48 +08:00
parent c1a1867511
commit 56fdb706b4
35 changed files with 2546 additions and 0 deletions

View File

@@ -29,6 +29,7 @@ import androidx.multidex.MultiDex;
import com.bumptech.glide.request.target.ViewTarget;
import com.bytedance.hume.readapk.HumeSDK;
import com.coorchice.library.utils.LogUtils;
import com.facebook.stetho.Stetho;
import com.heytap.msp.push.HeytapPushManager;
import com.hjq.toast.ToastUtils;
import com.huawei.hms.support.common.ActivityMgr;
@@ -275,6 +276,7 @@ public class XChatApplication extends BaseApp {
ViewTarget.setTagId(R.id.tag_glide);
init(channel);
initStetho(context);
if (!TextUtils.isEmpty(channel)) {
CrashReport.setAppChannel(context, channel);
@@ -663,4 +665,18 @@ public class XChatApplication extends BaseApp {
}
GlobalHandleManager.get().unInit();
}
/**
* 初始化Stetho网络调试
*/
private static void initStetho(Context context) {
if (Env.isDebug()) {
Stetho.initialize(
Stetho.newInitializerBuilder(context)
.enableDumpapp(Stetho.defaultDumperPluginsProvider(context))
.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context))
.build()
);
}
}
}

View File

@@ -32,6 +32,8 @@ android {
'src/module_luban/java',
'src/module_easyphoto/java',
'src/module_common/java',
'src/module_core/java',
'src/module_utils/java',
]
@@ -40,6 +42,8 @@ android {
'src/module_easypermission/res',
'src/module_easyphoto/res',
'src/module_common/res',
'src/module_core/res',
'src/module_utils/res',
]
@@ -132,6 +136,11 @@ dependencies {
api 'com.github.zhpanvip:BannerViewPager:3.5.6'
api 'com.google.android.exoplayer:exoplayer:2.18.1'
// 网络请求chrome数据调试
api 'com.facebook.stetho:stetho:1.5.1'
api 'com.facebook.stetho:stetho-okhttp3:1.5.1'
}
repositories {
mavenCentral()

View File

@@ -2,6 +2,7 @@ package com.nnbc123.library.net.rxnet.manager;
import android.content.Context;
import com.facebook.stetho.okhttp3.StethoInterceptor;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.nnbc123.library.BuildConfig;
@@ -64,6 +65,7 @@ public final class RxNetManager {
});
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
mBuilder.addInterceptor(loggingInterceptor);
mBuilder.addNetworkInterceptor(new StethoInterceptor());
}
for (Interceptor interceptor : interceptors) {

View File

@@ -26,6 +26,7 @@ public abstract class BaseApp extends Application{
public static void init(Application application) {
gContext = application;
com.chuhai.utils.AppUtils.init(application);
}
/**

View File

@@ -140,6 +140,25 @@ object PhotoProviderNew {
}
}
@JvmStatic
@JvmOverloads
fun photoVideoProvider(fragment: Fragment, maxSelect: Int = 1, canChooseGif: Boolean = false, resultCode: Int, isClearCache: Boolean = true, minFileSize: Long = 0L) {
cancelJop()
mPhotoJob = MainScope().launch {
if (isClearCache && isClearByTime()) {
withContext(Dispatchers.IO) { clearCache() }
}
EasyPhotos.createAlbum(fragment, false, false, GlideEngine())//参数说明上下文是否显示相机按钮是否使用宽高数据false时宽高数据为0扫描速度更快[配置Glide为图片加载引擎](https://github.com/HuanTanSheng/EasyPhotos/wiki/12-%E9%85%8D%E7%BD%AEImageEngine%EF%BC%8C%E6%94%AF%E6%8C%81%E6%89%80%E6%9C%89%E5%9B%BE%E7%89%87%E5%8A%A0%E8%BD%BD%E5%BA%93)
.setCount(maxSelect)//参数说明最大可选数默认1
.setGif(canChooseGif)
.setVideo(true)
.setMinFileSize(minFileSize)
.setPuzzleMenu(false)
.setCleanMenu(false)
.start(resultCode)
}
}
@JvmStatic
fun getResultUriList(data: Intent?): List<Uri>? {
val list: List<Photo>? = data?.getParcelableArrayListExtra(EasyPhotos.RESULT_PHOTOS)

View File

@@ -0,0 +1,62 @@
import androidx.lifecycle.*
/**
* Created by Max on 2023/10/24 15:11
* Desc:跟随目标生命周期销毁
**/
interface LifecycleCleared : LifecycleEventObserver {
/**
* 是否启用
*/
fun isEnabledLifecycleClear(): Boolean {
return true
}
/**
* 获取监听的目标生命周期
*/
abstract fun getTargetLifecycle(): Lifecycle?
/**
* 目标生命周期已销毁:执行清除资源操作
*/
abstract fun onTargetCleared()
/**
* 获取要执行清理的事件
*/
fun getClearEvent(): Lifecycle.Event? {
return Lifecycle.Event.ON_DESTROY
}
/**
* 绑定生命周期
*/
fun bindLifecycleClear() {
if (!isEnabledLifecycleClear()) {
return
}
getTargetLifecycle()?.addObserver(this)
}
/**
* 取消绑定生命周期(如果实现类是自己主动销毁的,需要主动调下本方法)
*/
fun unBindLifecycleClear() {
if (!isEnabledLifecycleClear()) {
return
}
getTargetLifecycle()?.removeObserver(this)
}
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
if (!isEnabledLifecycleClear()) {
return
}
if (getClearEvent() == event) {
unBindLifecycleClear()
onTargetCleared()
}
}
}

View File

@@ -0,0 +1,54 @@
package com.chuhai.core.component
import LifecycleCleared
import android.content.Context
import android.content.DialogInterface
import androidx.lifecycle.Lifecycle
import com.chuhai.utils.ktx.asLifecycle
import com.google.android.material.bottomsheet.BottomSheetDialog
/**
* Created by Max on 2023/10/24 15:11
* Desc:BottomSheetDialog
*/
open class SuperBottomSheetDialog : BottomSheetDialog, LifecycleCleared {
constructor(context: Context) : super(context) {
init()
}
constructor(context: Context, theme: Int) : super(context, theme) {
init()
}
constructor(
context: Context,
cancelable: Boolean,
cancelListener: DialogInterface.OnCancelListener?
) : super(context, cancelable, cancelListener) {
init()
}
protected open fun init() {
}
override fun getTargetLifecycle(): Lifecycle? {
return context.asLifecycle()
}
override fun onTargetCleared() {
dismiss()
}
override fun show() {
super.show()
bindLifecycleClear()
}
override fun dismiss() {
super.dismiss()
unBindLifecycleClear()
}
}

View File

@@ -0,0 +1,40 @@
package com.chuhai.core.exceptions
/**
* Created by Max on 2023/11/2 14:45
*/
open class SuperException : Exception {
/**
* 错误码
*/
var code: Int = 0
constructor(code: Int = 0) : super() {
this.code = code
}
constructor(message: String?, code: Int = 0) : super(message) {
this.code = code
}
constructor(message: String?, cause: Throwable?, code: Int = 0) : super(message, cause) {
this.code = code
}
constructor(cause: Throwable?, code: Int = 0) : super(cause) {
this.code = code
}
override fun toString(): String {
return "[${super.toString()}]\n[code=$code,message=${message}]\n[cause=${cause}]"
}
/**
* 可以toast
*/
open fun canToast(): Boolean {
return true
}
}

View File

@@ -0,0 +1,67 @@
package com.chuhai.core.player
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.chuhai.utils.ICleared
/**
* Created by Max on 2023/11/2 14:45
* Desc:播放器(基本接口)
**/
interface IPlayer : ICleared {
/**
* 绑定页面生命周期
*/
fun bindingLifeCycle(lifecycleOwner: LifecycleOwner)
/**
* 加载视频源加载后根据playWhenReady觉得是否播放
*/
fun prepare(item: PlayerMediaItem)
/**
* 停止播放(恢复到初始状态)
*/
fun stop()
/**
* 设置加载成功后是否播放(控制播放与暂停)
* @param playWhenReady
*/
fun setPlayWhenReady(playWhenReady: Boolean)
/**
* 获取是否加载成功后是否播放
*/
fun getPlayWhenReady(): Boolean
/**
* 重新播放
*/
fun restart()
/**
* 获取视图
*/
fun getView(): View
/**
* 当前源
*/
fun getCurrentMediaItem(): PlayerMediaItem?
/**
* 获取当前状态
*/
fun getPlaybackState(): PlaybackState
/**
* 设置播放器事件监听器
*/
fun setListener(listener: PlayerListener?)
override fun onCleared() {
super.onCleared()
}
}

View File

@@ -0,0 +1,61 @@
package com.chuhai.core.player
import com.google.android.exoplayer2.Player
/**
* Created by Max on 2023/11/2 14:45
* Desc:播放状态
*/
enum class PlaybackState {
/*
* 此枚举定义通用的播放器状态视频播放器svga播放器等
*/
/**
* 初始状态、播放器停止、播放失败时的状态。
*/
IDLE,
/**
* 加载状态
*/
BUFFERING,
/**
* 播放器可以立即从当前位置播放。
*/
READY,
/**
* 播放完成
*/
ENDED;
companion object {
fun build(@Player.State state: Int): PlaybackState? {
when (state) {
Player.STATE_IDLE -> {
return IDLE
}
Player.STATE_BUFFERING -> {
return BUFFERING
}
Player.STATE_READY -> {
return READY
}
Player.STATE_ENDED -> {
return ENDED
}
else -> {
return null
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
package com.chuhai.core.player
import com.chuhai.core.exceptions.SuperException
/**
* Created by Max on 2023/11/2 14:45
* Desc:播放器异常
*/
class PlayerException : SuperException {
companion object {
/**
* 数据源异常
*/
const val TYPE_SOURCE = 0
/**
* 意外错误
*/
const val TYPE_UNEXPECTED = 1
}
constructor(code: Int) : super(code)
constructor(code: Int, message: String?) : super(message, code)
constructor(code: Int, message: String?, cause: Throwable?) : super(message, cause, code)
constructor(code: Int, cause: Throwable?) : super(cause, code)
}

View File

@@ -0,0 +1,30 @@
package com.chuhai.core.player
/**
* Created by Max on 2023/11/2 14:45
* Desc:播放器事件监听器
*/
interface PlayerListener {
/**
* 播放器状态发送改变
* @param state 播放状态
*/
fun onPlaybackStateChanged(state: PlaybackState) {
}
/**
* playWhenReady 改变
* @param playWhenReady 准备好后是否继续播放
*/
fun onPlayWhenReadyChanged(playWhenReady: Boolean) {
}
/**
* 播放错误
*/
fun onPlayerError(throwable: PlayerException) {
}
}

View File

@@ -0,0 +1,51 @@
package com.chuhai.core.player
import com.chuhai.utils.ICleared
/**
* Created by Max on 2023/11/2 14:45
* Desc:播放器资源数据
*/
abstract class PlayerMediaItem : ICleared {
// 额外的自定义数据
private var tag: Any? = null
// 优先级
private var priority: Long? = null
/**
* 设置自定义参数
*/
fun setTag(tag: Any?) {
this.tag = tag
}
/**
* 获取自定义标记/参数
*/
fun getTag(): Any? {
return tag
}
/**
* 获取优先级(高到底)
*/
fun getPriority(): Long? {
return priority
}
/**
* 设置优先级
*/
fun setPriority(priority: Long?) {
this.priority = priority
}
/**
* 释放
*/
override fun onCleared() {
super.onCleared()
}
}

View File

@@ -0,0 +1,15 @@
package com.chuhai.core.player.exo
import android.net.Uri
import com.chuhai.core.player.PlayerMediaItem
/**
* Created by Max on 2023/11/2 14:53
* Desc:Exo
**/
class ExoMediaItem(
/**
* 资源uri
*/
var uri: Uri
) : PlayerMediaItem()

View File

@@ -0,0 +1,136 @@
package com.chuhai.core.player.exo
import android.content.Context
import android.util.AttributeSet
import android.view.View
import androidx.lifecycle.LifecycleOwner
import com.chuhai.core.player.IPlayer
import com.chuhai.core.player.PlayerListener
import com.chuhai.core.player.PlayerMediaItem
import com.chuhai.core.player.PlaybackState
import com.chuhai.core.player.PlayerException
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.PlaybackException
import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.ExoPlayer as ExoPlayerImpl
/**
* Created by Max on 2023/11/2 14:50
* Desc:基于Exo
**/
class ExoPlayer : StyledPlayerView, IPlayer {
private var currentItem: ExoMediaItem? = null
private var player: ExoPlayerImpl
private var listener: PlayerListener? = null
private var listenerAdapter: Player.Listener? = null
constructor(context: Context) : this(context, null)
constructor(context: Context, attrs: AttributeSet?) : this(context, attrs, 0)
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
context,
attrs,
defStyleAttr
) {
player = ExoPlayerImpl.Builder(context).build()
setPlayer(player)
}
override fun bindingLifeCycle(lifecycleOwner: LifecycleOwner) {
}
override fun prepare(item: PlayerMediaItem) {
if (item !is ExoMediaItem) {
return
}
currentItem = item
val mediaItem: MediaItem = MediaItem.fromUri(item.uri)
player.setMediaItem(mediaItem)
player.prepare()
}
override fun stop() {
player.stop()
}
override fun setPlayWhenReady(playWhenReady: Boolean) {
this.player.playWhenReady = playWhenReady
}
override fun getPlayWhenReady(): Boolean {
return this.player.playWhenReady
}
override fun restart() {
}
override fun getView(): View {
return this
}
override fun getCurrentMediaItem(): PlayerMediaItem? {
return currentItem
}
override fun getPlaybackState(): PlaybackState {
return PlaybackState.build(player.playbackState) ?: PlaybackState.IDLE
}
override fun setListener(listener: PlayerListener?) {
this.listener = listener
if (listener == null) {
listenerAdapter?.let {
player.removeListener(it)
}
listenerAdapter = null
} else {
player.addListener(getListenerAdapterNotNull())
}
}
private fun getListenerAdapterNotNull(): Player.Listener {
var listener = listenerAdapter
if (listener != null) {
return listener
}
listener = object : Player.Listener {
override fun onPlaybackStateChanged(playbackState: Int) {
super.onPlaybackStateChanged(playbackState)
this@ExoPlayer.listener?.onPlaybackStateChanged(
PlaybackState.build(playbackState) ?: PlaybackState.IDLE
)
}
override fun onPlayWhenReadyChanged(playWhenReady: Boolean, reason: Int) {
super.onPlayWhenReadyChanged(playWhenReady, reason)
this@ExoPlayer.listener?.onPlayWhenReadyChanged(playWhenReady)
}
override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
this@ExoPlayer.listener?.onPlayerError(
PlayerException(
PlayerException.TYPE_UNEXPECTED,
error
)
)
}
}
listenerAdapter = listener
return listener
}
override fun onCleared() {
super.onCleared()
setListener(null)
setPlayer(null)
player.release()
this.currentItem?.onCleared()
this.currentItem = null
}
}

View File

@@ -0,0 +1,403 @@
package com.chuhai.utils;
import android.annotation.SuppressLint;
import android.app.ActivityManager;
import android.app.Application;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.location.LocationManager;
import android.os.Handler;
import android.os.Looper;
import android.provider.Settings;
import android.text.TextUtils;
import android.view.View;
import android.view.Window;
import android.view.inputmethod.InputMethodManager;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* <pre>
* author:
* ___ ___ ___ ___
* _____ / /\ /__/\ /__/| / /\
* / /::\ / /::\ \ \:\ | |:| / /:/
* / /:/\:\ ___ ___ / /:/\:\ \ \:\ | |:| /__/::\
* / /:/~/::\ /__/\ / /\ / /:/~/::\ _____\__\:\ __| |:| \__\/\:\
* /__/:/ /:/\:| \ \:\ / /:/ /__/:/ /:/\:\ /__/::::::::\ /__/\_|:|____ \ \:\
* \ \:\/:/~/:/ \ \:\ /:/ \ \:\/:/__\/ \ \:\~~\~~\/ \ \:\/:::::/ \__\:\
* \ \::/ /:/ \ \:\/:/ \ \::/ \ \:\ ~~~ \ \::/~~~~ / /:/
* \ \:\/:/ \ \::/ \ \:\ \ \:\ \ \:\ /__/:/
* \ \::/ \__\/ \ \:\ \ \:\ \ \:\ \__\/
* \__\/ \__\/ \__\/ \__\/
* blog : http://blankj.com
* time : 16/12/08
* desc : utils about initialization
* </pre>
*/
public final class AppUtils {
private static final ExecutorService UTIL_POOL = Executors.newFixedThreadPool(3);
private static final Handler UTIL_HANDLER = new Handler(Looper.getMainLooper());
@SuppressLint("StaticFieldLeak")
private static Application sApplication;
private AppUtils() {
throw new UnsupportedOperationException("u can't instantiate me...");
}
/**
* Init utils.
* <p>Init it in the class of Application.</p>
*
* @param context context
*/
public static void init(final Context context) {
if (context == null) {
init(getApplicationByReflect());
return;
}
init((Application) context.getApplicationContext());
}
/**
* Init utils.
* <p>Init it in the class of Application.</p>
*
* @param app application
*/
public static void init(final Application app) {
if (sApplication == null) {
if (app == null) {
sApplication = getApplicationByReflect();
} else {
sApplication = app;
}
} else {
sApplication = app;
}
}
/**
* Return the context of Application object.
*
* @return the context of Application object
*/
public static Application getApp() {
if (sApplication != null) return sApplication;
Application app = getApplicationByReflect();
init(app);
return app;
}
public static String getPackageName(Context context) {
return context.getPackageName();
}
/**
* 获取版本名
*
* @param noSuffix 是否去掉后缀 (如:-debug、-test
*/
public static String getVersionName(boolean noSuffix) {
PackageInfo packageInfo = getPackageInfo(getApp());
if (packageInfo != null) {
String versionName = packageInfo.versionName;
if (noSuffix && versionName != null) {
int index = versionName.indexOf("-");
if (index >= 0) {
return versionName.substring(0, index);
}
}
return versionName;
}
return "";
}
//版本号
public static int getVersionCode() {
PackageInfo packageInfo = getPackageInfo(getApp());
if (packageInfo != null) {
return packageInfo.versionCode;
}
return 0;
}
/**
* 比较版本
* 1 = 大于当前版本
* 0 = 版本一样
* -1 = 当前版本大于更新版本
*/
public static int compareVersionNames(String newVersionName) {
try {
if (TextUtils.isEmpty(newVersionName)) {
return -1;
}
int res = 0;
String currentVersionName = getVersionName(true);
if (currentVersionName.equals(newVersionName)) {
return 0;
}
String[] oldNumbers = currentVersionName.split("\\.");
String[] newNumbers = newVersionName.split("\\.");
// To avoid IndexOutOfBounds
int minIndex = Math.min(oldNumbers.length, newNumbers.length);
for (int i = 0; i < minIndex; i++) {
int oldVersionPart = Integer.parseInt(oldNumbers[i]);
int newVersionPart = Integer.parseInt(newNumbers[i]);
if (oldVersionPart < newVersionPart) {
res = 1;
break;
} else if (oldVersionPart > newVersionPart) {
res = -1;
break;
}
}
// If versions are the same so far, but they have different length...
if (res == 0 && oldNumbers.length != newNumbers.length) {
res = (oldNumbers.length > newNumbers.length) ? -1 : 1;
}
return res;
} catch (Exception e) {
return -1;
}
}
private static PackageInfo getPackageInfo(Context context) {
PackageInfo packageInfo;
try {
PackageManager pm = context.getPackageManager();
packageInfo = pm.getPackageInfo(context.getPackageName(), PackageManager.GET_CONFIGURATIONS);
return packageInfo;
} catch (Exception e) {
return null;
}
}
static <T> Task<T> doAsync(final Task<T> task) {
UTIL_POOL.execute(task);
return task;
}
public static void runOnUiThread(final Runnable runnable) {
if (Looper.myLooper() == Looper.getMainLooper()) {
runnable.run();
} else {
AppUtils.UTIL_HANDLER.post(runnable);
}
}
public static void runOnUiThreadDelayed(final Runnable runnable, long delayMillis) {
AppUtils.UTIL_HANDLER.postDelayed(runnable, delayMillis);
}
static String getCurrentProcessName() {
String name = getCurrentProcessNameByFile();
if (!TextUtils.isEmpty(name)) return name;
name = getCurrentProcessNameByAms();
if (!TextUtils.isEmpty(name)) return name;
name = getCurrentProcessNameByReflect();
return name;
}
static void fixSoftInputLeaks(final Window window) {
InputMethodManager imm =
(InputMethodManager) AppUtils.getApp().getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) return;
String[] leakViews = new String[]{"mLastSrvView", "mCurRootView", "mServedView", "mNextServedView"};
for (String leakView : leakViews) {
try {
Field leakViewField = InputMethodManager.class.getDeclaredField(leakView);
if (leakViewField == null) continue;
if (!leakViewField.isAccessible()) {
leakViewField.setAccessible(true);
}
Object obj = leakViewField.get(imm);
if (!(obj instanceof View)) continue;
View view = (View) obj;
if (view.getRootView() == window.getDecorView().getRootView()) {
leakViewField.set(imm, null);
}
} catch (Throwable ignore) {/**/}
}
}
///////////////////////////////////////////////////////////////////////////
// private method
///////////////////////////////////////////////////////////////////////////
private static String getCurrentProcessNameByFile() {
try {
File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
String processName = mBufferedReader.readLine().trim();
mBufferedReader.close();
return processName;
} catch (Exception e) {
e.printStackTrace();
return "";
}
}
private static String getCurrentProcessNameByAms() {
ActivityManager am = (ActivityManager) AppUtils.getApp().getSystemService(Context.ACTIVITY_SERVICE);
if (am == null) return "";
List<ActivityManager.RunningAppProcessInfo> info = am.getRunningAppProcesses();
if (info == null || info.size() == 0) return "";
int pid = android.os.Process.myPid();
for (ActivityManager.RunningAppProcessInfo aInfo : info) {
if (aInfo.pid == pid) {
if (aInfo.processName != null) {
return aInfo.processName;
}
}
}
return "";
}
private static String getCurrentProcessNameByReflect() {
String processName = "";
try {
Application app = AppUtils.getApp();
Field loadedApkField = app.getClass().getField("mLoadedApk");
loadedApkField.setAccessible(true);
Object loadedApk = loadedApkField.get(app);
Field activityThreadField = loadedApk.getClass().getDeclaredField("mActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(loadedApk);
Method getProcessName = activityThread.getClass().getDeclaredMethod("getProcessName");
processName = (String) getProcessName.invoke(activityThread);
} catch (Exception e) {
e.printStackTrace();
}
return processName;
}
private static Application getApplicationByReflect() {
try {
@SuppressLint("PrivateApi")
Class<?> activityThread = Class.forName("android.app.ActivityThread");
Object thread = activityThread.getMethod("currentActivityThread").invoke(null);
Object app = activityThread.getMethod("getApplication").invoke(thread);
if (app == null) {
throw new NullPointerException("u should init first");
}
return (Application) app;
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
throw new NullPointerException("u should init first");
}
///////////////////////////////////////////////////////////////////////////
// interface
///////////////////////////////////////////////////////////////////////////
public abstract static class Task<Result> implements Runnable {
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int CANCELLED = 2;
private static final int EXCEPTIONAL = 3;
private volatile int state = NEW;
abstract Result doInBackground();
private final Callback<Result> mCallback;
public Task(final Callback<Result> callback) {
mCallback = callback;
}
@Override
public void run() {
try {
final Result t = doInBackground();
if (state != NEW) return;
state = COMPLETING;
UTIL_HANDLER.post(new Runnable() {
@Override
public void run() {
mCallback.onCall(t);
}
});
} catch (Throwable th) {
if (state != NEW) return;
state = EXCEPTIONAL;
}
}
public void cancel() {
state = CANCELLED;
}
public boolean isDone() {
return state != NEW;
}
public boolean isCanceled() {
return state == CANCELLED;
}
}
public interface Callback<T> {
void onCall(T data);
}
/**
* 判断是否打开定位
*/
public static boolean getGpsStatus(Context ctx) {
//从系统服务中获取定位管理器
LocationManager locationManager
= (LocationManager) ctx.getSystemService(Context.LOCATION_SERVICE);
// 通过GPS卫星定位定位级别可以精确到街通过24颗卫星定位在室外和空旷的地方定位准确、速度快
boolean gps = locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER);
// 通过WLAN或移动网络(3G/2G)确定的位置也称作AGPS辅助GPS定位。主要用于在室内或遮盖物建筑群或茂密的深林等密集的地方定位
boolean network = locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER);
if (gps || network) {
return true;
}
return false;
}
/**
* 打开系统定位界面
*/
public static void goToOpenGps(Context ctx) {
Intent intent = new Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS);
ctx.startActivity(intent);
}
}

View File

@@ -0,0 +1,14 @@
package com.chuhai.utils
/**
* Created by Max on 2023/10/26 11:50
* Desc:清除释放统一接口
**/
interface ICleared {
/**
* 清除/释放
*/
fun onCleared() {}
}

View File

@@ -0,0 +1,25 @@
package com.chuhai.utils
import android.os.SystemClock
/**
* Created by Max on 2023/10/24 15:11
* Desc:服务器时间
*/
object ServiceTime {
// 服务器时间与系统开机时间的时差
private var serviceTimeDiff: Long? = null
val time
get() = if (serviceTimeDiff == null) System.currentTimeMillis()
else SystemClock.elapsedRealtime() + serviceTimeDiff!!
/**
* 刷新服务器时间
*/
fun refreshServiceTime(time: Long) {
//serviceTimeDiff = 服务器时间 - 此刻系统启动时间
serviceTimeDiff = time - SystemClock.elapsedRealtime()
}
}

View File

@@ -0,0 +1,42 @@
package com.chuhai.utils
import android.graphics.Outline
import android.view.View
import android.view.ViewOutlineProvider
import kotlin.math.min
/**
* Created by Max on 2023/10/24 15:11
* Desc:
*/
class ShapeViewOutlineProvider {
/**
* Created by Max on 2/25/21 1:48 PM
* Desc:圆角
*/
class Round(var corner: Float) : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
outline.setRoundRect(
0,
0,
view.width,
view.height,
corner
)
}
}
/**
* Created by Max on 2/25/21 1:48 PM
* Desc:圆形
*/
class Circle : ViewOutlineProvider() {
override fun getOutline(view: View, outline: Outline) {
val min = min(view.width, view.height)
val left = (view.width - min) / 2
val top = (view.height - min) / 2
outline.setOval(left, top, min, min)
}
}
}

View File

@@ -0,0 +1,84 @@
package com.chuhai.utils
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.util.DisplayMetrics
import android.util.TypedValue
import android.view.View
import android.view.WindowManager
import androidx.core.text.TextUtilsCompat
import androidx.core.view.ViewCompat
/**
* Created by Max on 2023/10/24 15:11
*/
object UiUtils {
fun getScreenWidth(context: Context): Int {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
val outMetrics = DisplayMetrics()
wm?.defaultDisplay?.getMetrics(outMetrics)
return outMetrics.widthPixels
}
fun getScreenHeight(context: Context): Int {
val wm = context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager
val outMetrics = DisplayMetrics()
wm?.defaultDisplay?.getMetrics(outMetrics)
return outMetrics.heightPixels
}
fun getScreenRatio(context: Context): Float {
return getScreenWidth(context) * 1.0f / getScreenHeight(context)
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
fun dip2px(dpValue: Float): Int {
return dip2px(AppUtils.getApp(), dpValue)
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
fun px2dip(pxValue: Float): Float {
return px2dip(AppUtils.getApp(), pxValue)
}
/**
* 根据手机的分辨率从 dp 的单位 转成为 px(像素)
*/
fun dip2px(context: Context, dpValue: Float): Int {
return (TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpValue, context.resources.displayMetrics) + 0.5f).toInt()
}
/**
* 根据手机的分辨率从 px(像素) 的单位 转成为 dp
*/
fun px2dip(context: Context, pxValue: Float): Float {
return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, pxValue, context.resources.displayMetrics)
}
/**
* 是否从右到左布局
*/
@SuppressLint("NewApi")
fun isRtl(view: View): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
View.LAYOUT_DIRECTION_RTL == view.layoutDirection
} else {
false
}
}
/**
* 是否从右到左布局
*/
fun isRtl(context: Context): Boolean {
return TextUtilsCompat.getLayoutDirectionFromLocale(context.resources.configuration.locale) == ViewCompat.LAYOUT_DIRECTION_RTL
}
}

View File

@@ -0,0 +1,70 @@
package com.chuhai.utils.ktx
import android.app.Activity
import android.content.Context
import android.content.ContextWrapper
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
/**
* Created by Max on 2023/10/25 15:57
* Desc:Context相关工具
**/
/**
* Context转换为Activity
*/
fun Context?.asActivity(): Activity? {
return when {
this is Activity -> {
this
}
(this as? ContextWrapper)?.baseContext?.applicationContext != null -> {
baseContext.asActivity()
}
else -> {
null
}
}
}
/**
* Context转换为Lifecycle
*/
fun Context?.asLifecycle(): Lifecycle? {
if (this == null) return null
return when (this) {
is Lifecycle -> {
this
}
is LifecycleOwner -> {
this.lifecycle
}
is ContextWrapper -> {
this.baseContext.asLifecycle()
}
else -> {
null
}
}
}
/**
* Context转换为LifecycleOwner
*/
fun Context?.asLifecycleOwner(): LifecycleOwner? {
if (this == null) return null
return when (this) {
is LifecycleOwner -> {
this
}
is ContextWrapper -> {
this.baseContext.asLifecycleOwner()
}
else -> {
null
}
}
}

View File

@@ -0,0 +1,106 @@
package com.chuhai.utils.ktx
import android.text.Editable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.Spanned
import android.text.TextWatcher
import android.text.method.HideReturnsTransformationMethod
import android.text.method.PasswordTransformationMethod
import android.widget.EditText
/**
* 设置editText输入监听
* @param onChanged 改变事件
* @return 是否接受此次文本的改变
*/
inline fun EditText.setOnInputChangedListener(
/**
* @param Int当前长度
* @return 是否接受此次文本的改变
*/
crossinline onChanged: (Int).() -> Boolean
) {
this.addTextChangedListener(object : TextWatcher {
var flag = false
override fun afterTextChanged(p0: Editable?) {
if (flag) {
return
}
if (!onChanged(p0?.length ?: 0)) {
flag = true
this@setOnInputChangedListener.setText(
this@setOnInputChangedListener.getTag(
1982329101
) as? String
)
this@setOnInputChangedListener.setSelection(this@setOnInputChangedListener.length())
flag = false
} else {
flag = false
}
}
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
this@setOnInputChangedListener.setTag(1982329101, p0?.toString())
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
})
}
/**
* 切换密码可见度
*/
fun EditText.switchPasswordVisibility(visibility: Boolean) {
transformationMethod =
if (!visibility) HideReturnsTransformationMethod.getInstance() else PasswordTransformationMethod.getInstance()
}
/**
* 设置输入功能是否启用不启用就相当于TextView
*/
fun EditText.setInputEnabled(isEnabled: Boolean) {
if (isEnabled) {
isFocusable = true
isFocusableInTouchMode = true
isClickable = true
} else {
isFocusable = false
isFocusableInTouchMode = false
isClickable = false
keyListener = null
}
}
/**
* 添加输入长度限制过滤器
*/
fun EditText.addLengthFilter(maxLength: Int) {
val newFilters = filters.copyOf(filters.size + 1)
newFilters[newFilters.size - 1] = LengthFilter(maxLength)
filters = newFilters
}
/**
* 添加禁用文本过滤器
* @param disableText 不允许输入该文本
*/
fun EditText.addDisableFilter(vararg disableText: CharSequence) {
val newFilters = filters.copyOf(filters.size + 1)
newFilters[newFilters.size - 1] = InputFilter { source, p1, p2, p3, p4, p5 ->
disableText.forEach {
if (source.equals(it)) {
return@InputFilter ""
}
}
return@InputFilter null
}
filters = newFilters
}

View File

@@ -0,0 +1,194 @@
package com.chuhai.utils.ktx
import android.app.Activity
import android.content.Context
import android.content.res.TypedArray
import android.graphics.drawable.Drawable
import android.util.TypedValue
import androidx.annotation.*
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import com.chuhai.utils.AppUtils
/**
* Created by Max on 2023/10/24 15:11
* 资源工具类
*/
/**
* 获取颜色
*/
fun Fragment.getColorById(@ColorRes colorResId: Int): Int {
return ContextCompat.getColor(context!!, colorResId)
}
/**
* 获取图片
*/
fun Fragment.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
return ContextCompat.getDrawable(context!!, drawableRedId)
}
/**
* 获取颜色
*/
fun Activity.getColorById(@ColorRes colorResId: Int): Int {
return ContextCompat.getColor(this, colorResId)
}
/**
* 获取图片
*/
fun Activity.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableRedId)
}
/**
* 获取颜色
*/
fun Context.getColorById(@ColorRes colorResId: Int): Int {
return ContextCompat.getColor(this, colorResId)
}
/**
* 获取图片
*/
fun Context.getDrawableById(@DrawableRes drawableRedId: Int): Drawable? {
return ContextCompat.getDrawable(this, drawableRedId)
}
/**
* 获取字符串资源
*/
fun Any.getStringById(@StringRes stringResId: Int): String {
return AppUtils.getApp().getString(stringResId)
}
/**
* 获取字符串资源
*/
fun Int.toStringRes(): String {
return AppUtils.getApp().getString(this)
}
/**
* 获取资源drawable
* */
fun Int.toDrawableRes(): Drawable? {
return ContextCompat.getDrawable(AppUtils.getApp(), this)
}
/**
* 获取资源color
* */
fun Int.toColorRes(): Int {
return ContextCompat.getColor(AppUtils.getApp(), this)
}
/**
* 通过自定义属性-获取DrawableRes
*/
@DrawableRes
fun Context.getDrawableResFromAttr(
@AttrRes attrResId: Int,
typedValue: TypedValue = TypedValue(),
resolveRefs: Boolean = true
): Int? {
return try {
theme.resolveAttribute(attrResId, typedValue, resolveRefs)
return typedValue.resourceId
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 通过自定义属性-获取Drawable
*/
fun Context.getDrawableFromAttr(@AttrRes attrId: Int): Drawable? {
return try {
val drawableRes = getDrawableResFromAttr(attrId) ?: return null
ResourcesCompat.getDrawable(resources, drawableRes, null)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 通过自定义属性-获取ColorRes
*/
@ColorRes
fun Context.getColorResFromAttr(
@AttrRes attrResId: Int,
typedValue: TypedValue = TypedValue(),
resolveRefs: Boolean = true
): Int? {
return try {
theme.resolveAttribute(attrResId, typedValue, resolveRefs)
return typedValue.resourceId
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 通过自定义属性-获取Color
*/
@ColorRes
fun Context.getColorFromAttr(
@AttrRes attrResId: Int
): Int? {
return try {
val colorRes = getColorFromAttr(attrResId) ?: return null
ResourcesCompat.getColor(resources, colorRes, null)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 通过自定义属性-获取LayoutRes
*/
@LayoutRes
fun Context.getLayoutResFromAttr(
@AttrRes attrResId: Int,
typedValue: TypedValue = TypedValue(),
resolveRefs: Boolean = true
): Int? {
return try {
theme.resolveAttribute(attrResId, typedValue, resolveRefs)
return typedValue.resourceId
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 通过自定义属性-获取Boolean
*/
fun Context.getBooleanResFromAttr(
@AttrRes attrResId: Int,
defValue: Boolean = false
): Boolean {
var attrs: TypedArray? = null
try {
attrs = obtainStyledAttributes(null, intArrayOf(attrResId))
return attrs.getBoolean(0, defValue)
} catch (e: Exception) {
e.printStackTrace()
} finally {
attrs?.recycle()
}
return defValue
}

View File

@@ -0,0 +1,51 @@
package com.chuhai.utils.ktx
import com.chuhai.utils.UiUtils
import kotlin.math.roundToInt
/**
* Created by Max on 2023/10/24 15:11
*/
/**
* 转换为PX值
*/
val Float.dp: Int get() = this.toPX()
val Int.dp: Int get() = this.toPX()
/**
* 转换为DP值
*/
val Float.px: Int get() = this.toDP().roundToInt()
val Int.px: Int get() = this.toDP().roundToInt()
fun Long.toDP(): Float {
return UiUtils.px2dip(this.toFloat())
}
fun Float.toDP(): Float {
return UiUtils.px2dip(this)
}
fun Int.toDP(): Float {
return UiUtils.px2dip(this.toFloat())
}
fun Long.toPX(): Int {
return UiUtils.dip2px(this.toFloat())
}
fun Float.toPX(): Int {
return UiUtils.dip2px(this)
}
fun Int.toPX(): Int {
return UiUtils.dip2px(this.toFloat())
}

View File

@@ -0,0 +1,192 @@
package com.chuhai.utils.ktx
import android.graphics.*
import android.os.Build
import android.os.SystemClock
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.widget.Checkable
import android.widget.TextView
import androidx.core.view.ScrollingView
import com.chuhai.utils.ShapeViewOutlineProvider
import com.chuhai.utils.UiUtils
/**
* 是否右-左布局
*/
fun View.isRtl(): Boolean {
return UiUtils.isRtl(this)
}
/**
* 展示or隐藏
*/
fun View.visibleOrGone(isShow: Boolean) {
visibility = if (isShow) {
View.VISIBLE
} else {
View.GONE
}
}
/**
* 展示or隐藏
*/
inline fun View.visibleOrGone(show: View.() -> Boolean = { true }) {
visibility = if (show(this)) {
View.VISIBLE
} else {
View.GONE
}
}
/**
* 展示or不可见
*/
inline fun View.visibleOrInvisible(show: View.() -> Boolean = { true }) {
visibility = if (show(this)) {
View.VISIBLE
} else {
View.INVISIBLE
}
}
/**
* 点击事件
*/
inline fun <T : View> T.singleClick(time: Long = 800, crossinline block: (T) -> Unit) {
setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0L
override fun onClick(v: View?) {
val currentTimeMillis = SystemClock.elapsedRealtime()
if (currentTimeMillis - lastClickTime > time || this is Checkable) {
lastClickTime = currentTimeMillis
block(this@singleClick)
}
}
})
}
/**
* 点击事件
*/
fun <T : View> T.singleClick(onClickListener: View.OnClickListener, time: Long = 800) {
setOnClickListener(object : View.OnClickListener {
private var lastClickTime: Long = 0L
override fun onClick(v: View?) {
val currentTimeMillis = SystemClock.elapsedRealtime()
if (currentTimeMillis - lastClickTime > time || this is Checkable) {
lastClickTime = currentTimeMillis
onClickListener.onClick(v)
}
}
})
}
/**
* 设置View圆角矩形
*/
fun <T : View> T.roundCorner(corner: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Round) {
outlineProvider = ShapeViewOutlineProvider.Round(corner.toFloat())
} else if (outlineProvider != null && outlineProvider is ShapeViewOutlineProvider.Round) {
(outlineProvider as ShapeViewOutlineProvider.Round).corner = corner.toFloat()
}
clipToOutline = true
}
}
/**
* 设置View为圆形
*/
fun <T : View> T.circle() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
if (outlineProvider == null || outlineProvider !is ShapeViewOutlineProvider.Circle) {
outlineProvider = ShapeViewOutlineProvider.Circle()
}
clipToOutline = true
}
}
fun View.getBitmap(): Bitmap {
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
canvas.translate(scrollX.toFloat(), scrollY.toFloat())
draw(canvas)
return bitmap
}
/**
* 设置边距
*/
fun View?.setMargin(start: Int? = null, top: Int? = null, end: Int? = null, bottom: Int? = null) {
(this?.layoutParams as? ViewGroup.MarginLayoutParams)?.apply {
start?.let {
this.marginStart = start
}
top?.let {
this.topMargin = top
}
end?.let {
this.marginEnd = end
}
bottom?.let {
this.bottomMargin = bottom
}
}
}
/**
* 设置内边距
*/
fun View?.setPadding2(start: Int? = null, top: Int? = null, end: Int? = null, bottom: Int? = null) {
if (this == null) return
this.setPadding(
start ?: paddingStart, top ?: paddingTop, end ?: paddingEnd, bottom ?: paddingBottom
)
}
/**
* 描边宽度
*/
fun TextView.strokeWidth(width: Float) {
this.paint?.style = Paint.Style.FILL_AND_STROKE
this.paint?.strokeWidth = width
this.invalidate()
}
/**
* 模拟点击并取消
*/
fun ScrollingView.simulateClickAndCancel() {
val view = this as? View ?: return
val downEvent = MotionEvent.obtain(
System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_DOWN, (view.right - view.left) / 2f, (view.bottom - view.top) / 2f, 0
)
view.dispatchTouchEvent(downEvent)
val cancelEvent = MotionEvent.obtain(
System.currentTimeMillis(), System.currentTimeMillis(), MotionEvent.ACTION_CANCEL, (view.right - view.left) / 2f, (view.bottom - view.top) / 2f, 0
)
view.dispatchTouchEvent(cancelEvent)
}
/**
* 使用灰色滤镜
*/
fun View.applyGrayFilter(isGray: Boolean) {
try {
val paint = Paint()
val colorMatrix = ColorMatrix()
colorMatrix.setSaturation(if (isGray) 0f else 1f)
paint.colorFilter = ColorMatrixColorFilter(colorMatrix)
setLayerType(View.LAYER_TYPE_HARDWARE, paint)
} catch (e: Exception) {
e.printStackTrace()
}
}

View File

@@ -0,0 +1,13 @@
package com.chuhai.utils.log
import android.util.Log
/**
* Created by Max on 2023/10/26 10:29
* Desc:Android日志
*/
class AndroidLogPrinter : LogPrinter {
override fun println(level: Int, tag: String, message: String) {
Log.println(level, tag, message)
}
}

View File

@@ -0,0 +1,61 @@
package com.chuhai.utils.log
/**
* Created by Max on 2023/10/26 10:29
* Desc:日志快捷使用接口
*/
interface ILog {
companion object {
/**
* 清理退出APP时调用
*/
fun onCleared() {
}
}
/**
* 默认日志Tag
*/
fun getLogTag(): String {
return this::class.java.simpleName
}
fun logI(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
LogUtil.i(tag, message, filePrinter)
}
fun logV(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
LogUtil.v(tag, message, filePrinter)
}
fun logW(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
LogUtil.w(tag, message, filePrinter)
}
fun logD(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
LogUtil.d(tag, message, filePrinter)
}
fun logE(message: String, tag: String = getLogTag(), filePrinter: Boolean = false) {
LogUtil.e(tag, message, filePrinter)
}
fun logE(
throwable: Throwable,
tag: String = getLogTag(),
filePrinter: Boolean = false
) {
LogUtil.e(tag, throwable, filePrinter)
}
fun logE(
message: String,
throwable: Throwable,
tag: String = getLogTag(),
filePrinter: Boolean = false
) {
LogUtil.e(tag, message, throwable, filePrinter)
}
}

View File

@@ -0,0 +1,14 @@
package com.chuhai.utils.log
/**
* Created by Max on 2023/10/26 10:29
* Desc: 日志打印
*/
interface LogPrinter {
/**
* 打印
* @param level 级别 [android.util.Log]
*/
fun println(level: Int, tag: String, message: String)
}

View File

@@ -0,0 +1,101 @@
package com.chuhai.utils.log
import android.util.Log
/**
* Created by Max on 2023/10/26 10:29
* Desc:日志工具
*/
object LogUtil {
private var consolePrinter: LogPrinter? = AndroidLogPrinter()
private var filePrinter: LogPrinter? = null
// 是否启动控制台打印
var consolePrinterEnabled: Boolean = true
// 是否启动文件打印
var filePrinterEnabled: Boolean = true
/**
* 设置文件打印
*/
fun setFilePrinter(filePrinter: LogPrinter) {
this.filePrinter = filePrinter
}
fun e(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.ERROR, tag, message, filePrinter)
}
fun e(tag: String, throwable: Throwable, filePrinter: Boolean = false) {
val cause = Log.getStackTraceString(throwable)
if (cause.isEmpty()) {
return
}
e(tag, cause, filePrinter)
}
fun e(tag: String, message: String?, throwable: Throwable, filePrinter: Boolean = false) {
val cause = Log.getStackTraceString(throwable)
if (message == null && cause.isEmpty()) {
return
}
e(tag, message + "\t\t" + cause, filePrinter)
}
fun d(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.DEBUG, tag, message, filePrinter)
}
fun i(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.INFO, tag, message, filePrinter)
}
fun v(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.VERBOSE, tag, message, filePrinter)
}
fun w(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.WARN, tag, message, filePrinter)
}
/**
* 输出日志
*/
fun log(level: Int = Log.INFO, tag: String?, message: String?, filePrinter: Boolean = false) {
if (tag.isNullOrEmpty()) {
return
}
if (message.isNullOrEmpty()) {
return
}
// 输出控制台
logConsole(level, tag, message)
// 输出文件
if (filePrinter) {
logFile(level, tag, message)
}
}
/**
* 输出到控制台
*/
fun logConsole(level: Int = Log.INFO, tag: String, message: String) {
if (!consolePrinterEnabled) {
return
}
consolePrinter?.println(level, tag, message)
}
/**
* 输出到文件
*/
fun logFile(level: Int = Log.INFO, tag: String, message: String) {
if (!filePrinterEnabled) {
return
}
filePrinter?.println(level, tag, message)
}
}

View File

@@ -0,0 +1,156 @@
package com.chuhai.utils.spannable;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.text.TextPaint;
import android.text.TextUtils;
import android.text.style.ReplacementSpan;
import android.util.TypedValue;
import androidx.annotation.NonNull;
/**
* Created by Max on 2023/10/26 20:14
**/
public class IconTextSpan extends ReplacementSpan {
private Context mContext;
private int mBgColorResId; //Icon背景颜色
private String mText; //Icon内文字
private float mBgHeight; //Icon背景高度
private float mBgWidth; //Icon背景宽度
private float mRadius; //Icon圆角半径
private float mRightMargin; //右边距
private float mTextSize; //文字大小
private int mTextColorResId; //文字颜色
private Paint mBgPaint; //icon背景画笔
private Paint mTextPaint; //icon文字画笔
private int paddingHorizontal = 0;
public IconTextSpan(Context context, int bgColorResId, String text, int textColor, int mTextSize, int round, int marginRight, int paddingHorizontal) {
if (TextUtils.isEmpty(text)) {
return;
}
this.paddingHorizontal = paddingHorizontal;
//初始化默认数值
initDefaultValue(context, bgColorResId, text, textColor, mTextSize, round, marginRight);
//计算背景的宽度
this.mBgWidth = caculateBgWidth(text);
//初始化画笔
initPaint();
}
/**
* 初始化画笔
*/
private void initPaint() {
//初始化背景画笔
mBgPaint = new Paint();
mBgPaint.setColor(mBgColorResId);
mBgPaint.setStyle(Paint.Style.FILL);
mBgPaint.setAntiAlias(true);
//初始化文字画笔
mTextPaint = new TextPaint();
mTextPaint.setColor(mTextColorResId);
mTextPaint.setTextSize(mTextSize);
mTextPaint.setAntiAlias(true);
mTextPaint.setTextAlign(Paint.Align.CENTER);
}
/**
* 初始化默认数值
*
* @param context 上下文
* @param textColor 字体颜色
*/
private void initDefaultValue(Context context, int bgColorResId, String text, int textColor, int textSize, int round, int marginRight) {
this.mContext = context.getApplicationContext();
this.mBgColorResId = bgColorResId;
this.mText = text;
this.mBgHeight = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 18f, mContext.getResources().getDisplayMetrics());
this.mRightMargin = marginRight;
this.mRadius = round;
this.mTextSize = textSize;
this.mTextColorResId = textColor;
}
/**
* 计算icon背景宽度
*
* @param text icon内文字
*/
private float caculateBgWidth(String text) {
// if (text.length() > 1) {
//多字,宽度=文字宽度+padding
Rect textRect = new Rect();
Paint paint = new Paint();
paint.setTextSize(mTextSize);
paint.getTextBounds(text, 0, text.length(), textRect);
float padding = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, paddingHorizontal, mContext.getResources().getDisplayMetrics());
return textRect.width() + padding * 2;
// } else {
//单字,宽高一致为正方形
// return mBgHeight + paddingHorizontal;
// }
}
/**
* 设置右边距
* @param rightMarginDpValue 右边边距
*/
public void setRightMarginDpValue(int rightMarginDpValue) {
this.mRightMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, rightMarginDpValue, mContext.getResources().getDisplayMetrics());
}
/**
* 设置宽度,宽度=背景宽度+右边距
*/
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
return (int) (mBgWidth + mRightMargin);
}
/**
* draw
*
* @param text 完整文本
* @param start setSpan里设置的start
* @param end setSpan里设置的start
* @param top 当前span所在行的上方y
* @param y y其实就是metric里baseline的位置
* @param bottom 当前span所在行的下方y(包含了行间距)会和下一行的top重合
* @param paint 使用此span的画笔
*/
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint) {
//画背景
Paint bgPaint = new Paint();
bgPaint.setColor(mBgColorResId);
bgPaint.setStyle(Paint.Style.FILL);
bgPaint.setAntiAlias(true);
Paint.FontMetrics metrics = paint.getFontMetrics();
float textHeight = metrics.descent - metrics.ascent;
//算出背景开始画的y坐标
float bgStartY = y + (textHeight - mBgHeight) / 2 + metrics.ascent;
//画背景
RectF bgRect = new RectF(x, bgStartY, x + mBgWidth , bgStartY + mBgHeight);
canvas.drawRoundRect(bgRect, mRadius, mRadius, bgPaint);
//把字画在背景中间
TextPaint textPaint = new TextPaint();
textPaint.setColor(mTextColorResId);
textPaint.setTextSize(mTextSize);
textPaint.setAntiAlias(true);
textPaint.setTextAlign(Paint.Align.CENTER); //这个只针对x有效
Paint.FontMetrics fontMetrics = textPaint.getFontMetrics();
float textRectHeight = fontMetrics.bottom - fontMetrics.top;
canvas.drawText(mText, x + mBgWidth / 2, bgStartY + (mBgHeight - textRectHeight) / 2 - fontMetrics.top, textPaint);
}
}

View File

@@ -0,0 +1,29 @@
package com.chuhai.utils.spannable
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.RectF
import android.text.style.ReplacementSpan
/**
* Created by Max on 2023/10/26 20:14
* Desc:文字 圆背景
**/
class RoundBackgroundColorSpan(var textColor: Int, var textSize: Int, var bgColor: Int, var paddingHorizontal: Int, var paddingVertical: Int, var marginHorizontal: Int,var round:Int) : ReplacementSpan() {
override fun getSize(paint: Paint, text: CharSequence?, start: Int, end: Int, fm: Paint.FontMetricsInt?): Int {
return paint.measureText(text, start, end).toInt()+(paddingHorizontal)+marginHorizontal
}
override fun draw(canvas: Canvas, text: CharSequence?, start: Int, end: Int, x: Float, top: Int, y: Int, bottom: Int, paint: Paint) {
paint.color = this.textColor
paint.textSize = textSize.toFloat()
canvas.drawText(text.toString(), start, end, x + paddingHorizontal+marginHorizontal, y.toFloat()-paddingVertical, paint)
paint.color = paint.color
paint.color = this.bgColor;
val rectF = RectF(x+marginHorizontal, top.toFloat(), (paint.measureText(text.toString())) , bottom.toFloat())
canvas.drawRoundRect(rectF, round.toFloat(), round.toFloat(), paint)
}
}

View File

@@ -0,0 +1,327 @@
package com.chuhai.utils.spannable
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.*
import android.view.View
import android.widget.TextView
import androidx.annotation.ColorInt
import androidx.annotation.DrawableRes
import com.chuhai.utils.ktx.dp
/**
* Created by Max on 2023/10/26 20:14
* Desc:可扩展文本
**/
class SpannableTextBuilder(private val textView: TextView) {
private val spannableBuilder: SpannableStringBuilder by lazy {
SpannableStringBuilder()
}
/**
* 添加一段文本
*/
fun appendText(node: TextNode) {
val onClick: ((String) -> Unit)? = if (node.getOnClickListener() != null) {
{
node.getOnClickListener()?.invoke(node)
}
} else {
null
}
appendText(
text = node.getContent(),
textColor = node.getTextColor(),
textSize = node.getTextSize(),
backgroundColor = node.getBackgroundColor(),
underline = node.isUnderline(),
clickListener = onClick
)
}
/**
* 添加一段文本
* @param text 文本
* @param textColor 文本颜色
* @param backgroundColor 背景颜色
* @param textSize 文本大小
* @param textStyle 文本样式
* @param underline 是否有下划线
* @param clickListener 点击事件
*/
fun appendText(
text: String,
@ColorInt textColor: Int? = null,
@ColorInt backgroundColor: Int? = null,
textSize: Int? = null,
textStyle: Int? = null,
underline: Boolean? = null,
clickListener: ((String) -> Unit)? = null
): SpannableTextBuilder {
val start = spannableBuilder.length
spannableBuilder.append(text)
val end = spannableBuilder.length
// 文本颜色
if (textColor != null) {
spannableBuilder.setSpan(
ForegroundColorSpan(textColor),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// 文本背景颜色
if (backgroundColor != null) {
spannableBuilder.setSpan(
BackgroundColorSpan(backgroundColor),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// 文本大小
if (textSize != null) {
spannableBuilder.setSpan(
AbsoluteSizeSpan(textSize, true),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// 文本样式
if (textStyle != null) {
spannableBuilder.setSpan(
StyleSpan(textStyle),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
// 下划线
if (underline == true) {
spannableBuilder.setSpan(UnderlineSpan(), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
// 点击事件
if (clickListener != null) {
// 设置highlightColor=Color.TRANSPARENT可以解决点击时的高亮色问题但光标的区域选中也是透明的貌似对用户体验不太好
// textView.highlightColor = Color.TRANSPARENT
textView.movementMethod = LinkMovementMethod.getInstance()
val clickableSpan = TextClickableSpan(
clickListener, text, textColor
?: textView.currentTextColor, underline ?: false
)
spannableBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
/**
* 添加图片
* @param drawable 图片
* @param clickListener 点击事件
*/
fun appendDrawable(
@DrawableRes drawable: Int,
clickListener: ((Int) -> Unit)?
): SpannableTextBuilder {
// 需要时再完善
val start = spannableBuilder.length
spannableBuilder.append("[icon}")
val end = spannableBuilder.length
// 图片
val imageSpan: ImageSpan = VerticalImageSpan(textView.context, drawable)
spannableBuilder.setSpan(imageSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
// 点击事件
if (clickListener != null) {
textView.movementMethod = LinkMovementMethod.getInstance()
val clickableSpan = DrawableClickableSpan(clickListener, drawable)
spannableBuilder.setSpan(clickableSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
}
return this
}
/**
* 添加有背景圆角的文字
* @param text 文本
* @param textColor 文本颜色
* @param backgroundColor 背景颜色
* @param paddingHorizontal 内横向边距
* @param paddingVertical 内竖向边距
* @param marginHorizontal 外横向边距
*/
fun appendTextRoundBackground(
text: String,
@ColorInt textColor: Int,
textSize: Int,
@ColorInt backgroundColor: Int,
paddingHorizontal: Int,
paddingVertical: Int,
marginHorizontal: Int,
round: Int
): SpannableTextBuilder {
val start = spannableBuilder.length
spannableBuilder.append(text)
val end = spannableBuilder.length
spannableBuilder.setSpan(
RoundBackgroundColorSpan(
textColor,
textSize,
backgroundColor,
paddingHorizontal,
paddingVertical,
marginHorizontal,
round
), start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
);
return this
}
/**
* 添加有背景圆角的文字
* @param text 文本
* @param textColor 文本颜色
* @param backgroundColor 背景颜色
* @param paddingHorizontal 内横向边距
* @param paddingVertical 内竖向边距
* @param marginHorizontal 外横向边距
*/
fun appendIconTextRoundBackground(
text: String,
@ColorInt textColor: Int,
textSize: Int,
@ColorInt backgroundColor: Int,
marginRight: Int,
round: Int
): SpannableTextBuilder {
val start = spannableBuilder.length
spannableBuilder.append(text)
val end = spannableBuilder.length
spannableBuilder.setSpan(
IconTextSpan(
textView.context,
backgroundColor,
text,
textColor,
textSize,
round,
marginRight,
2.dp
),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return this
}
fun build(): SpannableStringBuilder {
return spannableBuilder
}
/**
* 应用
*/
fun apply() {
textView.text = spannableBuilder
}
/**
* 文本点击
*/
class TextClickableSpan(
private val clickListener: ((String) -> Unit)? = null,
private val text: String,
private val textColor: Int,
private val underline: Boolean
) : ClickableSpan() {
override fun onClick(widget: View) {
clickListener?.invoke(text)
}
override fun updateDrawState(ds: TextPaint) {
ds.color = textColor
ds.isUnderlineText = underline
}
}
/**
* 图片点击
*/
class DrawableClickableSpan(
private val clickListener: ((Int) -> Unit)? = null,
private val drawable: Int
) : ClickableSpan() {
override fun onClick(widget: View) {
clickListener?.invoke(drawable)
}
}
interface TextNode {
/**
* 内容
*/
fun getContent(): String
/**
* 文本颜色
*/
fun getTextSize(): Int? {
return null
}
/**
* 文本颜色
*/
fun getTextColor(): Int? {
return null
}
/**
* 文本样式
*/
fun getTextStyle(): Int? {
return null
}
/**
* 背景颜色
*/
fun getBackgroundColor(): Int? {
return null
}
/**
* 是否有下划线
*/
fun isUnderline(): Boolean {
return false
}
/**
* 获取点击事件
*/
fun getOnClickListener(): ((TextNode) -> Unit)? {
return null
}
}
}
/**
* 快速构建生成器
*/
fun TextView.spannableBuilder(): SpannableTextBuilder {
return SpannableTextBuilder(this)
}

View File

@@ -0,0 +1,66 @@
package com.chuhai.utils.spannable
import android.content.Context
import android.graphics.Canvas
import android.graphics.Paint
import android.graphics.drawable.Drawable
import android.text.style.ImageSpan
/**
* Created by Max on 2023/10/26 20:14
* Desc:垂直居中的ImageSpan
**/
class VerticalImageSpan : ImageSpan {
constructor(drawable: Drawable) : super(drawable)
constructor(context: Context, resourceId: Int) : super(context, resourceId)
/**
* update the text line height
*/
override fun getSize(
paint: Paint, text: CharSequence?, start: Int, end: Int,
fontMetricsInt: Paint.FontMetricsInt?
): Int {
val drawable = drawable
val rect = drawable.bounds
if (fontMetricsInt != null) {
val fmPaint = paint.fontMetricsInt
val fontHeight = fmPaint.descent - fmPaint.ascent
val drHeight = rect.bottom - rect.top
val centerY = fmPaint.ascent + fontHeight / 2
fontMetricsInt.ascent = centerY - drHeight / 2
fontMetricsInt.top = fontMetricsInt.ascent
fontMetricsInt.bottom = centerY + drHeight / 2
fontMetricsInt.descent = fontMetricsInt.bottom
}
return rect.right
}
/**
* see detail message in android.text.TextLine
*
* @param canvas the canvas, can be null if not rendering
* @param text the text to be draw
* @param start the text start position
* @param end the text end position
* @param x the edge of the replacement closest to the leading margin
* @param top the top of the line
* @param y the baseline
* @param bottom the bottom of the line
* @param paint the work paint
*/
override fun draw(
canvas: Canvas, text: CharSequence, start: Int, end: Int,
x: Float, top: Int, y: Int, bottom: Int, paint: Paint
) {
val drawable = drawable
canvas.save()
val fmPaint = paint.fontMetricsInt
val fontHeight = fmPaint.descent - fmPaint.ascent
val centerY = y + fmPaint.descent - fontHeight / 2
val transY = centerY - (drawable.bounds.bottom - drawable.bounds.top) / 2
canvas.translate(x, transY.toFloat())
drawable.draw(canvas)
canvas.restore()
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -0,0 +1,3 @@
<resources>
</resources>