feat : libs

This commit is contained in:
eggmanQQQ2
2025-07-07 10:49:39 +08:00
parent 310c7c4e65
commit 59046caf0d
83 changed files with 7191 additions and 0 deletions

1
libs/lib_utils/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,10 @@
apply from : "../lib_standard.gradle"
android {
namespace 'com.example.lib_utils'
}
dependencies {
api "androidx.core:core-ktx:1.9.0"
api 'androidx.appcompat:appcompat:1.6.1'
}

View File

21
libs/lib_utils/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# 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

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@@ -0,0 +1,403 @@
package com.example.lib_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);
}
}

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -0,0 +1,521 @@
package com.example.lib_utils
import android.app.Application
import android.os.Build
import android.os.Environment
import java.io.File
/**
* getRootPath : 获取根路径
* getDataPath : 获取数据路径
* getDownloadCachePath : 获取下载缓存路径
* getInternalAppDataPath : 获取内存应用数据路径
* getInternalAppCodeCacheDir : 获取内存应用代码缓存路径
* getInternalAppCachePath : 获取内存应用缓存路径
* getInternalAppDbsPath : 获取内存应用数据库路径
* getInternalAppDbPath : 获取内存应用数据库路径
* getInternalAppFilesPath : 获取内存应用文件路径
* getInternalAppSpPath : 获取内存应用 SP 路径
* getInternalAppNoBackupFilesPath: 获取内存应用未备份文件路径
* getExternalStoragePath : 获取外存路径
* getExternalMusicPath : 获取外存音乐路径
* getExternalPodcastsPath : 获取外存播客路径
* getExternalRingtonesPath : 获取外存铃声路径
* getExternalAlarmsPath : 获取外存闹铃路径
* getExternalNotificationsPath : 获取外存通知路径
* getExternalPicturesPath : 获取外存图片路径
* getExternalMoviesPath : 获取外存影片路径
* getExternalDownloadsPath : 获取外存下载路径
* getExternalDcimPath : 获取外存数码相机图片路径
* getExternalDocumentsPath : 获取外存文档路径
* getExternalAppDataPath : 获取外存应用数据路径
* getExternalAppCachePath : 获取外存应用缓存路径
* getExternalAppFilesPath : 获取外存应用文件路径
* getExternalAppMusicPath : 获取外存应用音乐路径
* getExternalAppPodcastsPath : 获取外存应用播客路径
* getExternalAppRingtonesPath : 获取外存应用铃声路径
* getExternalAppAlarmsPath : 获取外存应用闹铃路径
* getExternalAppNotificationsPath: 获取外存应用通知路径
* getExternalAppPicturesPath : 获取外存应用图片路径
* getExternalAppMoviesPath : 获取外存应用影片路径
* getExternalAppDownloadPath : 获取外存应用下载路径
* getExternalAppDcimPath : 获取外存应用数码相机图片路径
* getExternalAppDocumentsPath : 获取外存应用文档路径
* getExternalAppObbPath : 获取外存应用 OBB 路径
* 路径 工具类 By https://github.com/Blankj/AndroidUtilCode -> PathUtils.java
* Created by Max on 2018/12/12.
*/
object PathUtils {
/**
* Return the path of /system.
*
* @return the path of /system
*/
val rootPath: String
get() = Environment.getRootDirectory().absolutePath
/**
* Return the path of /data.
*
* @return the path of /data
*/
val dataPath: String
get() = Environment.getDataDirectory().absolutePath
/**
* Return the path of /cache.
*
* @return the path of /cache
*/
val downloadCachePath: String
get() = Environment.getDownloadCacheDirectory().absolutePath
/**
* Return the path of /data/data/package.
*
* @return the path of /data/data/package
*/
fun getInternalAppDataPath(application: Application): String {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
application.applicationInfo.dataDir
} else application.dataDir.absolutePath
}
/**
* Return the path of /data/data/package/code_cache.
*
* @return the path of /data/data/package/code_cache
*/
fun getInternalAppCodeCacheDir(application: Application): String {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
application.applicationInfo.dataDir + "/code_cache"
} else application.codeCacheDir.absolutePath
}
/**
* Return the path of /data/data/package/cache.
*
* @return the path of /data/data/package/cache
*/
fun getInternalAppCachePath(application: Application): String {
return application.cacheDir.absolutePath
}
/**
* Return the path of /data/data/package/databases.
*
* @return the path of /data/data/package/databases
*/
fun getInternalAppDbsPath(application: Application): String {
return application.applicationInfo.dataDir + "/databases"
}
/**
* Return the path of /data/data/package/databases/name.
*
* @param name The name of database.
* @return the path of /data/data/package/databases/name
*/
fun getInternalAppDbPath(application: Application, name: String?): String {
return application.getDatabasePath(name).absolutePath
}
/**
* Return the path of /data/data/package/files.
*
* @return the path of /data/data/package/files
*/
fun getInternalAppFilesPath(application: Application): String {
return application.filesDir.absolutePath
}
/**
* Return the path of /data/data/package/shared_prefs.
*
* @return the path of /data/data/package/shared_prefs
*/
fun getInternalAppSpPath(application: Application): String {
return application.applicationInfo.dataDir + "shared_prefs"
}
/**
* Return the path of /data/data/package/no_backup.
*
* @return the path of /data/data/package/no_backup
*/
fun getInternalAppNoBackupFilesPath(application: Application): String {
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
application.applicationInfo.dataDir + "no_backup"
} else application.noBackupFilesDir.absolutePath
}
/**
* Return the path of /storage/emulated/0.
*
* @return the path of /storage/emulated/0
*/
val externalStoragePath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStorageDirectory().absolutePath
/**
* Return the path of /storage/emulated/0/Music.
*
* @return the path of /storage/emulated/0/Music
*/
val externalMusicPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MUSIC
).absolutePath
/**
* Return the path of /storage/emulated/0/Podcasts.
*
* @return the path of /storage/emulated/0/Podcasts
*/
val externalPodcastsPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PODCASTS
).absolutePath
/**
* Return the path of /storage/emulated/0/Ringtones.
*
* @return the path of /storage/emulated/0/Ringtones
*/
val externalRingtonesPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_RINGTONES
).absolutePath
/**
* Return the path of /storage/emulated/0/Alarms.
*
* @return the path of /storage/emulated/0/Alarms
*/
val externalAlarmsPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_ALARMS
).absolutePath
/**
* Return the path of /storage/emulated/0/Notifications.
*
* @return the path of /storage/emulated/0/Notifications
*/
val externalNotificationsPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_NOTIFICATIONS
).absolutePath
/**
* Return the path of /storage/emulated/0/Pictures.
*
* @return the path of /storage/emulated/0/Pictures
*/
val externalPicturesPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_PICTURES
).absolutePath
/**
* Return the path of /storage/emulated/0/Movies.
*
* @return the path of /storage/emulated/0/Movies
*/
val externalMoviesPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_MOVIES
).absolutePath
/**
* Return the path of /storage/emulated/0/Download.
*
* @return the path of /storage/emulated/0/Download
*/
val externalDownloadsPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS
).absolutePath
/**
* Return the path of /storage/emulated/0/DCIM.
*
* @return the path of /storage/emulated/0/DCIM
*/
val externalDcimPath: String?
get() = if (isExternalStorageDisable) null else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DCIM
).absolutePath
/**
* Return the path of /storage/emulated/0/Documents.
*
* @return the path of /storage/emulated/0/Documents
*/
val externalDocumentsPath: String?
get() {
if (isExternalStorageDisable) return null
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
Environment.getExternalStorageDirectory().absolutePath + "/Documents"
} else Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOCUMENTS
).absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package.
*
* @return the path of /storage/emulated/0/Android/data/package
*/
fun getExternalAppDataPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.externalCacheDir?.parentFile?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/cache.
*
* @return the path of /storage/emulated/0/Android/data/package/cache
*/
fun getExternalAppCachePath(application: Application): String? {
return if (isExternalStorageDisable) null else application.externalCacheDir?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files.
*
* @return the path of /storage/emulated/0/Android/data/package/files
*/
fun getExternalAppFilesPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(null)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Music.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Music
*/
fun getExternalAppMusicPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_MUSIC
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Podcasts.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Podcasts
*/
fun getExternalAppPodcastsPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_PODCASTS
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Ringtones.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Ringtones
*/
fun getExternalAppRingtonesPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_RINGTONES
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Alarms.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Alarms
*/
fun getExternalAppAlarmsPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_ALARMS
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Notifications.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Notifications
*/
fun getExternalAppNotificationsPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_NOTIFICATIONS
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Pictures.
*
* @return path of /storage/emulated/0/Android/data/package/files/Pictures
*/
fun getExternalAppPicturesPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_PICTURES
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Movies.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Movies
*/
fun getExternalAppMoviesPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_MOVIES
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Download.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Download
*/
fun getExternalAppDownloadPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_DOWNLOADS
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/DCIM.
*
* @return the path of /storage/emulated/0/Android/data/package/files/DCIM
*/
fun getExternalAppDcimPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.getExternalFilesDir(
Environment.DIRECTORY_DCIM
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/data/package/files/Documents.
*
* @return the path of /storage/emulated/0/Android/data/package/files/Documents
*/
fun getExternalAppDocumentsPath(application: Application): String? {
if (isExternalStorageDisable) return null
return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
application.getExternalFilesDir(null)?.absolutePath + "/Documents"
} else application.getExternalFilesDir(
Environment.DIRECTORY_DOCUMENTS
)?.absolutePath
}
/**
* Return the path of /storage/emulated/0/Android/obb/package.
*
* @return the path of /storage/emulated/0/Android/obb/package
*/
fun getExternalAppObbPath(application: Application): String? {
return if (isExternalStorageDisable) null else application.obbDir.absolutePath
}
private val isExternalStorageDisable: Boolean
private get() = Environment.MEDIA_MOUNTED != Environment.getExternalStorageState()
/**
* 判断sub是否在parent之下的文件或子文件夹<br></br>
*
* @param parent
* @param sub
* @return
*/
fun isSub(parent: File, sub: File): Boolean {
return try {
sub.absolutePath.startsWith(parent.absolutePath)
} catch (e: Exception) {
false
}
}
/**
* 获取子绝对路径与父绝对路径的相对路径
*
* @param parentPath
* @param subPath
* @return
*/
fun getRelativePath(parentPath: String?, subPath: String?): String? {
return try {
if (parentPath == null || subPath == null) {
return null
}
if (subPath.startsWith(parentPath)) {
subPath.substring(parentPath.length)
} else {
null
}
} catch (e: Exception) {
null
}
}
/**
* 拼接两个路径
*
* @param pathA 路径A
* @param pathB 路径B
* @return 拼接后的路径
*/
fun plusPath(pathA: String?, pathB: String?): String? {
if (pathA == null) {
return pathB
}
if (pathB == null) {
return pathA
}
return plusPathNotNull(pathA, pathB)
}
/**
* 拼接两个路径
*
* @param pathA 路径A
* @param pathB 路径B
* @return 拼接后的路径
*/
fun plusPathNotNull(pathA: String, pathB: String): String {
val pathAEndSeparator = pathA.endsWith(File.separator)
val pathBStartSeparator = pathB.startsWith(File.separator)
return if (pathAEndSeparator && pathBStartSeparator) {
pathA + pathB.substring(1)
} else if (pathAEndSeparator || pathBStartSeparator) {
pathA + pathB
} else {
pathA + File.separator + pathB
}
}
/**
* 获取后缀名称
* @param path 路径
* @return 后缀格式 .mp4 .gif 等
*/
fun getSuffixType(path: String): String? {
if (path.isEmpty()) {
return null
}
val dotIndex = path.indexOfLast {
'.' == it
}
val separatorIndex = path.indexOfLast {
'/' == it
}
if (dotIndex >= 0 && dotIndex > separatorIndex) {
val suffix = path.substring(dotIndex)
val askIndex = suffix.indexOfLast {
'?' == it
}
return if (askIndex >= 0) {
suffix.substring(0, askIndex)
} else {
suffix
}
}
return null
}
}

View File

@@ -0,0 +1,25 @@
package com.example.lib_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.example.lib_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,39 @@
package com.example.lib_utils
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.widget.EditText
class SolveEditTextScrollClash(private val editText: EditText) : OnTouchListener {
override fun onTouch(view: View, event: MotionEvent): Boolean {
//触摸的是EditText而且当前EditText能够滚动则将事件交给EditText处理。否则将事件交由其父类处理
if (view.id == editText.id && canVerticalScroll(editText)) {
view.parent.requestDisallowInterceptTouchEvent(true)
if (event.action == MotionEvent.ACTION_UP) {
view.parent.requestDisallowInterceptTouchEvent(false)
}
}
return false
}
/**
* EditText竖直方向能否够滚动
* @param editText 须要推断的EditText
* @return true能够滚动 false不能够滚动
*/
private fun canVerticalScroll(editText: EditText): Boolean {
//滚动的距离
val scrollY = editText.scrollY
//控件内容的总高度
val scrollRange = editText.layout.height
//控件实际显示的高度
val scrollExtent =
editText.height - editText.compoundPaddingTop - editText.compoundPaddingBottom
//控件内容总高度与实际显示高度的差值
val scrollDifference = scrollRange - scrollExtent
return if (scrollDifference == 0) {
false
} else scrollY > 0 || scrollY < scrollDifference - 1
}
}

View File

@@ -0,0 +1,68 @@
package com.example.lib_utils
import java.util.regex.Pattern
/**
* Created by Max on 2/10/21 4:56 PM
* Desc:字符串工具
*/
object StringUtils2 {
fun toInt(str: String?): Int {
return str?.toIntOrNull() ?: 0
}
/**
* 拆分字符串(根据匹配规则,按顺序拆分出来)
* @param pattern 匹配节点的规则模式
* @param onNormalNode<节点内容> 普通节点
* @param onMatchNode<节点内容> 匹配节点
*/
fun split(
content: String,
pattern: Pattern,
onNormalNode: (String) -> Unit,
onMatchNode: (String) -> Unit,
) {
try {
if (content.isEmpty()) {
onNormalNode.invoke(content)
return
}
val matcher = pattern.matcher(content)
// 最后一个匹配项的结束位置
var lastItemEnd = 0
var noMatch = true
while (matcher.find()) {
noMatch = false
// 匹配元素的开启位置
val start = matcher.start()
// 匹配元素的结束位置
val end = matcher.end()
// 匹配元素的文本
val text = matcher.group()
// 匹配元素的对应索引
// logD("split() start:$start ,end:$end ,text:$text")
if (start > lastItemEnd) {
// 普通节点
val nodeContent = content.substring(lastItemEnd, start)
onNormalNode.invoke(nodeContent)
}
// 匹配节点显示内容
onMatchNode.invoke(text)
lastItemEnd = end
}
if (lastItemEnd > 0 && lastItemEnd < content.length) {
// 最后的匹配项不是尾部(追加最后的尾部)
val nodeContent = content.substring(lastItemEnd, content.length)
onNormalNode.invoke(nodeContent)
}
if (noMatch) {
// 无匹配
onNormalNode.invoke(content)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}

View File

@@ -0,0 +1,148 @@
package com.example.lib_utils
import android.content.Context
import android.telephony.TelephonyManager
import com.example.lib_utils.log.ILog
/**
* Created by Max on 2023/11/14 10:17
* Desc:TelephonyManager 相关工具
**/
object TelephonyUtils : ILog {
/**
* 是否为中国运营商任意卡属于中国就为true
*/
fun isChinaOperator(): Boolean {
try {
val tm =
AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
?: return false
if (tm.simState == TelephonyManager.SIM_STATE_READY) {
if (!tm.simOperator.isNullOrEmpty() && tm.simOperator.startsWith("460")) {
return true
}
if (isChainOperator(tm.simOperatorName)) {
return true
}
}
if (!tm.networkOperator.isNullOrEmpty() && tm.networkOperator.startsWith("460")) {
return true
}
if (isChainOperator(tm.networkOperatorName)) {
return true
}
} catch (e: Exception) {
e.printStackTrace()
}
return false
}
/**
* 获取运营商优先SIM
*/
fun getOperatorFirstSim(): String? {
val operator = getSimOperator()
return if (operator.isNullOrEmpty()) {
getNetWorkOperator()
} else {
operator
}
}
/**
* 获取SIM运营商名称
*/
fun getSimOperator(): String? {
try {
val tm =
AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
?: return null
if (tm.simState != TelephonyManager.SIM_STATE_READY) {
logD("SIM状态不对${tm.simState}")
return null
}
val simOperator = tm.simOperator
logD("getSimOperator()获取的MCC+MNC为$simOperator")
logD("getOperatorName()方法获取的运营商名称为:${tm.simOperatorName} ")
logD("通过getSimOperator()人为判断的运营商名称是: ${getOperatorName(simOperator)}")
return simOperator
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* 获取网络运营商
*/
fun getNetWorkOperator(): String? {
try {
val tm =
AppUtils.getApp().getSystemService(Context.TELEPHONY_SERVICE) as? TelephonyManager
?: return null
//用于判断拨号那张卡的运营商
val networkOperator = tm.networkOperator
logD("getNetWorkOperator() 获取的MCC+MNC为$networkOperator")
logD("getNetWorkOperator() phoneType${tm.phoneType}")
logD("getNetworkOperatorName()方法获取的网络类型名称是: ${tm.networkOperatorName}")
logD("通过getNetWorkOperator()人为判断的运营商名称是: ${getOperatorName(networkOperator)}")
return tm.networkOperator
} catch (e: Exception) {
e.printStackTrace()
return null
}
}
/**
* 是否中国运营商
*/
private fun isChainOperator(operatorName: String?): Boolean {
if (operatorName == null) return false
if (operatorName == "CUCC"
|| operatorName == "CMCC"
|| operatorName == "CTCC"
|| operatorName == "CTT"
|| operatorName.contains("中国")
|| operatorName.contains("中國")
) {
return true
}
return false
}
/**
* 运营商类型
*/
private fun getOperatorName(simOperator: String?): String? {
if (simOperator == null) {
return null
}
return when (simOperator) {
"46001", "46006", "46009" -> {
// 联通
"CUCC"
}
"46000", "46002", "46004", "46007" -> {
// 移动
"CMCC"
}
"46003", "46005", "46011" -> {
// 电信
"CTCC"
}
"46020" -> {
// 铁通
"CTT"
}
else -> {
"OHTER"
}
}
}
}

View File

@@ -0,0 +1,79 @@
package com.example.lib_utils
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.os.LocaleList
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
import java.util.Locale
/**
* 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)
}
/**
* 是否从右到左布局
*/
fun isRtl(context: Context): Boolean {
val locale = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
context.resources.configuration.locales.get(0)
} else {
context.resources.configuration.locale
}
return TextUtilsCompat.getLayoutDirectionFromLocale(locale) == ViewCompat.LAYOUT_DIRECTION_RTL
}
}

View File

@@ -0,0 +1,70 @@
package com.example.lib_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,105 @@
package com.example.lib_utils.ktx
import android.text.Editable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
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,250 @@
package com.example.lib_utils.ktx
import android.content.*
import android.graphics.Bitmap
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import android.util.Log
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
import java.io.OutputStream
private const val TAG = "ImageToAlbumKtx"
private val ALBUM_DIR = Environment.DIRECTORY_PICTURES
private class OutputFileTaker(var file: File? = null)
/**
* 复制图片文件到相册的Pictures文件夹
*
* @param context 上下文
* @param fileName 文件名。 需要携带后缀
* @param relativePath 相对于Pictures的路径
*/
fun File.copyToAlbum(context: Context, fileName: String, relativePath: String?): Uri? {
if (!this.canRead() || !this.exists()) {
Log.w(TAG, "check: read file error: $this")
return null
}
return this.inputStream().use {
it.saveToAlbum(context, fileName, relativePath)
}
}
/**
* 保存图片Stream到相册的Pictures文件夹
*
* @param context 上下文
* @param fileName 文件名。 需要携带后缀
* @param relativePath 相对于Pictures的路径
*/
fun InputStream.saveToAlbum(context: Context, fileName: String, relativePath: String?): Uri? {
val resolver = context.contentResolver
val outputFile = OutputFileTaker()
val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile)
if (imageUri == null) {
Log.w(TAG, "insert: error: uri == null")
return null
}
(imageUri.outputStream(resolver) ?: return null).use { output ->
this.use { input ->
input.copyTo(output)
imageUri.finishPending(context, resolver, outputFile.file)
}
}
return imageUri
}
/**
* 保存Bitmap到相册的Pictures文件夹
*
* https://developer.android.google.cn/training/data-storage/shared/media
*
* @param context 上下文
* @param fileName 文件名。 需要携带后缀
* @param relativePath 相对于Pictures的路径
* @param quality 质量
*/
fun Bitmap.saveToAlbum(
context: Context,
fileName: String,
relativePath: String? = null,
quality: Int = 75,
): Uri? {
// 插入图片信息
val resolver = context.contentResolver
val outputFile = OutputFileTaker()
val imageUri = resolver.insertMediaImage(fileName, relativePath, outputFile)
if (imageUri == null) {
Log.w(TAG, "insert: error: uri == null")
return null
}
// 保存图片
(imageUri.outputStream(resolver) ?: return null).use {
val format = fileName.getBitmapFormat()
this@saveToAlbum.compress(format, quality, it)
imageUri.finishPending(context, resolver, outputFile.file)
}
return imageUri
}
private fun Uri.outputStream(resolver: ContentResolver): OutputStream? {
return try {
resolver.openOutputStream(this)
} catch (e: FileNotFoundException) {
Log.e(TAG, "save: open stream error: $e")
null
}
}
private fun Uri.finishPending(
context: Context,
resolver: ContentResolver,
outputFile: File?,
) {
val imageValues = ContentValues()
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
if (outputFile != null) {
imageValues.put(MediaStore.Images.Media.SIZE, outputFile.length())
}
resolver.update(this, imageValues, null, null)
// 通知媒体库更新
val intent = Intent(@Suppress("DEPRECATION") Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, this)
context.sendBroadcast(intent)
} else {
// Android Q添加了IS_PENDING状态为0时其他应用才可见
imageValues.put(MediaStore.Images.Media.IS_PENDING, 0)
resolver.update(this, imageValues, null, null)
}
}
private fun String.getBitmapFormat(): Bitmap.CompressFormat {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> Bitmap.CompressFormat.PNG
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> Bitmap.CompressFormat.JPEG
fileName.endsWith(".webp") -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
Bitmap.CompressFormat.WEBP_LOSSLESS else Bitmap.CompressFormat.WEBP
else -> Bitmap.CompressFormat.PNG
}
}
private fun String.getMimeType(): String? {
val fileName = this.lowercase()
return when {
fileName.endsWith(".png") -> "image/png"
fileName.endsWith(".jpg") || fileName.endsWith(".jpeg") -> "image/jpeg"
fileName.endsWith(".webp") -> "image/webp"
fileName.endsWith(".gif") -> "image/gif"
else -> null
}
}
/**
* 插入图片到媒体库
*/
private fun ContentResolver.insertMediaImage(
fileName: String,
relativePath: String?,
outputFileTaker: OutputFileTaker? = null,
): Uri? {
// 图片信息
val imageValues = ContentValues().apply {
val mimeType = fileName.getMimeType()
if (mimeType != null) {
put(MediaStore.Images.Media.MIME_TYPE, mimeType)
}
val date = System.currentTimeMillis() / 1000
put(MediaStore.Images.Media.DATE_ADDED, date)
put(MediaStore.Images.Media.DATE_MODIFIED, date)
}
// 保存的位置
val collection: Uri
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val path = if (relativePath != null) "${ALBUM_DIR}/${relativePath}" else ALBUM_DIR
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
put(MediaStore.Images.Media.RELATIVE_PATH, path)
put(MediaStore.Images.Media.IS_PENDING, 1)
}
collection = MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
// 高版本不用查重直接插入,会自动重命名
} else {
// 老版本
val pictures =
@Suppress("DEPRECATION") Environment.getExternalStoragePublicDirectory(ALBUM_DIR)
val saveDir = if (relativePath != null) File(pictures, relativePath) else pictures
if (!saveDir.exists() && !saveDir.mkdirs()) {
Log.e(TAG, "save: error: can't create Pictures directory")
return null
}
// 文件路径查重,重复的话在文件名后拼接数字
var imageFile = File(saveDir, fileName)
val fileNameWithoutExtension = imageFile.nameWithoutExtension
val fileExtension = imageFile.extension
var queryUri = this.queryMediaImage28(imageFile.absolutePath)
var suffix = 1
while (queryUri != null) {
val newName = fileNameWithoutExtension + "(${suffix++})." + fileExtension
imageFile = File(saveDir, newName)
queryUri = this.queryMediaImage28(imageFile.absolutePath)
}
imageValues.apply {
put(MediaStore.Images.Media.DISPLAY_NAME, imageFile.name)
// 保存路径
val imagePath = imageFile.absolutePath
Log.v(TAG, "save file: $imagePath")
put(@Suppress("DEPRECATION") MediaStore.Images.Media.DATA, imagePath)
}
outputFileTaker?.file = imageFile// 回传文件路径,用于设置文件大小
collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
}
// 插入图片信息
return this.insert(collection, imageValues)
}
/**
* Android Q以下版本查询媒体库中当前路径是否存在
* @return Uri 返回null时说明不存在可以进行图片插入逻辑
*/
private fun ContentResolver.queryMediaImage28(imagePath: String): Uri? {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) return null
val imageFile = File(imagePath)
if (imageFile.canRead() && imageFile.exists()) {
Log.v(TAG, "query: path: $imagePath exists")
// 文件已存在返回一个file://xxx的uri
return Uri.fromFile(imageFile)
}
// 保存的位置
val collection = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
// 查询是否已经存在相同图片
val query = this.query(
collection,
arrayOf(MediaStore.Images.Media._ID, @Suppress("DEPRECATION") MediaStore.Images.Media.DATA),
"${@Suppress("DEPRECATION") MediaStore.Images.Media.DATA} == ?",
arrayOf(imagePath), null
)
query?.use {
while (it.moveToNext()) {
val idColumn = it.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
val id = it.getLong(idColumn)
val existsUri = ContentUris.withAppendedId(collection, id)
Log.v(TAG, "query: path: $imagePath exists uri: $existsUri")
return existsUri
}
}
return null
}

View File

@@ -0,0 +1,216 @@
package com.example.lib_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.AttrRes
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes
import androidx.annotation.LayoutRes
import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.fragment.app.Fragment
import com.example.lib_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.getString(): String {
return AppUtils.getApp().getString(this)
}
fun Int.getString(vararg : Any): String {
return AppUtils.getApp().getString(this,vararg)
}
/**
* *any 使用 *来展开数组
*/
fun Int.getString(vararg any : Any): String {
return AppUtils.getApp().getString(this,*any)
}
fun Int.getDimension(): Float {
return AppUtils.getApp().resources.getDimension(this)
}
fun Int.getDimensionToInt(): Int {
return this.getDimension().toInt()
}
/**
* 获取资源drawable
* */
fun Int.getDrawable(): Drawable? {
return ContextCompat.getDrawable(AppUtils.getApp(), this)
}
/**
* 获取资源color
* */
fun Int.getColor(): 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.example.lib_utils.ktx
import com.example.lib_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.example.lib_utils.ktx
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.ColorMatrix
import android.graphics.ColorMatrixColorFilter
import android.graphics.Paint
import android.os.Build
import android.os.SystemClock
import android.util.Log
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.example.lib_utils.ShapeViewOutlineProvider
/**
* 展示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)
Log.v("点击","点击执行")
} else {
Log.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.example.lib_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.example.lib_utils.log
/**
* Created by Max on 2023/10/26 10:29
* Desc:日志快捷使用接口
*/
interface ILog {
companion object {
/**
* 清理退出APP时调用
*/
fun onCleared() {
}
}
/**
* 默认日志Tag
*/
fun getLogTag(): String {
return "LogUtil"
}
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.example.lib_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,106 @@
package com.example.lib_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) {
LogUtil.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)
}
@JvmStatic
fun d(tag: String, message: String, filePrinter: Boolean = false) {
log(Log.DEBUG, tag, message, filePrinter)
}
@JvmStatic
fun d(message: String, filePrinter: Boolean = false) {
log(Log.DEBUG, "LogUtil", 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.example.lib_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.example.lib_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,517 @@
package com.example.lib_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.example.lib_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
setTextStyle(
text,
start,
end,
textColor,
backgroundColor,
textSize,
textStyle,
underline,
null,
clickListener
)
return this
}
fun appendText(
text: String
): SpannableTextBuilder {
val start = spannableBuilder.length
spannableBuilder.append(text)
val end = spannableBuilder.length
setTextStyle(
text,
start,
end,
null,
null,
null,
null,
null,
null,
null
)
return this
}
fun setTextStyle(
text: String,
@ColorInt textColor: Int? = null,
@ColorInt backgroundColor: Int? = null,
textSize: Int? = null,
textStyle: Int? = null,
underline: Boolean? = null,
clickListener: ((String) -> Unit)? = null
): SpannableTextBuilder {
if (text.isEmpty()) {
return this
}
val start = spannableBuilder.indexOf(text)
if (start == -1) {
return this
}
val end = start + text.length
return setTextStyle(
text,
start,
end,
textColor,
backgroundColor,
textSize,
textStyle,
underline,
null,
clickListener
)
}
fun setTextStyle(
text: String,
@ColorInt textColor: Int? = null,
@ColorInt backgroundColor: Int? = null,
textSize: Int? = null,
textStyle: Int? = null,
underline: Boolean? = null,
delLine: Boolean? = null,
textStart: Int = 0,
clickListener: ((String) -> Unit)? = null
): SpannableTextBuilder {
if (text.isEmpty()) {
return this
};
val start = spannableBuilder.indexOf(text, textStart)
if (start == -1) {
return this
}
val end = start + text.length
return setTextStyle(
text,
start,
end,
textColor,
backgroundColor,
textSize,
textStyle,
underline,
delLine,
clickListener
)
}
/**
* 添加图片
* @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(
com.example.lib_utils.spannable.IconTextSpan(
textView.context,
backgroundColor,
text,
textColor,
textSize,
round,
marginRight,
2.dp
),
start, end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
return this
}
private fun setTextStyle(
text: String,
start: Int,
end: Int,
@ColorInt textColor: Int? = null,
@ColorInt backgroundColor: Int? = null,
textSize: Int? = null,
textStyle: Int? = null,
underline: Boolean? = null,
delLine: Boolean? = null,
clickListener: ((String) -> Unit)? = null
): SpannableTextBuilder {
if (start < 0 || end > spannableBuilder.length) {
return this
}
// 文本颜色
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 (delLine == true) {
spannableBuilder.setSpan(
StrikethroughSpan(),
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
}
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
}
}
public class TextStyleBean {
var text: String = ""
@ColorInt
var textColor: Int? = null
@ColorInt
var backgroundColor: Int? = null
var textSize: Int? = null
var textStyle: Int? = null
var underline: Boolean? = null
var delLine: Boolean? = null
var textStart: Int = 0
var clickListener: ((String) -> Unit)? = null
}
//按添加顺序 匹配,上一个匹配的结束索引位置,是下一个的起始位置
fun addTextStyleList(list: List<TextStyleBean>) : SpannableTextBuilder{
var start = 0;
list.forEach {
val indexStart = start
val findIndex = spannableBuilder.toString().indexOf(it.text, indexStart)
val end = findIndex + it.text.length
start = end
setTextStyle(
it.text,
findIndex,
end,
it.textColor,
it.backgroundColor,
it.textSize,
it.textStyle,
it.underline,
it.delLine,
it.clickListener
)
}
return this
}
//全局匹配
fun addTextStyleListAll(list: List<TextStyleBean>) : SpannableTextBuilder{
list.forEach {
val indexStart = 0
val findIndex = spannableBuilder.toString().indexOf(it.text, indexStart)
val end = findIndex + it.text.length
setTextStyle(
it.text,
findIndex,
end,
it.textColor,
it.backgroundColor,
it.textSize,
it.textStyle,
it.underline,
it.delLine,
it.clickListener
)
}
return this
}
}
/**
* 快速构建生成器
*/
fun TextView.spannableBuilder(): SpannableTextBuilder {
return SpannableTextBuilder(this)
}

View File

@@ -0,0 +1,66 @@
package com.example.lib_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()
}
}