初始代码提交

This commit is contained in:
2026-01-14 10:36:27 +08:00
parent 600aa4dbb0
commit 196cacfe5a
268 changed files with 22545 additions and 2 deletions
@@ -0,0 +1,751 @@
package com.sw.inbound.utils;
import static android.content.Context.TELEPHONY_SERVICE;
import android.annotation.SuppressLint;
import android.bluetooth.BluetoothAdapter;
import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager;
import android.os.Build;
import android.provider.Settings;
import android.telephony.TelephonyManager;
import android.text.TextUtils;
import android.util.Log;
import androidx.core.content.FileProvider;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.net.NetworkInterface;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.UUID;
public class AppUtil {
public static String getAppPackageName(Context context) {
String packageName = "";
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
packageName = pi.packageName;
if (AppUtil.isEmpty(packageName)) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return packageName;
}
public static String getAppVersionName(Context context) {
String versionName = "";
// int versioncode=1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versionName = pi.versionName;
// versioncode = pi.versionCode;表示更新了多少次
if (versionName == null || versionName.length() <= 0) {
return "";
}
} catch (Exception e) {
e.printStackTrace();
}
return versionName;
}
public static int getAppVersionCode(Context context) {
int versioncode = 1;
try {
PackageManager pm = context.getPackageManager();
PackageInfo pi = pm.getPackageInfo(context.getPackageName(), 0);
versioncode = pi.versionCode;
} catch (Exception e) {
e.printStackTrace();
}
return versioncode;
}
//判断微信是否安装
public static boolean isWeixinInstalled(Context context) {
final PackageManager packageManager = context.getPackageManager();// 获取packagemanager
List<PackageInfo> pinfo = packageManager.getInstalledPackages(0);// 获取所有已安装程序的包信息
if (pinfo != null) {
for (int i = 0; i < pinfo.size(); i++) {
String pn = pinfo.get(i).packageName;
if (pn.equals("com.tencent.mm")) {
return true;
}
}
}
return false;
}
/**
* 打电话
* <p>
* Intent.ACTION_DIAL Intent.ACTION_CALL
*
* @param context
* @param mobile
*/
public static void callUp(Context context, String mobile) {
Intent intent = new Intent(Intent.ACTION_DIAL, Uri.parse("tel:"
+ mobile));
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
context.startActivity(intent);
}
/**
* 获取设备ID
*
* @param context
* @return
*/
public static String getDevId(Context context) {
TelephonyManager TelephonyMgr = (TelephonyManager) context
.getSystemService(Context.TELEPHONY_SERVICE);
return TelephonyMgr.getDeviceId();
}
/**
* 姓名脱敏
*
* @param fullName
* @return
*/
public static String desensitizedName(String fullName) {
if (fullName == null || fullName.length() <= 1) {
return fullName;
}
char[] nameArr = fullName.toCharArray();
if (nameArr.length > 2) {
for (int i = 1; i < nameArr.length - 1; i++) {
nameArr[i] = '*';
}
} else {
nameArr[1] = '*';
}
return new String(nameArr);
}
public static String formatDateGetFull(String date) {
if (isEmpty(date)) {
return "";
}
Date d = new Date(Long.parseLong(date));
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm");
return dateFormat1.format(d);
}
public static String formatDateGetCurrentTime() {
Date d = new Date(System.currentTimeMillis());
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");//.SSS
return dateFormat1.format(d);
}
public static String formatDateGetFull(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
return dateFormat1.format(d);
}
public static String formatDateGetDay(long date) {
if (date == 0) {
return "";
}
Date d = new Date(date);
SimpleDateFormat dateFormat1 = new SimpleDateFormat("yyyy-MM-dd");
return dateFormat1.format(d);
}
public static boolean isEmpty(String s) {
if (TextUtils.isEmpty(s) || s.trim().equals("null")) {
return true;
} else {
return false;
}
}
/**
* 格式化浮点型
*
* @param data
* @return
*/
public static String formatDouble(double data) {
return new DecimalFormat("0.00").format(data);
}
/**
* 格式化分钟
*
* @param minutes
* @return
*/
public static String formatMinutes(int minutes) {
int hour = minutes / 60;
int minute = minutes % 60;
if (hour > 0 && minute > 0) {
return hour + "小时" + minute + "分钟";
} else if (hour > 0) {
return hour + "小时";
} else {
return minute + "分钟";
}
}
//com.fawan.news
public static void goToMarket(Context context, String packageName) {
Uri uri = Uri.parse("market://details?id=" + packageName);
Intent goToMarket = new Intent(Intent.ACTION_VIEW, uri);
try {
context.startActivity(goToMarket);
} catch (ActivityNotFoundException e) {
e.printStackTrace();
}
}
/**
* true为存在,false为不存在
*
* @param context
* @param packageName
* @return
*/
public static boolean isInstallApp(Context context, String packageName) {
try {
context.getPackageManager().getApplicationInfo(packageName, PackageManager.GET_UNINSTALLED_PACKAGES);
return true;
} catch (PackageManager.NameNotFoundException e) {
return false;
}
}
/**
* 格式化float 保留两位小数
*
* @param data
* @return
*/
public static float formatFloat2(float data) {
// DecimalFormat decimalFormat = new DecimalFormat("0.00");//构造方法的字符格式这里如果小数不足2位,会以0补足.
// return decimalFormat.format(data);//返回字符串
int scale = 1;//设置位数
int roundingMode = 4;//表示四舍五入,可以选择其他舍值方式,例如去尾,等等.
BigDecimal bd = new BigDecimal((double) data);
bd = bd.setScale(scale, roundingMode);
data = bd.floatValue();
return data;
}
/**
* Android 6.0 之前(不包括6.0)获取mac地址
* 必须的权限 <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
*
* @param context * @return
*/
public static String getMacDefault(Context context) {
String mac = "";
if (context == null) {
return mac;
}
WifiManager wifi = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
WifiInfo info = null;
try {
info = wifi.getConnectionInfo();
} catch (Exception e) {
e.printStackTrace();
}
if (info == null) {
return null;
}
mac = info.getMacAddress();
if (!TextUtils.isEmpty(mac)) {
mac = mac.toUpperCase(Locale.ENGLISH);
}
return mac;
}
/**
* Android 6.0-Android 7.0 获取mac地址
*/
public static String getMacAddress() {
String macSerial = null;
String str = "";
try {
Process pp = Runtime.getRuntime().exec("cat/sys/class/net/wlan0/address");
InputStreamReader ir = new InputStreamReader(pp.getInputStream());
LineNumberReader input = new LineNumberReader(ir);
while (null != str) {
str = input.readLine();
if (str != null) {
macSerial = str.trim();//去空格
break;
}
}
} catch (IOException ex) {
// 赋予默认值
ex.printStackTrace();
}
return macSerial;
}
/**
* Android 7.0之后获取Mac地址
* 遍历循环所有的网络接口,找到接口是 wlan0
* 必须的权限 <uses-permission android:name="android.permission.INTERNET"></uses-permission>
*
* @return
*/
public static String getMacFromHardware() {
try {
ArrayList<NetworkInterface> all = Collections.list(NetworkInterface.getNetworkInterfaces());
for (NetworkInterface nif : all) {
if (!nif.getName().equals("wlan0"))
continue;
byte[] macBytes = nif.getHardwareAddress();
if (macBytes == null) return "";
StringBuilder res1 = new StringBuilder();
for (Byte b : macBytes) {
res1.append(String.format("%02X:", b));
}
if (!TextUtils.isEmpty(res1)) {
res1.deleteCharAt(res1.length() - 1);
}
return res1.toString();
}
} catch (Exception e) {
e.printStackTrace();
}
return "";
}
/**
* 获取mac地址(适配所有Android版本)
*
* @return
*/
public static String getMac(Context context) {
String mac = "";
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
mac = getMacDefault(context);
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
mac = getMacAddress();
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
mac = getMacFromHardware();
}
return mac;
}
//把String转化为float
public static double convertToFloat(String number, double defaultValue) {
if (TextUtils.isEmpty(number)) {
return defaultValue;
}
try {
return Double.parseDouble(number);
} catch (Exception e) {
return defaultValue;
}
}
/**
* 获取AndroidId
*
* @param context
* @return
*/
public static String getAndroidId(Context context) {
String androidId = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
return androidId;
}
/**
* 获取设备唯一 UDID
*
* @param context
* @return
*/
@SuppressLint("MissingPermission")
public static String getUDID(Context context) {
// String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
// L.e("androidID===" + androidID);
// return androidID;
String androidID = Settings.Secure.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
if (!androidID.equals("")) {
try {
if (!"9774d56d682e549c".equals(androidID)) {
androidID = UUID.nameUUIDFromBytes(androidID.getBytes("utf8")).toString();
} else {
@SuppressLint("MissingPermission") final String deviceId = ((TelephonyManager) context.getSystemService(TELEPHONY_SERVICE)).getDeviceId();
androidID = deviceId != null ? UUID.nameUUIDFromBytes(deviceId.getBytes("utf8")).toString() : UUID.randomUUID().toString();
}
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
return androidID;
}
//需要权限 android.permission.READ_PHONE_STATE
TelephonyManager TelephonyMgr = (TelephonyManager) context.getSystemService(TELEPHONY_SERVICE);
String szImei = TelephonyMgr.getDeviceId();
if (!szImei.equals("")) {
return szImei;
}
//需要权限 android.permission.ACCESS_WIFI_STATE
WifiManager wm = (WifiManager) context.getSystemService(Context.WIFI_SERVICE);
String m_szWLANMAC = wm.getConnectionInfo().getMacAddress();
if (!m_szWLANMAC.equals("")) {
return m_szWLANMAC;
}
//需要权限 android.permission.BLUETOOTH
BluetoothAdapter m_BluetoothAdapter = null;
m_BluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
String m_szBTMAC = m_BluetoothAdapter.getAddress();
if (!m_szBTMAC.equals("")) {
return m_szBTMAC;
}
return getUniquePsuedoID();
}
//获得 Psuedo ID
public static String getUniquePsuedoID() {
String serial = null;
String m_szDevIDShort = "35" +
Build.BOARD.length() % 10 + Build.BRAND.length() % 10 +
Build.CPU_ABI.length() % 10 + Build.DEVICE.length() % 10 +
Build.DISPLAY.length() % 10 + Build.HOST.length() % 10 +
Build.ID.length() % 10 + Build.MANUFACTURER.length() % 10 +
Build.MODEL.length() % 10 + Build.PRODUCT.length() % 10 +
Build.TAGS.length() % 10 + Build.TYPE.length() % 10 +
Build.USER.length() % 10; //13 位
try {
serial = Build.class.getField("SERIAL").get(null).toString();
//API>=9 使用serial号
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
} catch (Exception exception) {
//serial需要一个初始化,随意值
serial = "serial";
}
//使用硬件信息拼凑出来的15位号码
return new UUID(m_szDevIDShort.hashCode(), serial.hashCode()).toString();
}
public static String getCPUSerial() {
String line = "";
String TAG = "aaa";
Log.e(TAG, " get_quck_Sn() ");
Class<?> c = null;
try {
c = Class.forName("android.os.SystemProperties");
Method get = c.getMethod("get", String.class);
line = (String) get.invoke(c, "ro.serialno");
} catch (ClassNotFoundException | NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
Log.e(TAG, " get_quck_Sn() " + line);
System.out.println("设备串号" + line);
return line;
}
/**
* 判断网络连接状态
*
* @param context
* @return
*/
public static boolean isNetworkConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null) {
return mNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断WiFi连接状态
*
* @param context
* @return
*/
public static boolean isWifiConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mWiFiNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI);
if (mWiFiNetworkInfo != null) {
return mWiFiNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 判断移动网络状态
*
* @param context
* @return
*/
public static boolean isMobileConnected(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mMobileNetworkInfo = mConnectivityManager.getNetworkInfo(ConnectivityManager.TYPE_MOBILE);
if (mMobileNetworkInfo != null) {
return mMobileNetworkInfo.isAvailable();
}
}
return false;
}
/**
* 获取网络连接类型
*
* @param context
* @return
*/
public static int getConnectedType(Context context) {
if (context != null) {
ConnectivityManager mConnectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
NetworkInfo mNetworkInfo = mConnectivityManager.getActiveNetworkInfo();
if (mNetworkInfo != null && mNetworkInfo.isAvailable()) {
return mNetworkInfo.getType();
}
}
return -1;
}
/**
* 根据字符的起始和结束索引提取子串
*
* @param input 原始字符串
* @param startIndex 起始索引(包含,从0开始)
* @param endIndex 结束索引(不包含)
* @return 子串,若输入无效或索引越界则返回空字符串
*/
public static String getSubstringByIndices(String input, int startIndex, int endIndex) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(endIndex, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
public static String getSubstringByIndex(String input, int startIndex, int length) {
if (input == null) {
return "";
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return "";
}
return input.substring(safeStart, safeEnd);
}
/**
* 十进制转十六进制
*
* @param decimal
* @return
*/
public static String decimalToHexWithPadding(int decimal, int padding) {
// 将十进制转换为十六进制,并转换为字符串
String hex = Integer.toHexString(decimal);
// 确保字符串长度为至少4位,不足部分前面补0
while (hex.length() < padding) {
hex = "0" + hex;
}
return hex.toUpperCase(); // 返回大写形式的十六进制字符串
}
/**
* 十进制转二进制,且返回的二进制为至少7位数
*
* @param decimal
* @return
*/
public static String decimalToBinary(int decimal) {
// 如果输入为0,直接返回"0"
if (decimal == 0) {
return "0";
}
StringBuilder binary = new StringBuilder();
// 除2取余法,将余数加入二进制字符串
while (decimal > 0) {
int remainder = decimal % 2;
binary.insert(0, remainder);
decimal = decimal / 2;
}
int length = binary.length();
if (length < 7) {
int padding = 7 - length;
for (int i = 0; i < padding; i++) {
binary.insert(0, '0');
}
}
return binary.toString();
}
/**
* 将二进制字符串转换为十六进制字符串,每8位转换为两位十六进制,不足两位前面补零
*
* @param binaryStr 输入的二进制字符串(仅包含0和1)
* @return 转换后的十六进制字符串
* @throws IllegalArgumentException 如果输入不是有效的二进制字符串
*/
public static String binaryToHex(String binaryStr) {
// 校验输入合法性
if (binaryStr == null || !binaryStr.matches("[01]+")) {
throw new IllegalArgumentException("Invalid binary string");
}
// 补前导零使长度成为8的倍数
int length = binaryStr.length();
int padding = (8 - (length % 8)) % 8; // 计算需要补零的数量
StringBuilder paddedBinary = new StringBuilder();
for (int i = 0; i < padding; i++) {
paddedBinary.append('0');
}
paddedBinary.append(binaryStr);
// 每8位转换为两位十六进制
StringBuilder hexStr = new StringBuilder();
for (int i = 0; i < paddedBinary.length(); i += 8) {
String byteStr = paddedBinary.substring(i, i + 8);
int decimalValue = Integer.parseInt(byteStr, 2);
hexStr.append(String.format("%02X", decimalValue & 0xFF));
}
return hexStr.toString();
}
/**
* 数据校验 异或处理
*/
public static String getXor(String content) {
int a = 0;
for (int i = 0; i < content.length() / 2; i++) {
a = a ^ Integer.parseInt(content.substring(i * 2, (i * 2) + 2), 16);
}
String result = Integer.toHexString(a).toUpperCase();
if (result.length() == 1) {
return "0" + result;
} else {
return result;
}
}
public static double formatPersonInfo(String input, int startIndex, int length) {
if (input == null) {
return 0;
}
// 处理索引越界问题
int safeStart = Math.max(startIndex, 0);
int safeEnd = Math.min(startIndex + length, input.length());
if (safeStart > safeEnd) {
return 0;
}
String result = input.substring(safeStart, safeEnd);
double num = Integer.parseInt(result, 16);
return num;
}
/**
* 安装apk
*
* @param activity
* @param apkFile
*/
public static void installApk(Context activity, File apkFile) {
//文件有所有者概念,现在是属于当前进程的,需要把这个文件暴露给系统安装程序(其他进程)去安装
//因此,可能会存在权限问题,需要做下面的设置
//如果文件是sdcard上的,就不需要这个操作了
try {
apkFile.setExecutable(true, false);
apkFile.setReadable(true, false);
apkFile.setWritable(true, false);
} catch (Exception e) {
e.printStackTrace();
}
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.setAction(Intent.ACTION_VIEW);
Uri uri;
//TODO N FileProvider
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
uri = FileProvider.getUriForFile(activity, activity.getPackageName() + ".fileProvider", apkFile);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
} else {
uri = Uri.fromFile(apkFile);
}
intent.setDataAndType(uri, "application/vnd.android.package-archive");
activity.startActivity(intent);
//TODO 0 INSTALL PERMISSION
//在AndroidManifest中加入权限即可
}
}
@@ -0,0 +1,76 @@
package com.sw.inbound.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
object AssetsTool {
// fun readJson(context: Context, fileName: String): List<String> {
// val json = readAssetsFile(context, fileName)
// val json2 = json.replace("[[", "[").replaceAfterLast("]]", "]")
// val list = json2.split("],")
// val resultList = mutableListOf<String>()
// var count = 0
// val tempList = mutableListOf<String>()
// list.forEachIndexed { index, text ->
// if (count >= 500) {
// val tempJson = "[${tempList.joinToString (",")}]"
// resultList.add(tempJson)
// tempList.clear()
// count = 0
// }
// val newText = if (index == list.size - 1) text else "${text}]"
// tempList.add(newText)
// count++
// }
// if (count < 500) {
// val tempJson = "[${tempList.joinToString(",")}]"
// resultList.add(tempJson)
// }
// return resultList
// }
fun readAssetsFile(context: Context, fileName: String): String {
val stringBuilder = StringBuilder()
try {
val bf = BufferedReader(InputStreamReader(context.assets.open(fileName)))
bf.useLines { lines -> lines.forEach { stringBuilder.append(it) } }
} catch (e: IOException) {
e.printStackTrace()
}
return stringBuilder.toString()
}
fun loadImagesFromAssets(context: Context, subPath: String): MutableList<Bitmap> {
val bitmaps: MutableList<Bitmap> = mutableListOf()
val assetManager = context.assets
try {
val files = assetManager.list(subPath)
files?.forEach { file ->
assetManager.open("$subPath/$file").use { `is` ->
val bitmap = BitmapFactory.decodeStream(`is`)
if (bitmap != null) {
bitmaps.add(bitmap)
}
}
}
} catch (e: IOException) {
e.printStackTrace()
}
return bitmaps
}
fun loadImageBitmapFromAssets(context: Context, imagePath:String, action:(bmp: Bitmap)-> Unit) {
context.assets.open(imagePath).use { `is` ->
val bitmap = BitmapFactory.decodeStream(`is`)
action(bitmap)
}
}
}
@@ -0,0 +1,42 @@
package com.sw.inbound.utils
import android.graphics.Bitmap
import android.util.DisplayMetrics
object BitmapCropper {
/**
* 裁剪Bitmap中心区域为指定尺寸
* @param original 原始Bitmap
* @param targetWidth 目标宽度
* @param targetHeight 目标高度
* @return 裁剪后的Bitmap
*/
fun cropCenter(original: Bitmap, targetWidth: Int, targetHeight: Int, offsetX:Int = 0, offsetY:Int = 0): Bitmap {
val originalWidth = original.width
val originalHeight = original.height
// 计算中心点坐标
var startX = (originalWidth - targetWidth) / 2 + offsetX
var startY = (originalHeight - targetHeight) / 2 + offsetY
// 边界检查
startX = startX.coerceAtLeast(0)
startY = startY.coerceAtLeast(0)
val actualWidth = minOf(targetWidth, originalWidth - startX)
val actualHeight = minOf(targetHeight, originalHeight - startY)
return Bitmap.createBitmap(original, startX, startY, actualWidth, actualHeight).also {
// it.setConfig(Bitmap.Config.RGB_565)
// it.density = DisplayMetrics.DENSITY_LOW
}
}
}
// 使用示例
//fun main() {
// // 假设这是从资源加载的Bitmap
// val originalBitmap = Bitmap.createBitmap(1000, 1000, Bitmap.Config.ARGB_8888)
// // 裁剪中心500x500区域
// val croppedBitmap = BitmapCropper.cropCenter(originalBitmap, 500, 500)
// println("裁剪后尺寸:${croppedBitmap.width}x${croppedBitmap.height}")
//}
@@ -0,0 +1,58 @@
package com.sw.inbound.utils
import android.content.Context
import android.graphics.Bitmap
import android.os.Environment
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
object BitmapSaver {
// 保存到公共目录(需WRITE_EXTERNAL_STORAGE权限)
fun saveToPublicDirectory(
bitmap: Bitmap,
folderName: String = Environment.DIRECTORY_PICTURES,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.PNG,
quality: Int = 100
): File? {
val dir = Environment.getExternalStoragePublicDirectory(folderName)
if (!dir.exists()) dir.mkdirs()
return saveBitmap(bitmap, File(dir, fileName), format, quality)
}
// 保存到应用私有目录(无需权限)
fun saveToAppFilesDir(
bitmap: Bitmap,
context: Context,
fileName: String,
format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG,
quality: Int = 100
): File? {
//val dir = context.getExternalFilesDir(null)
val dir = context.cacheDir
val cropFile = File(dir, "crop")
if (cropFile.exists().not()) {
cropFile.mkdirs()
}
return saveBitmap(bitmap, File(cropFile, fileName), format, quality)
}
private fun saveBitmap(
bitmap: Bitmap,
outputFile: File,
format: Bitmap.CompressFormat,
quality: Int
): File? {
return try {
FileOutputStream(outputFile).use { fos ->
bitmap.compress(format, quality, fos)
fos.flush()
}
outputFile
} catch (e: IOException) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,81 @@
package com.sw.inbound.utils
import android.net.Uri
import androidx.activity.ComponentActivity
import androidx.camera.core.CameraSelector
import androidx.camera.view.CameraController
import androidx.camera.view.LifecycleCameraController
import androidx.camera.view.PreviewView
class CameraUtils(private var activity: ComponentActivity) {
private var cameraController: LifecycleCameraController? = null
private var photoCaptureHelper: PhotoCaptureHelper? = null
// private var isCameraReady = false
fun takePhoto(callback: (Uri) -> Unit) {
cameraController?.let {
if (photoCaptureHelper == null) {
initCaptureHelper()
}
}
photoCaptureHelper?.let {
it.addSuccessCallback(callback)
it.bindCameraCallback {
bind()
}
it.takePhoto()
}
}
private fun initCaptureHelper() {
photoCaptureHelper = PhotoCaptureHelper(
context = activity,
cameraController = cameraController!!,
onSuccess = {},
onError = { msg ->
//toast(msg)
}
)
}
fun setPreviewController(previewView: PreviewView?) {
if (previewView?.controller == null) {
previewView?.controller = cameraController
}
}
fun initCamera() {
if (cameraController == null) {
cameraController = LifecycleCameraController(activity).apply {
// 必须设置有效的用例
setEnabledUseCases(
CameraController.IMAGE_CAPTURE
// or CameraController.VIDEO_CAPTURE
)
cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA
}
bind()
}
// if (isCameraReady.not()) {
// try {
// cameraController!!.initializationFuture.addListener({
// isCameraReady = true
// Timber.d("Camera initialized successfully")
// }, ContextCompat.getMainExecutor(this))
// } catch (e: Exception) {
// Timber.d("Camera initialized error = ${e.message}")
// }
// }
}
fun bind() {
cameraController?.bindToLifecycle(activity)
}
fun unbind() {
cameraController?.unbind()
}
}
@@ -0,0 +1,50 @@
package com.sw.inbound.utils
import android.content.Context
import android.view.View
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
/**
* Context 工具类
*/
object ContextUtils {
// ========== Composable 内获取 ==========
/**
* 获取当前 Composable 的 Activity Context
* 只能在 @Composable 函数中调用
*/
@Composable
fun getActivityContext(): Context {
return LocalContext.current
}
/**
* 获取当前 Composable 的 View
*/
@Composable
fun getLocalView(): View {
return LocalView.current
}
// ========== 非 Composable 环境获取 ==========
/**
* 通过静态 Application 引用获取
* 需要在 Application 类中初始化
*/
private var _applicationContext: Context? = null
fun initAppContext(context: Context) {
_applicationContext = context.applicationContext
}
fun getAppContext(): Context {
return _applicationContext ?: throw IllegalStateException(
"Application context not initialized. Call initAppContext() first."
)
}
}
@@ -0,0 +1,252 @@
package com.sw.inbound.utils
import android.app.ActivityManager
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.os.Process
import com.sw.inbound.MainActivity
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.io.StringWriter
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlin.system.exitProcess
/**
* 崩溃处理
*/
class CrashHandler private constructor(private val context: Context) :
Thread.UncaughtExceptionHandler {
companion object {
private const val TAG = "CrashHandler"
private const val CRASH_REPORTS_DIR = "crash_reports"
private const val LOG_LINES = 500 // 收集最近500行日志
@Volatile
private var instance: CrashHandler? = null
fun init(context: Context) {
if (instance == null) {
synchronized(CrashHandler::class.java) {
if (instance == null) {
instance = CrashHandler(context.applicationContext)
}
}
}
}
//fun getCrashReportFiles(context: Context): Array<File> {
// val crashDir = getCrashDir()
// return if (crashDir.exists() && crashDir.isDirectory) {
// crashDir.listFiles { _, name -> name.endsWith(".log") } ?: emptyArray()
// } else {
// emptyArray()
// }
//}
//
//fun clearCrashReports(context: Context) {
// getCrashReportFiles(context).forEach { it.delete() }
//}
}
private val defaultHandler = Thread.getDefaultUncaughtExceptionHandler()
init {
Thread.setDefaultUncaughtExceptionHandler(this)
}
override fun uncaughtException(thread: Thread, ex: Throwable) {
handleException(thread, ex)
try {
Thread.sleep(3000)
} catch (e: InterruptedException) {
e.printStackTrace()
}
// 如果系统提供了默认的异常处理器,则交给系统去结束程序
// 否则自己结束程序
defaultHandler?.uncaughtException(thread, ex) ?: run {
Process.killProcess(Process.myPid())
exitProcess(1)
}
}
/**
* 自动重启app
*/
private fun restartApp() {
// 延迟1秒后重启应用
Handler(Looper.getMainLooper()).postDelayed({
val intent = Intent(context, MainActivity::class.java).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val pendingIntent = PendingIntent.getActivity(
context, 0, intent,
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.set(AlarmManager.RTC, System.currentTimeMillis() + 100, pendingIntent)
Process.killProcess(Process.myPid())
exitProcess(1)
}, 1000)
}
private fun handleException(thread: Thread, ex: Throwable) {
// 收集设备信息和异常信息
val crashInfo = collectCrashInfo(thread, ex)
// 保存日志文件
saveCrashInfoToFile(crashInfo)
// 这里可以添加其他处理逻辑,比如上传到服务器等
}
private fun collectCrashInfo(thread: Thread, ex: Throwable): String {
return buildString {
// 收集设备信息
collectDeviceInfo(this)
// 收集应用日志
append("\n\n").append(collectLogs())
// 收集线程和异常信息
append("\n\n========== Thread & Exception Info ==========\n")
append("Thread: ${thread.name}\n")
append("Stack Trace:\n")
val sw = StringWriter()
val pw = PrintWriter(sw)
ex.printStackTrace(pw)
var cause: Throwable? = ex.cause
while (cause != null) {
cause.printStackTrace(pw)
cause = cause.cause
}
pw.close()
append(sw.toString())
}
}
private fun collectDeviceInfo(sb: StringBuilder) {
sb.append("========== Device Info ==========\n")
try {
// 应用信息
val pm = context.packageManager
val pi = pm.getPackageInfo(context.packageName, 0)
sb.append("App Version: ${pi.versionName}_${pi.versionCode}\n")
// Android 版本信息
sb.append("OS Version: ${Build.VERSION.RELEASE}_${Build.VERSION.SDK_INT}\n")
// 设备信息
sb.append("Vendor: ${Build.MANUFACTURER}\n")
sb.append("Model: ${Build.MODEL}\n")
sb.append("CPU ABI: ${Build.SUPPORTED_ABIS[0]}\n")
// 其他信息
sb.append("Locale: ${Locale.getDefault()}\n")
sb.append(
"Current Time: ${
SimpleDateFormat(
"yyyy-MM-dd HH:mm:ss",
Locale.getDefault()
).format(Date())
}\n"
)
// 内存信息
val memoryInfo = ActivityManager.MemoryInfo()
val activityManager =
context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
activityManager.getMemoryInfo(memoryInfo)
sb.append("Available Memory: ${memoryInfo.availMem / (1024 * 1024)}MB\n")
sb.append("Total Memory: ${memoryInfo.totalMem / (1024 * 1024)}MB\n")
sb.append("Low Memory: ${memoryInfo.lowMemory}\n")
} catch (e: Exception) {
Timber.e(e, "Error while collecting device info")
sb.append("Error while collecting device info: ${e.message}\n")
}
}
private fun collectLogs(): String {
return buildString {
append("========== Application Logs ==========\n")
try {
val process = Runtime.getRuntime().exec("logcat -d -v threadtime")
val reader = process.inputStream.bufferedReader()
val logLines = reader.readLines()
val start = maxOf(0, logLines.size - LOG_LINES)
logLines.subList(start, logLines.size).forEach {
append(it).append("\n")
}
} catch (e: IOException) {
Timber.e(e, "Error collecting logs")
append("Error collecting logs: ${e.message}\n")
}
}
}
private fun saveCrashInfoToFile(crashInfo: String) {
try {
val time = SimpleDateFormat("yyyy-MM-dd_HH-mm-ss", Locale.getDefault()).format(Date())
val fileName = "crash_$time.log"
val crashDir = getCrashDir()
val crashFile = File(crashDir, fileName)
FileOutputStream(crashFile).use { it.write(crashInfo.toByteArray()) }
Timber.tag(TAG).d("Crash info saved to: ${crashFile.absolutePath}")
} catch (e: Exception) {
Timber.tag(TAG).e(e, "Error saving crash info to file")
}
}
/**
* 清理旧的崩溃日志
*/
fun cleanupOldCrashReports(maxAgeDays: Int = 7) {
val crashDir = getCrashDir()
if (!crashDir.exists() || !crashDir.isDirectory) return
val now = System.currentTimeMillis()
val maxAgeMillis = maxAgeDays * 24 * 60 * 60 * 1000L
crashDir.listFiles()?.forEach { file ->
if (file.lastModified() < now - maxAgeMillis) {
file.delete()
}
}
}
private fun getCrashDir():File {
//val crashDir = File(context.getExternalFilesDir(null), CRASH_REPORTS_DIR)
var crashDir = File(context.filesDir, CRASH_REPORTS_DIR)
if (!crashDir.exists()) {
crashDir.mkdirs()
}
if (!(crashDir.exists())) {
crashDir = File(context.cacheDir, CRASH_REPORTS_DIR)
}
return crashDir
}
}
@@ -0,0 +1,105 @@
package com.sw.inbound.utils;
import android.annotation.SuppressLint;
import android.content.Context;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.widget.TextView;
import android.widget.Toast;
import com.sw.inbound.MyApp;
import com.sw.inbound.R;
import java.util.Timer;
import java.util.TimerTask;
/**
* The type Toast utils.
*/
public class CustomToastUtils {
private static Toast toast;
@SuppressLint("StaticFieldLeak")
private static TextView textCenterView;
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text) {
Context context = MyApp.Companion.getInstance();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setDuration(Toast.LENGTH_SHORT);
toast.setView(view);
}
textCenterView.setText(text);
toast.show();
}
/**
* Show center toast.
*
* @param text the text
*/
public static void showToast(String text, int duration) {
Context context = MyApp.Companion.getInstance();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.CENTER, 0, 20);
toast.setView(view);
}
textCenterView.setText(text);
toast.setDuration(duration);
toast.show();
}
public static void showLongToast(String text) {
Context context = MyApp.Companion.getInstance();
if (toast == null) {
View view = LayoutInflater.from(context).inflate(R.layout.layout_custom_toast, null);
textCenterView = view.findViewById(R.id.toast_tv);
toast = new Toast(context);
toast.setGravity(Gravity.TOP, 0, 20);
toast.setDuration(Toast.LENGTH_LONG);
toast.setView(view);
}
textCenterView.setText(text);
showMyToast(toast, 1000000 * 30);
}
//自定义Toast控件
private static void showMyToast(final Toast toast, final int cnt) {
final Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
toast.show();
}
}, 0, Toast.LENGTH_LONG);
new Timer().schedule(new TimerTask() {
@Override
public void run() {
toast.cancel();
timer.cancel();
}
}, cnt);
}
public static void showSystemToast(String text) {
Context context = MyApp.Companion.getInstance();
Toast.makeText(context, text, Toast.LENGTH_SHORT).show();
}
}
@@ -0,0 +1,37 @@
package com.sw.inbound.utils
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.flow
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 时间格式化工具类
*/
object DateTimeUtils {
/**
* 获取完整中文日期格式(示例:2025年6月11日 星期三)
*/
fun getChineseDateString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE", Locale.CHINA).format(date)
}
/**
* 获取带时间的完整中文格式(示例:2025年6月11日 星期三 14:30
*/
fun getChineseDateTimeString(date: Date = Date()): String {
return SimpleDateFormat("yyyy年M月d日 EEEE HH:mm:ss", Locale.CHINA).format(date)
}
/**
* 实时时间流(每秒更新)
*/
fun realTimeChineseDateFlow() = flow {
while (true) {
emit(getChineseDateString())
delay(1000)
}
}
}
@@ -0,0 +1,159 @@
package com.sw.inbound.utils
import android.content.ContentUris
import android.content.Context
import android.net.Uri
import android.os.Build
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.MultipartBody
import okhttp3.RequestBody.Companion.asRequestBody
import timber.log.Timber
import java.io.File
object FileUtils {
/**
* 从Uri获取File
* example: file:///data/user/0/com.sw.inbound/cache/IMG_17515262353556856678814444882273.jpg
*/
private fun getFileFromUri(context: Context, uri: Uri): File? {
Timber.d("getFileFromUri uri = ${uri.scheme}")
return when (uri.scheme) {
"file" -> File(uri.path ?: return null)
"content" -> {
try {
val inputStream = context.contentResolver.openInputStream(uri) ?: return null
val cacheDir = context.cacheDir
val file = File.createTempFile(
"upload_${System.currentTimeMillis()}",
".jpg",
cacheDir
)
file.outputStream().use { output ->
inputStream.copyTo(output)
}
file
} catch (e: Exception) {
Timber.e(e)
null
}
}
else -> null
}
}
/**
* 通过uri生成http请求体
*/
fun genRequestPart(context: Context, imageUri: Uri): MultipartBody.Part? {
Timber.d("genRequestPart imageUri = $imageUri")
// 1. 从Uri获取文件
val file = getFileFromUri(context, imageUri)
return genRequestPart(file)
}
fun genRequestPart(file: File?): MultipartBody.Part? {
if (file == null) {
Timber.e("getFileFromUri file is null")
return null
}
// 2. 创建请求体
val requestFile = file
.asRequestBody("application/octet-stream".toMediaTypeOrNull())
val imagePart = MultipartBody.Part.createFormData(
"file",
file.name,
requestFile
)
return imagePart
}
/**
* 通过Uri删除文件
* @param context 上下文
* @param uri 文件Uri
* @return Boolean 是否删除成功
*/
fun deleteFileWithUri(context: Context, uri: Uri): Boolean {
Timber.d("deleteFileWithUri uri = ${uri.scheme}")
return when {
// 1. 处理 content:// 类型的Uri (MediaStore)
uri.scheme.equals("content", ignoreCase = true) -> {
deleteContentUriFile(context, uri)
}
// 2. 处理 file:// 类型的Uri
uri.scheme.equals("file", ignoreCase = true) -> {
deleteFileUriFile(uri)
}
// 3. 其他情况尝试直接解析路径
else -> {
deleteFileFromPath(uri.path ?: return false)
}
}
}
// 删除Content Uri文件
private fun deleteContentUriFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteContentUriFile uri = ${uri.scheme}")
return try {
context.contentResolver.delete(uri, null, null) > 0
} catch (e: SecurityException) {
// Android 10+需要特殊处理
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
deleteMediaStoreFile(context, uri)
} else {
false
}
} catch (e: Exception) {
Timber.e(e)
false
}
}
// Android 10+删除MediaStore文件
@RequiresApi(Build.VERSION_CODES.Q)
private fun deleteMediaStoreFile(context: Context, uri: Uri): Boolean {
Timber.d("deleteMediaStoreFile uri = ${uri.scheme}")
val contentResolver = context.contentResolver
val projection = arrayOf(MediaStore.MediaColumns._ID)
return try {
contentResolver.query(uri, projection, null, null, null)?.use { cursor ->
if (cursor.moveToFirst()) {
val id =
cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
val contentUri = ContentUris.withAppendedId(uri, id)
contentResolver.delete(contentUri, null, null) > 0
} else {
false
}
} ?: false
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 删除File Uri文件
private fun deleteFileUriFile(uri: Uri): Boolean {
Timber.d("deleteFileUriFile uri = $uri")
return try {
File(uri.path ?: return false).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
// 直接通过路径删除文件
private fun deleteFileFromPath(path: String): Boolean {
Timber.d("deleteFileFromPath path = $path")
return try {
File(path).delete()
} catch (e: Exception) {
Timber.e(e)
false
}
}
}
@@ -0,0 +1,123 @@
package com.sw.inbound.utils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type
object GsonUtils {
// 默认的 Gson 实例
private val defaultGson: Gson by lazy {
GsonBuilder()
.setDateFormat("yyyy-MM-dd HH:mm:ss") // 设置日期格式
// .disableHtmlEscaping() // 禁止转义HTML标签
.create()
}
/**
* 获取默认配置的 Gson 实例
*/
fun getGson(): Gson = defaultGson
/**
* 将对象转换为 JSON 字符串
* @param obj 要转换的对象
* @return JSON 字符串
*/
fun toJson(obj: Any?): String {
return if (obj == null) "" else defaultGson.toJson(obj)
}
/**
* 将 JSON 字符串转换为对象
* @param json JSON 字符串
* @param clazz 目标类
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, clazz: Class<T>): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, clazz)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为对象 (支持泛型)
* @param json JSON 字符串
* @param type 类型令牌,用于获取泛型类型
* @return 转换后的对象
*/
fun <T> fromJson(json: String?, type: Type): T? {
if (json.isNullOrEmpty()) {
return null
}
return try {
defaultGson.fromJson(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 List 对象
* @param json JSON 字符串
* @param clazz List 中的元素类型
* @return 转换后的 List 对象
*/
fun <T> fromJsonList(json: String?, clazz: Class<T>): List<T>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(List::class.java, clazz).type
defaultGson.fromJson<List<T>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将 JSON 字符串转换为 Map 对象
* @param json JSON 字符串
* @param keyClazz Map 的 key 类型
* @param valueClazz Map 的 value 类型
* @return 转换后的 Map 对象
*/
fun <K, V> fromJsonMap(
json: String?,
keyClazz: Class<K>,
valueClazz: Class<V>
): Map<K, V>? {
if (json.isNullOrEmpty()) {
return null
}
return try {
val type = TypeToken.getParameterized(Map::class.java, keyClazz, valueClazz).type
defaultGson.fromJson<Map<K, V>>(json, type)
} catch (e: Exception) {
e.printStackTrace()
null
}
}
/**
* 将对象转换为另一种类型的对象
* @param obj 源对象
* @param clazz 目标类型
* @return 转换后的对象
*/
fun <T> convert(obj: Any?, clazz: Class<T>): T? {
if (obj == null) {
return null
}
return fromJson(toJson(obj), clazz)
}
}
@@ -0,0 +1,40 @@
package com.sw.inbound.utils
import com.sw.inbound.objbox.FoodCollectionBean
import java.util.concurrent.atomic.AtomicInteger
import kotlin.collections.chunked
import kotlin.collections.count
class ImageUploader(
private val totalList: List<FoodCollectionBean>,
private val uploadImage: suspend (List<FoodCollectionBean>)-> Boolean,
private val onProgress: (Int, List<FoodCollectionBean>) -> Unit,
private val onError:(List<FoodCollectionBean>) -> Unit,
private val onComplete:() -> Unit
) {
companion object {
private const val BATCH_SIZE = 5
}
private val uploadedCount = AtomicInteger(0)
suspend fun processUploads() {
val batches = totalList.chunked(BATCH_SIZE)
for (batch in batches) {
val success = uploadImage(batch)
if (!success) {
//println("上传失败,终止流程")
onError(batch)
return
}
val fileCount = batch.count { it.imageFile!=null }
uploadedCount.addAndGet(fileCount)
onProgress(uploadedCount.get(), batch)
//println("已上传 ${uploadedCount.get()}/$totalImages")
}
onComplete()
//println("流程完成,总计上传 ${uploadedCount.get()} 张图片")
}
}
@@ -0,0 +1,26 @@
package com.sw.inbound.utils
import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
object ImageUtil {
fun uriToBitmap(context: Context, uri: Uri): Bitmap? {
return try {
val options = BitmapFactory.Options()
//options.inSampleSize = 2; // 这会将图片的尺寸缩小到原来的1/2
options.inJustDecodeBounds = false
// options.inPreferredConfig = Bitmap.Config.ARGB_8888
options.inPreferredConfig = Bitmap.Config.RGB_565
context.contentResolver.openInputStream(uri)?.use { stream ->
BitmapFactory.decodeStream(stream, null, options)
// BitmapFactory.decodeStream(stream)
}
} catch (e: Exception) {
e.printStackTrace()
null
}
}
}
@@ -0,0 +1,223 @@
package com.sw.inbound.utils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Jetpack Compose 交互工具集
* 包含快速点击过滤、防抖、节流、双击检测、长按检测等功能
*/
object InteractionUtils {
// ======================== 点击过滤 ========================
/**
* 快速点击过滤器
* @param minInterval 最小点击间隔时间(毫秒),默认500ms
*/
class ClickFilter(private val minInterval: Long = 500L) {
private var lastClickTime: Long = 0
/**
* 处理点击事件
* @return Boolean 是否允许此次点击(true=允许,false=拦截)
*/
fun processClick(): Boolean {
val currentTime = System.currentTimeMillis()
return if (currentTime - lastClickTime > minInterval) {
lastClickTime = currentTime
true
} else {
false
}
}
/**
* 处理点击事件(带回调)
*/
fun processClick(block: () -> Unit) {
if (processClick()) {
block()
}
}
}
/**
* 记住点击过滤器
*/
@Composable
fun rememberClickFilter(minInterval: Long = 500L): ClickFilter {
return remember { ClickFilter(minInterval) }
}
// ======================== 防抖处理 ========================
/**
* 防抖工具类
* @param delayMillis 防抖延迟时间(毫秒)
*/
class Debouncer(private val delayMillis: Long) {
private var lastActionTime = 0L
/**
* 执行防抖操作
* @param block 要执行的代码块
* @return Boolean 是否实际执行了操作
*/
fun debounce(block: () -> Unit): Boolean {
val currentTime = System.currentTimeMillis()
if (currentTime - lastActionTime >= delayMillis) {
lastActionTime = currentTime
block()
return true
}
return false
}
}
// ======================== 双击检测 ========================
/**
* 双击检测器
*/
class DoubleClickDetector(
private val timeout: Long = 500L,
private val onSingleClick: () -> Unit = {},
private val onDoubleClick: () -> Unit
) {
private var clickCount by mutableIntStateOf(0)
private var lastClickTime by mutableLongStateOf(0L)
/**
* 处理点击事件
*/
fun processClick(coroutineScope: CoroutineScope) {
val currentTime = System.currentTimeMillis()
if (currentTime - lastClickTime < timeout) {
clickCount++
if (clickCount == 2) {
onDoubleClick()
clickCount = 0
}
} else {
clickCount = 1
coroutineScope.launch {
delay(timeout)
if (clickCount == 1) {
onSingleClick()
}
clickCount = 0
}
}
lastClickTime = currentTime
}
}
/**
* 记住双击检测器
*/
@Composable
fun rememberDoubleClickDetector(
timeout: Long = 300L,
onSingleClick: () -> Unit = {},
onDoubleClick: () -> Unit
): () -> Unit {
val detector = remember { DoubleClickDetector(timeout, onSingleClick, onDoubleClick) }
val scope = rememberCoroutineScope()
return {
detector.processClick(scope)
}
}
// ======================== 长按检测 ========================
/**
* 长按检测器
*/
class LongPressDetector(
private val delay: Long = 1000L,
private val onLongPress: () -> Unit,
private val onClick: () -> Unit = {}
) {
private var pressJob: Job? = null
/**
* 处理按压事件
*/
fun handlePress(coroutineScope: CoroutineScope) {
pressJob = coroutineScope.launch {
delay(delay)
onLongPress()
}
}
/**
* 处理释放事件
*/
fun handleRelease() {
pressJob?.cancel()
pressJob = null
onClick()
}
}
/**
* 记住长按检测器
*/
@Composable
fun rememberLongPressDetector(
delay: Long = 1000L,
onLongPress: () -> Unit,
onClick: () -> Unit = {}
): Pair<() -> Unit, () -> Unit> {
val detector = remember { LongPressDetector(delay, onLongPress, onClick) }
val scope = rememberCoroutineScope()
return Pair(
first = { detector.handlePress(scope) },
second = { detector.handleRelease() }
)
}
// ======================== 组合工具 ========================
/**
* 带状态的按钮控制器
*/
class StatefulButtonController {
var isLoading by mutableStateOf(false)
private val clickFilter = ClickFilter()
/**
* 处理按钮点击
*/
suspend fun handleClick(block: suspend () -> Unit) {
if (clickFilter.processClick()) {
isLoading = true
try {
block()
} finally {
isLoading = false
}
}
}
}
/**
* 记住带状态的按钮控制器
*/
@Composable
fun rememberStatefulButtonController(): StatefulButtonController {
return remember { StatefulButtonController() }
}
}
@@ -0,0 +1,49 @@
package com.sw.inbound.utils
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.view.View
import java.util.Arrays
class MultiClickDetector(private val targetCount: Int = 10,
private val intervalMs: Long = 50) {
private var clickCount = 0
private val clickTimestamps = LongArray(targetCount)
private val handler = Handler(Looper.getMainLooper())
private lateinit var callback: () -> Unit
fun setOnMultiClickListener(view: View, action: () -> Unit) {
callback = action
view.setOnClickListener {
System.arraycopy(clickTimestamps, 1, clickTimestamps, 0, targetCount - 1)
clickTimestamps[targetCount - 1] = SystemClock.uptimeMillis()
if (clickTimestamps[0] >= SystemClock.uptimeMillis() - intervalMs) {
callback.invoke()
resetCount()
}
}
}
// 使用Handler的延迟检测方案
fun setOnDelayedMultiClickListener(view: View, action: () -> Unit) {
callback = action
view.setOnClickListener {
clickCount++
handler.removeCallbacks(resetTask)
handler.postDelayed(resetTask, intervalMs)
if (clickCount >= targetCount) {
callback.invoke()
resetCount()
}
}
}
private val resetTask = Runnable { resetCount() }
private fun resetCount() {
clickCount = 0
Arrays.fill(clickTimestamps, 0L)
}
}
@@ -0,0 +1,121 @@
package com.sw.inbound.utils
import android.content.Context
import android.net.Uri
import androidx.camera.core.ImageCapture
import androidx.camera.core.ImageCaptureException
import androidx.camera.view.CameraController
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import timber.log.Timber
import java.io.File
/**
* 拍照工具类
* @param context Context 上下文
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
*/
class PhotoCaptureHelper(
private val context: Context,
private val cameraController: CameraController,
private val onSuccess: (Uri) -> Unit = {},
private val onError: (String) -> Unit = {}
) {
private val callbackList: MutableList<(Uri) -> Unit> = mutableListOf()
fun addSuccessCallback(callback:(Uri) -> Unit) {
if (callbackList.contains(callback).not()) {
callbackList.add(callback)
}
}
private var bindCamera:(()->Unit)?=null
fun bindCameraCallback(callback:()->Unit) {
this.bindCamera = callback
}
/**
* 拍照方法
* @param fileNamePrefix 文件名前缀,默认为"IMG_"
* @param fileExtension 文件扩展名,默认为".jpg"
*/
fun takePhoto(
fileNamePrefix: String = "IMG_",
fileExtension: String = ".jpg"
) {
Timber.d("开始拍照采集")
try {
val executor = ContextCompat.getMainExecutor(context)
val cacheDir = context.cacheDir
val photoFile = File.createTempFile(
"${fileNamePrefix}${System.currentTimeMillis()}",
fileExtension,
cacheDir
)
val outputOptions = ImageCapture.OutputFileOptions.Builder(photoFile).build()
cameraController.takePicture(
outputOptions,
executor,
object : ImageCapture.OnImageSavedCallback {
override fun onImageSaved(outputFileResults: ImageCapture.OutputFileResults) {
val photoUri = outputFileResults.savedUri ?: Uri.fromFile(photoFile)
Timber.d("照片保存成功: $photoUri")
onSuccess(photoUri)
callbackList.forEach {
it(photoUri)
}
}
override fun onError(exception: ImageCaptureException) {
val errorMsg = "拍照失败: ${exception.message}"
if (errorMsg.contains("Not bound to a valid Camera")) {
if (bindCamera != null) {
bindCamera?.invoke()
//takePhoto()
}
}
Timber.e(exception, errorMsg)
onError(errorMsg)
}
}
)
} catch (e: Exception) {
val errorMsg = "创建临时文件失败: ${e.message}"
Timber.e(e, errorMsg)
onError(errorMsg)
}
}
}
/**
* 用于Compose的拍照Hook
* @param cameraController CameraController 相机控制器
* @param onSuccess (Uri) -> Unit 拍照成功回调
* @param onError (String) -> Unit 拍照失败回调
* @return Pair<PhotoCaptureHelper, () -> Unit> 返回工具类实例和拍照函数
*/
@Composable
fun rememberPhotoCapture(
cameraController: CameraController,
onSuccess: (Uri) -> Unit = {},
onError: (String) -> Unit = {}
): Pair<PhotoCaptureHelper, () -> Unit> {
val context = LocalContext.current
val photoCaptureHelper = remember {
PhotoCaptureHelper(
context = context,
cameraController = cameraController,
onSuccess = onSuccess,
onError = onError
)
}
return Pair(photoCaptureHelper) { photoCaptureHelper.takePhoto() }
}
@@ -0,0 +1,83 @@
package com.sw.inbound.utils
import android.os.Handler
import android.os.Looper
import android.os.SystemClock
import android.util.Log
class PreciseDelayHandler {
val TAG = "PreciseDelayHandler"
private val handler = Handler(Looper.getMainLooper())
private var isRunning = false
private var endTime = 0L
/**
* 精确延迟执行
* @param delayMillis 总延迟时间(毫秒)
* @param runnable 要执行的任务
*/
fun precisePostDelayed(delayMillis: Long, block:()-> Unit) {
isRunning = true
endTime = SystemClock.uptimeMillis() + delayMillis
val ticker = object : Runnable {
override fun run() {
if (!isRunning) return
val now = SystemClock.uptimeMillis()
val interval = now - endTime
if (interval >= 0) {
block()
isRunning = false
} else {
// 计算下一个整百毫秒时间点进行校准
val next = now + (100 - now % 100)
handler.postAtTime(this, next)
}
}
}
handler.post(ticker)
}
/**
* 精确循环执行
* @param interval 执行间隔(毫秒)
* @param runnable 要执行的任务
*/
fun preciseLoop(interval: Long, block:()-> Unit) {
isRunning = true
val ticker = object : Runnable {
override fun run() {
if (!isRunning) return
val startTime = SystemClock.uptimeMillis()
block()
val elapsed = SystemClock.uptimeMillis() - startTime
val remainingDelay = interval - elapsed
if (remainingDelay > 0) {
handler.postDelayed(this, remainingDelay)
} else {
handler.post(this)
}
}
}
handler.post(ticker)
}
/**
* 取消所有延迟任务
*/
fun cancelAll() {
isRunning = false
handler.removeCallbacksAndMessages(null)
}
/**
* 获取是否正在运行
*/
fun isRunning(): Boolean = isRunning
}
@@ -0,0 +1,103 @@
package com.sw.inbound.utils
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import com.google.zxing.BarcodeFormat
import com.google.zxing.EncodeHintType
import com.google.zxing.WriterException
import com.google.zxing.common.BitMatrix
import com.google.zxing.qrcode.QRCodeWriter
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
import kotlin.apply
import kotlin.ranges.until
import kotlin.text.isEmpty
/**
* 二维码生成工具类
*/
object QRCodeUtil {
/**
* 生成二维码(默认大小)
* @param content 二维码内容
* @return 生成的二维码Bitmap
*/
@JvmOverloads
fun generateQRCode(content: String, size: Int = 500): Bitmap? {
return generateQRCode(content, size, Color.BLACK, Color.WHITE)
}
/**
* 生成二维码(自定义颜色)
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param colorCode 二维码颜色
* @param backgroundColor 背景颜色
* @return 生成的二维码Bitmap
*/
fun generateQRCode(
content: String,
size: Int,
colorCode: Int,
backgroundColor: Int
): Bitmap? {
if (content.isEmpty()) {
return null
}
return try {
val hints = mutableMapOf<EncodeHintType, Any>().apply {
put(EncodeHintType.CHARACTER_SET, "UTF-8")
put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.H) // 纠错级别
put(EncodeHintType.MARGIN, 1) // 边距
}
val bitMatrix = QRCodeWriter().encode(
content,
BarcodeFormat.QR_CODE,
size,
size,
hints
)
val pixels = IntArray(size * size).apply {
for (y in 0 until size) {
for (x in 0 until size) {
this[y * size + x] = if (bitMatrix.get(x, y)) colorCode else backgroundColor
}
}
}
Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
setPixels(pixels, 0, size, 0, 0, size, size)
}
} catch (e: WriterException) {
e.printStackTrace()
null
}
}
/**
* 生成带Logo的二维码
* @param content 二维码内容
* @param size 二维码边长(像素)
* @param logo Logo Bitmap
* @return 带Logo的二维码Bitmap
*/
fun generateQRCodeWithLogo(content: String, size: Int, logo: Bitmap?): Bitmap? {
val qrCode = generateQRCode(content, size) ?: return null
logo ?: return qrCode
val logoSize = size / 5 // Logo大小约为二维码的1/5
val scaledLogo = Bitmap.createScaledBitmap(logo, logoSize, logoSize, false)
val offset = (size - logoSize) / 2
return Bitmap.createBitmap(size, size, Bitmap.Config.ARGB_8888).apply {
val canvas = Canvas(this)
canvas.drawBitmap(qrCode, 0f, 0f, null)
canvas.drawBitmap(scaledLogo, offset.toFloat(), offset.toFloat(), null)
}
}
}
@@ -0,0 +1,59 @@
package com.sw.inbound.utils
/**
* 去除小数点后无效零的工具类
* 功能示例:
* 1230.00 => 1230
* 1230.10 => 1230.1
* 3.40 => 3.4
* 3.0 => 3
*/
class RemoveZeroUtils {
companion object {
/**
* 方法1:使用字符串格式化(最简单)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByFormat(number: Double): String {
return "%.10f".format(number) // 先格式化为固定小数位
.replace(Regex("0*$"), "") // 移除末尾的零
.replace(Regex("\\.$"), "") // 如果小数点后全为零,移除小数点
}
/**
* 方法2:使用DecimalFormat(推荐)
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByDecimalFormat(number: Double): String {
val format = java.text.DecimalFormat("0.##########")
format.roundingMode = java.math.RoundingMode.FLOOR
return format.format(number)
}
/**
* 方法3:使用正则表达式处理字符串
* @param number 输入的double数值
* @return 去除无效零后的字符串
*/
fun removeZeroByRegex(number: Double): String {
var str = number.toString()
// 如果包含小数点,处理末尾的零
if (str.contains(".")) {
str = str.replace(Regex("0+?$"), "") // 移除末尾的零
.replace(Regex("[.]$"), "") // 如果小数点后全为零,移除小数点
}
return str
}
}
}
// 扩展函数方式,更符合Kotlin风格
fun Double?.removeTrailingZeros(): String {
if (this == null) return ""
return RemoveZeroUtils.removeZeroByDecimalFormat(this)
}
@@ -0,0 +1,110 @@
package com.sw.inbound.utils
import android.content.Context
import androidx.core.content.edit
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class SPUtil private constructor(context: Context, private val spName: String) {
companion object {
@Volatile
private var instance: SPUtil? = null
fun getInstance(
context: Context = ContextUtils.getAppContext(),
spName: String = "default_sp"
): SPUtil {
return instance ?: synchronized(this) {
instance ?: SPUtil(context.applicationContext, spName).also { instance = it }
}
}
}
private val sharedPreferences by lazy {
context.getSharedPreferences(spName, Context.MODE_PRIVATE)
}
// 基础存储方法
fun put(key: String, value: Any?) {
when (value) {
null -> remove(key) // 存入null视为删除
is String -> sharedPreferences.edit { putString(key, value) }
is Int -> sharedPreferences.edit { putInt(key, value) }
is Long -> sharedPreferences.edit { putLong(key, value) }
is Float -> sharedPreferences.edit { putFloat(key, value) }
is Boolean -> sharedPreferences.edit { putBoolean(key, value) }
is Set<*> -> sharedPreferences.edit { putStringSet(key, value as Set<String>) }
else -> throw IllegalArgumentException("Unsupported type: ${value.javaClass.name}")
}
notifyDataChanged(key)
}
@Suppress("UNCHECKED_CAST")
fun <T> get(key: String, defaultValue: T? = null): T? {
return when (defaultValue) {
is String -> sharedPreferences.getString(key, defaultValue) as T
is Int -> sharedPreferences.getInt(key, defaultValue) as T
is Long -> sharedPreferences.getLong(key, defaultValue) as T
is Float -> sharedPreferences.getFloat(key, defaultValue) as T
is Boolean -> sharedPreferences.getBoolean(key, defaultValue) as T
is Set<*> -> sharedPreferences.getStringSet(key, defaultValue as Set<String>) as T
null -> when {
sharedPreferences.contains(key) -> get(key, "") as? T // 尝试作为String获取
else -> null
}
else -> throw IllegalArgumentException("Unsupported type: ${defaultValue.javaClass.name}")
}
}
fun remove(key: String) {
if (sharedPreferences.contains(key)) {
sharedPreferences.edit { remove(key) }
notifyDataChanged(key)
}
}
fun clear() {
sharedPreferences.edit { clear() }
notifyDataChanged(null)
}
fun contains(key: String): Boolean {
return sharedPreferences.contains(key)
}
// 监听变化
private val dataChangeFlow = MutableStateFlow(0)
private fun notifyDataChanged(key: String?) {
dataChangeFlow.value++
}
fun observeKey(key: String): Flow<Any?> {
return dataChangeFlow.map { get(key) }
}
// 属性委托支持
fun int(key: String, default: Int = 0) = SpProperty(key, default)
fun long(key: String, default: Long = 0L) = SpProperty(key, default)
fun float(key: String, default: Float = 0f) = SpProperty(key, default)
fun boolean(key: String, default: Boolean = false) = SpProperty(key, default)
fun string(key: String, default: String = "") = SpProperty(key, default)
fun stringSet(key: String, default: Set<String> = emptySet()) = SpProperty(key, default)
inner class SpProperty<T>(private val key: String, private val defaultValue: T) :
ReadWriteProperty<Any?, T> {
override fun getValue(thisRef: Any?, property: KProperty<*>): T {
return get(key, defaultValue) ?: defaultValue
}
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
put(key, value)
}
}
}
@@ -0,0 +1,124 @@
package com.sw.inbound.utils
import android.os.Handler
import android.os.Looper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
import kotlin.coroutines.CoroutineContext
/**
* 多功能线程工具类
* 结合协程、Handler和线程池实现线程切换
*/
object ThreadUtils : CoroutineScope {
// 主线程Handler
private val mainHandler by lazy { Handler(Looper.getMainLooper()) }
// 后台线程池(IO密集型任务)
private val ioThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2)
}
// CPU密集型线程池
private val cpuThreadPool: ExecutorService by lazy {
Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())
}
// 协程Job管理
private val job = Job()
override val coroutineContext: CoroutineContext
get() = Dispatchers.Main + job
// ========== Handler相关方法 ==========
/**
* 在主线程执行任务
* @param delayMillis 延迟时间(毫秒)
*/
fun runOnUiThread(delayMillis: Long = 0, block: () -> Unit) {
if (delayMillis > 0) {
mainHandler.postDelayed(block, delayMillis)
} else {
if (isOnMainThread()) {
block()
} else {
mainHandler.post(block)
}
}
}
/**
* 移除主线程任务
*/
fun removeUiThreadTask(block: () -> Unit) {
mainHandler.removeCallbacks(block)
}
// ========== 线程池相关方法 ==========
/**
* 在IO线程执行任务
*/
fun runOnIoThread(block: () -> Unit) {
ioThreadPool.execute(block)
}
/**
* 在CPU计算线程执行任务
*/
fun runOnCpuThread(block: () -> Unit) {
cpuThreadPool.execute(block)
}
// ========== 协程相关方法 ==========
/**
* 启动协程(默认在主线程)
*/
fun launch(block: suspend CoroutineScope.() -> Unit): Job {
return launch(coroutineContext, block = block)
}
/**
* 在IO线程启动协程
*/
fun launchOnIo(block: suspend CoroutineScope.() -> Unit): Job {
return launch(Dispatchers.IO, block = block)
}
/**
* 切换到主线程(协程环境)
*/
suspend fun <T> switchToMain(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.Main, block)
}
/**
* 切换到IO线程(协程环境)
*/
suspend fun <T> switchToIo(block: suspend CoroutineScope.() -> T): T {
return withContext(Dispatchers.IO, block)
}
/**
* 是否在主线程
*/
fun isOnMainThread(): Boolean {
return Looper.myLooper() == Looper.getMainLooper()
}
/**
* 释放资源
*/
fun release() {
job.cancel()
ioThreadPool.shutdown()
cpuThreadPool.shutdown()
}
}
@@ -0,0 +1,56 @@
package com.sw.inbound.utils
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlinx.coroutines.delay
object ToastUtils {
private var show by mutableStateOf(false)
private var message by mutableStateOf("")
fun showToast(msg: String) {
message = msg
show = true
// Toast.makeText(ContextUtils.getAppContext(), msg, Toast.LENGTH_LONG).show()
}
@Composable
fun ToastComposable() {
if (show) {
LaunchedEffect(Unit) {
delay(2000) // 自动2秒后消失
show = false
}
Box(
modifier = Modifier
// .fillMaxWidth()
.fillMaxSize()
.padding(bottom = 156.dp),
contentAlignment = Alignment.BottomCenter
) {
Text(
text = message,
modifier = Modifier
.background(Color.Black.copy(alpha = 0.7f), RoundedCornerShape(8.dp))
.padding(horizontal = 24.dp, vertical = 12.dp),
color = Color.White,
fontSize = 24.sp
)
}
}
}
}
@@ -0,0 +1,251 @@
package com.sw.inbound.utils.ext
import android.annotation.SuppressLint
import android.app.Activity
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.res.Resources
import android.graphics.Color
import android.graphics.drawable.GradientDrawable
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import androidx.core.app.ActivityOptionsCompat
import androidx.core.graphics.toColorInt
import androidx.fragment.app.Fragment
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.sw.inbound.utils.CustomToastUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import java.math.BigDecimal
import java.math.RoundingMode
fun Context.toast(
message: String?,
duration: Int = Toast.LENGTH_SHORT,
) {
if (message.isNullOrBlank()) {
return
}
Handler(Looper.getMainLooper()).post {
//Toast.makeText(this, message, duration).show()
CustomToastUtils.showToast(message, duration)
}
}
fun Fragment.toast(
message: String?,
duration: Int = 2000
) {
activity?.toast(message, duration)
}
fun View.visible() {
visibility = View.VISIBLE
}
fun View.invisible() {
visibility = View.INVISIBLE
}
fun View.gone() {
visibility = View.GONE
}
inline fun <reified T : Activity> Context.startActivity(
bundle: Bundle? = null,
options: ActivityOptionsCompat? = null
) {
Intent(this, T::class.java).apply {
bundle?.let { putExtras(it) }
if (options != null && this@startActivity is Activity) {
startActivity(this, options.toBundle())
} else {
startActivity(this)
}
}
}
inline fun <reified T> Context.startActivity(
block: Intent.() -> Unit = {}
) {
Intent(this, T::class.java).apply {
block()
startActivity(this)
}
}
//inline fun <reified T : Activity> Context.startActivity(action:(bundle: Bundle)-> Unit) {
// Intent(this, T::class.java).apply {
// action(Bundle())
// startActivity(this)
// }
//}
inline fun <reified T> String.toType(gson: Gson? = null, typeToken: TypeToken<T>): T {
return (gson ?: Gson()).fromJson(this, typeToken.type)
}
inline fun <reified T> String.toObject(gson: Gson? = null): T {
return (gson ?: Gson()).fromJson(this, T::class.java)
}
fun Any?.toJsonString(gson: Gson? = null): String {
return (gson ?: Gson()).toJson(this) ?: ""
}
@SuppressLint("ApplySharedPref")
inline fun SharedPreferences.edit(
commit: Boolean = false,
action: SharedPreferences.Editor.() -> Unit
) {
val editor = edit()
action(editor)
if (commit) editor.commit() else editor.apply()
}
fun SharedPreferences.put(vararg pairs: Pair<String, Any>) {
edit {
pairs.forEach { (key, value) ->
when (value) {
is Int -> putInt(key, value)
is String -> putString(key, value)
is Boolean -> putBoolean(key, value)
is Float -> putFloat(key, value)
is Long -> putLong(key, value)
}
}
}
}
val Float.dp: Float
get() = TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
this,
Resources.getSystem().displayMetrics
)
val Int.dp: Int
get() = this.toFloat().dp.toInt()
fun EditText.addOnActionSearchListener(searchCallback: () -> Unit) {
setOnEditorActionListener { v, actionId, event ->
if (actionId == EditorInfo.IME_ACTION_SEARCH) {
// 处理搜索逻辑
searchCallback()
return@setOnEditorActionListener true // 阻止事件继续传递
}
return@setOnEditorActionListener false
}
}
fun View.clickWithDebounce(delay: Long = 300, action: () -> Unit) {
var job: Job? = null
setOnClickListener {
job?.cancel()
job = CoroutineScope(Dispatchers.Main).launch {
delay(delay)
action()
}
}
}
fun Double.roundedDecimalPlace(num: Int): Double {
return BigDecimal(this).setScale(num, RoundingMode.HALF_UP).toDouble()
}
fun Double.roundedOneDecimalPlace(): Double {
return this.roundedDecimalPlace(1)
}
fun View.setShapeDrawable(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
radius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
cornerRadius = radius.toFloat()
}
}
fun View.setShapeDrawable2(
solidColor: String = "#FFFFFF",
strokeWidth: Int = 0,
strokeColor: String = "#FFFFFF",
topLeftRadius: Int = 0,
topRightRadius: Int = 0,
bottomRightRadius: Int = 0,
bottomLeftRadius: Int = 0
) {
background = GradientDrawable().apply {
setColor(solidColor.toColorInt())
setStroke(strokeWidth, strokeColor.toColorInt())
setCornerRadii(
floatArrayOf(
topLeftRadius.toFloat(), topLeftRadius.toFloat(),
topRightRadius.toFloat(), topRightRadius.toFloat(),
bottomRightRadius.toFloat(), bottomRightRadius.toFloat(),
bottomLeftRadius.toFloat(), bottomLeftRadius.toFloat()
)
)
}
}
fun String?.ifNullOrBlank(defaultValue: String): String {
return if (this.isNullOrBlank()) defaultValue else this
}
fun Int?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0) defaultValue else this.toString()
}
fun Double?.ifNullOrZero(defaultValue: String): String {
return if (this == null || this == 0.toDouble()) defaultValue else this.toString()
}
fun View.hideKeyboard() {
val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
imm.hideSoftInputFromWindow(this.windowToken, 0)
this.clearFocus() // 清除焦点避免键盘再次弹出
}
/**
* 复制文本到剪贴板
* @param context 上下文
* @param text 要复制的文本内容
* @param showToast 是否显示复制成功提示,默认为true
*/
fun String.copyText(context: Context, showToast: Boolean = true) {
// 获取剪贴板管理器
val clipboardManager = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
// 创建ClipData对象,包含要复制的文本
val clipData = ClipData.newPlainText("label", this)
// 将数据设置到剪贴板
clipboardManager.setPrimaryClip(clipData)
// 显示复制成功提示
if (showToast) {
Toast.makeText(context, "文本已复制到剪贴板", Toast.LENGTH_SHORT).show()
}
}
@@ -0,0 +1,24 @@
package com.sw.inbound.utils.ext
import android.text.SpannableStringBuilder
import android.text.Spanned
inline fun buildSpannableString(builderAction: SpannableStringBuilder.() -> Unit): SpannableStringBuilder {
return SpannableStringBuilder().apply(builderAction)
}
fun SpannableStringBuilder.appendText(text: String, vararg spans: Any): SpannableStringBuilder {
val start = length
append(text)
spans.forEach { span ->
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}
return this
}
fun SpannableStringBuilder.withSpan(span: Any, block: SpannableStringBuilder.() -> Unit) {
val start = length
block()
setSpan(span, start, length, Spanned.SPAN_INCLUSIVE_EXCLUSIVE)
}