feat : libs
This commit is contained in:
1
libs/lib_utils/.gitignore
vendored
Normal file
1
libs/lib_utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/build
|
||||
10
libs/lib_utils/build.gradle
Normal file
10
libs/lib_utils/build.gradle
Normal 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'
|
||||
}
|
||||
0
libs/lib_utils/consumer-rules.pro
Normal file
0
libs/lib_utils/consumer-rules.pro
Normal file
21
libs/lib_utils/proguard-rules.pro
vendored
Normal file
21
libs/lib_utils/proguard-rules.pro
vendored
Normal 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
|
||||
4
libs/lib_utils/src/main/AndroidManifest.xml
Normal file
4
libs/lib_utils/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
403
libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java
Normal file
403
libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java
Normal 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);
|
||||
}
|
||||
}
|
||||
1294
libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java
Normal file
1294
libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,14 @@
|
||||
package com.example.lib_utils
|
||||
|
||||
|
||||
/**
|
||||
* Created by Max on 2023/10/26 11:50
|
||||
* Desc:清除释放统一接口
|
||||
**/
|
||||
interface ICleared {
|
||||
|
||||
/**
|
||||
* 清除/释放
|
||||
*/
|
||||
fun onCleared() {}
|
||||
}
|
||||
521
libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt
Normal file
521
libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt
Normal 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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user