feat : libs
1
libs/lib_core/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
21
libs/lib_core/build.gradle
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
apply from : "../lib_standard.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.lib_core'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// 工具集
|
||||||
|
api project(path: ":libs:lib_utils")
|
||||||
|
|
||||||
|
api 'androidx.constraintlayout:constraintlayout:2.1.4'
|
||||||
|
api 'com.google.android.material:material:1.6.1'
|
||||||
|
api 'androidx.recyclerview:recyclerview:1.3.0'
|
||||||
|
api 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
|
||||||
|
api 'androidx.lifecycle:lifecycle-extensions:2.2.0'
|
||||||
|
|
||||||
|
|
||||||
|
// api "com.alibaba:fastjson:1.2.41"
|
||||||
|
api 'com.alibaba.fastjson2:fastjson2:2.0.57.android5' // 检查最新版本
|
||||||
|
|
||||||
|
}
|
0
libs/lib_core/consumer-rules.pro
Normal file
21
libs/lib_core/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_core/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>
|
@@ -0,0 +1,62 @@
|
|||||||
|
import androidx.lifecycle.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Max on 2023/10/24 15:11
|
||||||
|
* Desc:跟随目标生命周期销毁
|
||||||
|
**/
|
||||||
|
interface LifecycleCleared : LifecycleEventObserver {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否启用
|
||||||
|
*/
|
||||||
|
fun isEnabledLifecycleClear(): Boolean {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取监听的目标生命周期
|
||||||
|
*/
|
||||||
|
abstract fun getTargetLifecycle(): Lifecycle?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 目标生命周期已销毁:执行清除资源操作
|
||||||
|
*/
|
||||||
|
abstract fun onTargetCleared()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取要执行清理的事件
|
||||||
|
*/
|
||||||
|
fun getClearEvent(): Lifecycle.Event? {
|
||||||
|
return Lifecycle.Event.ON_DESTROY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定生命周期
|
||||||
|
*/
|
||||||
|
fun bindLifecycleClear() {
|
||||||
|
if (!isEnabledLifecycleClear()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getTargetLifecycle()?.addObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消绑定生命周期(如果实现类是自己主动销毁的,需要主动调下本方法)
|
||||||
|
*/
|
||||||
|
fun unBindLifecycleClear() {
|
||||||
|
if (!isEnabledLifecycleClear()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
getTargetLifecycle()?.removeObserver(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
if (!isEnabledLifecycleClear()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (getClearEvent() == event) {
|
||||||
|
unBindLifecycleClear()
|
||||||
|
onTargetCleared()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,54 @@
|
|||||||
|
package com.example.lib_core.component
|
||||||
|
|
||||||
|
import LifecycleCleared
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import com.example.lib_utils.ktx.asLifecycle
|
||||||
|
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Created by Max on 2023/10/24 15:11
|
||||||
|
* Desc:BottomSheetDialog
|
||||||
|
*/
|
||||||
|
open class SuperBottomSheetDialog : BottomSheetDialog, LifecycleCleared {
|
||||||
|
|
||||||
|
constructor(context: Context) : super(context) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(context: Context, theme: Int) : super(context, theme) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
context: Context,
|
||||||
|
cancelable: Boolean,
|
||||||
|
cancelListener: DialogInterface.OnCancelListener?
|
||||||
|
) : super(context, cancelable, cancelListener) {
|
||||||
|
init()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getTargetLifecycle(): Lifecycle? {
|
||||||
|
return context.asLifecycle()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onTargetCleared() {
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun show() {
|
||||||
|
super.show()
|
||||||
|
bindLifecycleClear()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun dismiss() {
|
||||||
|
super.dismiss()
|
||||||
|
unBindLifecycleClear()
|
||||||
|
}
|
||||||
|
}
|
3
libs/lib_crop/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
/build
|
||||||
|
*.iml
|
||||||
|
*.DS_Store
|
31
libs/lib_crop/build.gradle
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
apply plugin: 'com.android.library'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion COMPILE_SDK_VERSION.toInteger()
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion MIN_SDK_VERSION.toInteger()
|
||||||
|
targetSdkVersion TARGET_SDK_VERSION.toInteger()
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildToolsVersion = '30.0.3'
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api 'androidx.annotation:annotation:1.6.0'
|
||||||
|
api 'androidx.legacy:legacy-support-v4:1.0.0'
|
||||||
|
implementation "androidx.core:core-ktx:1.9.0"
|
||||||
|
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
|
||||||
|
}
|
||||||
|
repositories {
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
1
libs/lib_crop/src/main/AndroidManifest.xml
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<manifest package="com.soundcloud.crop" />
|
266
libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java
Normal file
@@ -0,0 +1,266 @@
|
|||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.app.Fragment;
|
||||||
|
import android.content.ActivityNotFoundException;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builder for crop Intents and utils for handling result
|
||||||
|
*/
|
||||||
|
public class Crop {
|
||||||
|
|
||||||
|
public static final int REQUEST_CROP = 6709;
|
||||||
|
public static final int REQUEST_PICK = 9162;
|
||||||
|
public static final int RESULT_ERROR = 404;
|
||||||
|
|
||||||
|
interface Extra {
|
||||||
|
String ASPECT_X = "aspect_x";
|
||||||
|
String ASPECT_Y = "aspect_y";
|
||||||
|
String MAX_X = "max_x";
|
||||||
|
String MAX_Y = "max_y";
|
||||||
|
String AS_PNG = "as_png";
|
||||||
|
String ERROR = "error";
|
||||||
|
}
|
||||||
|
|
||||||
|
private Intent cropIntent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a crop Intent builder with source and destination image Uris
|
||||||
|
*
|
||||||
|
* @param source Uri for image to crop
|
||||||
|
* @param destination Uri for saving the cropped image
|
||||||
|
*/
|
||||||
|
public static Crop of(Uri source, Uri destination) {
|
||||||
|
return new Crop(source, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Crop(Uri source, Uri destination) {
|
||||||
|
cropIntent = new Intent();
|
||||||
|
cropIntent.setData(source);
|
||||||
|
cropIntent.putExtra(MediaStore.EXTRA_OUTPUT, destination);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set fixed aspect ratio for crop area
|
||||||
|
*
|
||||||
|
* @param x Aspect X
|
||||||
|
* @param y Aspect Y
|
||||||
|
*/
|
||||||
|
public Crop withAspect(int x, int y) {
|
||||||
|
cropIntent.putExtra(Extra.ASPECT_X, x);
|
||||||
|
cropIntent.putExtra(Extra.ASPECT_Y, y);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Crop area with fixed 1:1 aspect ratio
|
||||||
|
*/
|
||||||
|
public Crop asSquare() {
|
||||||
|
cropIntent.putExtra(Extra.ASPECT_X, 1);
|
||||||
|
cropIntent.putExtra(Extra.ASPECT_Y, 1);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set maximum crop size
|
||||||
|
*
|
||||||
|
* @param width Max width
|
||||||
|
* @param height Max height
|
||||||
|
*/
|
||||||
|
public Crop withMaxSize(int width, int height) {
|
||||||
|
cropIntent.putExtra(Extra.MAX_X, width);
|
||||||
|
cropIntent.putExtra(Extra.MAX_Y, height);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set whether to save the result as a PNG or not. Helpful to preserve alpha.
|
||||||
|
* @param asPng whether to save the result as a PNG or not
|
||||||
|
*/
|
||||||
|
public Crop asPng(boolean asPng) {
|
||||||
|
cropIntent.putExtra(Extra.AS_PNG, asPng);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent from an Activity
|
||||||
|
*
|
||||||
|
* @param activity Activity to receive result
|
||||||
|
*/
|
||||||
|
public void start(Activity activity) {
|
||||||
|
start(activity, REQUEST_CROP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent from an Activity with a custom request code
|
||||||
|
*
|
||||||
|
* @param activity Activity to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
public void start(Activity activity, int requestCode) {
|
||||||
|
activity.startActivityForResult(getIntent(activity), requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent from a Fragment
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
*/
|
||||||
|
public void start(Context context, Fragment fragment) {
|
||||||
|
start(context, fragment, REQUEST_CROP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent from a support library Fragment
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
*/
|
||||||
|
public void start(Context context, androidx.fragment.app.Fragment fragment) {
|
||||||
|
start(context, fragment, REQUEST_CROP);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent with a custom request code
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
|
public void start(Context context, Fragment fragment, int requestCode) {
|
||||||
|
fragment.startActivityForResult(getIntent(context), requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send the crop Intent with a custom request code
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
public void start(Context context, androidx.fragment.app.Fragment fragment, int requestCode) {
|
||||||
|
fragment.startActivityForResult(getIntent(context), requestCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get Intent to start crop Activity
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @return Intent for CropImageActivity
|
||||||
|
*/
|
||||||
|
public Intent getIntent(Context context) {
|
||||||
|
cropIntent.setClass(context, CropImageActivity.class);
|
||||||
|
return cropIntent;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve URI for cropped image, as set in the Intent builder
|
||||||
|
*
|
||||||
|
* @param result Output Image URI
|
||||||
|
*/
|
||||||
|
public static Uri getOutput(Intent result) {
|
||||||
|
return result.getParcelableExtra(MediaStore.EXTRA_OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve error that caused crop to fail
|
||||||
|
*
|
||||||
|
* @param result Result Intent
|
||||||
|
* @return Throwable handled in CropImageActivity
|
||||||
|
*/
|
||||||
|
public static Throwable getError(Intent result) {
|
||||||
|
return (Throwable) result.getSerializableExtra(Extra.ERROR);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from an Activity
|
||||||
|
*
|
||||||
|
* @param activity Activity to receive result
|
||||||
|
*/
|
||||||
|
public static void pickImage(Activity activity) {
|
||||||
|
pickImage(activity, REQUEST_PICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from a Fragment
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
*/
|
||||||
|
public static void pickImage(Context context, Fragment fragment) {
|
||||||
|
pickImage(context, fragment, REQUEST_PICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from a support library Fragment
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
*/
|
||||||
|
public static void pickImage(Context context, androidx.fragment.app.Fragment fragment) {
|
||||||
|
pickImage(context, fragment, REQUEST_PICK);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from an Activity with a custom request code
|
||||||
|
*
|
||||||
|
* @param activity Activity to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
public static void pickImage(Activity activity, int requestCode) {
|
||||||
|
try {
|
||||||
|
activity.startActivityForResult(getImagePicker(), requestCode);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
showImagePickerError(activity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from a Fragment with a custom request code
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
@TargetApi(Build.VERSION_CODES.HONEYCOMB)
|
||||||
|
public static void pickImage(Context context, Fragment fragment, int requestCode) {
|
||||||
|
try {
|
||||||
|
fragment.startActivityForResult(getImagePicker(), requestCode);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
showImagePickerError(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pick image from a support library Fragment with a custom request code
|
||||||
|
*
|
||||||
|
* @param context Context
|
||||||
|
* @param fragment Fragment to receive result
|
||||||
|
* @param requestCode requestCode for result
|
||||||
|
*/
|
||||||
|
public static void pickImage(Context context, androidx.fragment.app.Fragment fragment, int requestCode) {
|
||||||
|
try {
|
||||||
|
fragment.startActivityForResult(getImagePicker(), requestCode);
|
||||||
|
} catch (ActivityNotFoundException e) {
|
||||||
|
showImagePickerError(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Intent getImagePicker() {
|
||||||
|
return new Intent(Intent.ACTION_GET_CONTENT).setType("image/*");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void showImagePickerError(Context context) {
|
||||||
|
Toast.makeText(context.getApplicationContext(), R.string.crop__pick_error, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,441 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2007 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.BitmapFactory;
|
||||||
|
import android.graphics.BitmapRegionDecoder;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.opengl.GLES10;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.Window;
|
||||||
|
import android.view.WindowManager;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from original in AOSP.
|
||||||
|
*/
|
||||||
|
public class CropImageActivity extends MonitoredActivity {
|
||||||
|
|
||||||
|
private static final int SIZE_DEFAULT = 2048;
|
||||||
|
private static final int SIZE_LIMIT = 4096;
|
||||||
|
|
||||||
|
private final Handler handler = new Handler();
|
||||||
|
|
||||||
|
private int aspectX;
|
||||||
|
private int aspectY;
|
||||||
|
|
||||||
|
// Output image
|
||||||
|
private int maxX;
|
||||||
|
private int maxY;
|
||||||
|
private int exifRotation;
|
||||||
|
private boolean saveAsPng;
|
||||||
|
|
||||||
|
private Uri sourceUri;
|
||||||
|
private Uri saveUri;
|
||||||
|
|
||||||
|
private boolean isSaving;
|
||||||
|
|
||||||
|
private int sampleSize;
|
||||||
|
private RotateBitmap rotateBitmap;
|
||||||
|
private CropImageView imageView;
|
||||||
|
private HighlightView cropView;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onCreate(Bundle icicle) {
|
||||||
|
super.onCreate(icicle);
|
||||||
|
setupWindowFlags();
|
||||||
|
setupViews();
|
||||||
|
|
||||||
|
loadInput();
|
||||||
|
if (rotateBitmap == null) {
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
startCrop();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.KITKAT)
|
||||||
|
private void setupWindowFlags() {
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE);
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||||
|
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setupViews() {
|
||||||
|
setContentView(R.layout.crop__activity_crop);
|
||||||
|
|
||||||
|
imageView = (CropImageView) findViewById(R.id.crop_image);
|
||||||
|
imageView.context = this;
|
||||||
|
imageView.setRecycler(new ImageViewTouchBase.Recycler() {
|
||||||
|
@Override
|
||||||
|
public void recycle(Bitmap b) {
|
||||||
|
b.recycle();
|
||||||
|
System.gc();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.btn_cancel).setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
setResult(RESULT_CANCELED);
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
findViewById(R.id.btn_done).setOnClickListener(new View.OnClickListener() {
|
||||||
|
public void onClick(View v) {
|
||||||
|
onSaveClicked();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadInput() {
|
||||||
|
Intent intent = getIntent();
|
||||||
|
Bundle extras = intent.getExtras();
|
||||||
|
|
||||||
|
if (extras != null) {
|
||||||
|
aspectX = extras.getInt(Crop.Extra.ASPECT_X);
|
||||||
|
aspectY = extras.getInt(Crop.Extra.ASPECT_Y);
|
||||||
|
maxX = extras.getInt(Crop.Extra.MAX_X);
|
||||||
|
maxY = extras.getInt(Crop.Extra.MAX_Y);
|
||||||
|
saveAsPng = extras.getBoolean(Crop.Extra.AS_PNG, false);
|
||||||
|
saveUri = extras.getParcelable(MediaStore.EXTRA_OUTPUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
sourceUri = intent.getData();
|
||||||
|
if (sourceUri != null) {
|
||||||
|
exifRotation = CropUtil.getExifRotation(CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri));
|
||||||
|
|
||||||
|
InputStream is = null;
|
||||||
|
try {
|
||||||
|
sampleSize = calculateBitmapSampleSize(sourceUri);
|
||||||
|
is = getContentResolver().openInputStream(sourceUri);
|
||||||
|
BitmapFactory.Options option = new BitmapFactory.Options();
|
||||||
|
option.inSampleSize = sampleSize;
|
||||||
|
rotateBitmap = new RotateBitmap(BitmapFactory.decodeStream(is, null, option), exifRotation);
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Error reading image: " + e.getMessage(), e);
|
||||||
|
setResultException(e);
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
Log.e("OOM reading image: " + e.getMessage(), e);
|
||||||
|
setResultException(e);
|
||||||
|
} finally {
|
||||||
|
CropUtil.closeSilently(is);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculateBitmapSampleSize(Uri bitmapUri) throws IOException {
|
||||||
|
InputStream is = null;
|
||||||
|
BitmapFactory.Options options = new BitmapFactory.Options();
|
||||||
|
options.inJustDecodeBounds = true;
|
||||||
|
try {
|
||||||
|
is = getContentResolver().openInputStream(bitmapUri);
|
||||||
|
BitmapFactory.decodeStream(is, null, options); // Just get image size
|
||||||
|
} finally {
|
||||||
|
CropUtil.closeSilently(is);
|
||||||
|
}
|
||||||
|
|
||||||
|
int maxSize = getMaxImageSize();
|
||||||
|
int sampleSize = 1;
|
||||||
|
while (options.outHeight / sampleSize > maxSize || options.outWidth / sampleSize > maxSize) {
|
||||||
|
sampleSize = sampleSize << 1;
|
||||||
|
}
|
||||||
|
return sampleSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMaxImageSize() {
|
||||||
|
int textureLimit = getMaxTextureSize();
|
||||||
|
if (textureLimit == 0) {
|
||||||
|
return SIZE_DEFAULT;
|
||||||
|
} else {
|
||||||
|
return Math.min(textureLimit, SIZE_LIMIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getMaxTextureSize() {
|
||||||
|
// The OpenGL texture size is the maximum size that can be drawn in an ImageView
|
||||||
|
int[] maxSize = new int[1];
|
||||||
|
GLES10.glGetIntegerv(GLES10.GL_MAX_TEXTURE_SIZE, maxSize, 0);
|
||||||
|
return maxSize[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startCrop() {
|
||||||
|
if (isFinishing()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
imageView.setImageRotateBitmapResetBase(rotateBitmap, true);
|
||||||
|
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__wait),
|
||||||
|
new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
final CountDownLatch latch = new CountDownLatch(1);
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
if (imageView.getScale() == 1F) {
|
||||||
|
imageView.center();
|
||||||
|
}
|
||||||
|
latch.countDown();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
latch.await();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
new Cropper().crop();
|
||||||
|
}
|
||||||
|
}, handler
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class Cropper {
|
||||||
|
|
||||||
|
private void makeDefault() {
|
||||||
|
if (rotateBitmap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HighlightView hv = new HighlightView(imageView);
|
||||||
|
final int width = rotateBitmap.getWidth();
|
||||||
|
final int height = rotateBitmap.getHeight();
|
||||||
|
|
||||||
|
Rect imageRect = new Rect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Make the default size about 4/5 of the width or height
|
||||||
|
int cropWidth = Math.min(width, height) * 4 / 5;
|
||||||
|
@SuppressWarnings("SuspiciousNameCombination")
|
||||||
|
int cropHeight = cropWidth;
|
||||||
|
|
||||||
|
if (aspectX != 0 && aspectY != 0) {
|
||||||
|
if (aspectX > aspectY) {
|
||||||
|
cropHeight = cropWidth * aspectY / aspectX;
|
||||||
|
} else {
|
||||||
|
cropWidth = cropHeight * aspectX / aspectY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int x = (width - cropWidth) / 2;
|
||||||
|
int y = (height - cropHeight) / 2;
|
||||||
|
|
||||||
|
RectF cropRect = new RectF(x, y, x + cropWidth, y + cropHeight);
|
||||||
|
hv.setup(imageView.getUnrotatedMatrix(), imageRect, cropRect, aspectX != 0 && aspectY != 0);
|
||||||
|
imageView.add(hv);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void crop() {
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
makeDefault();
|
||||||
|
imageView.invalidate();
|
||||||
|
if (imageView.highlightViews.size() == 1) {
|
||||||
|
cropView = imageView.highlightViews.get(0);
|
||||||
|
cropView.setFocus(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSaveClicked() {
|
||||||
|
if (cropView == null || isSaving) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
isSaving = true;
|
||||||
|
|
||||||
|
Bitmap croppedImage;
|
||||||
|
Rect r = cropView.getScaledCropRect(sampleSize);
|
||||||
|
int width = r.width();
|
||||||
|
int height = r.height();
|
||||||
|
|
||||||
|
int outWidth = width;
|
||||||
|
int outHeight = height;
|
||||||
|
if (maxX > 0 && maxY > 0 && (width > maxX || height > maxY)) {
|
||||||
|
float ratio = (float) width / (float) height;
|
||||||
|
if ((float) maxX / (float) maxY > ratio) {
|
||||||
|
outHeight = maxY;
|
||||||
|
outWidth = (int) ((float) maxY * ratio + .5f);
|
||||||
|
} else {
|
||||||
|
outWidth = maxX;
|
||||||
|
outHeight = (int) ((float) maxX / ratio + .5f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
croppedImage = decodeRegionCrop(r, outWidth, outHeight);
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
setResultException(e);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (croppedImage != null) {
|
||||||
|
imageView.setImageRotateBitmapResetBase(new RotateBitmap(croppedImage, exifRotation), true);
|
||||||
|
imageView.center();
|
||||||
|
imageView.highlightViews.clear();
|
||||||
|
}
|
||||||
|
saveImage(croppedImage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveImage(Bitmap croppedImage) {
|
||||||
|
if (croppedImage != null) {
|
||||||
|
final Bitmap b = croppedImage;
|
||||||
|
CropUtil.startBackgroundJob(this, null, getResources().getString(R.string.crop__saving),
|
||||||
|
new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
saveOutput(b);
|
||||||
|
}
|
||||||
|
}, handler
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Bitmap decodeRegionCrop(Rect rect, int outWidth, int outHeight) {
|
||||||
|
// Release memory now
|
||||||
|
clearImageView();
|
||||||
|
|
||||||
|
InputStream is = null;
|
||||||
|
Bitmap croppedImage = null;
|
||||||
|
try {
|
||||||
|
is = getContentResolver().openInputStream(sourceUri);
|
||||||
|
BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(is, false);
|
||||||
|
final int width = decoder.getWidth();
|
||||||
|
final int height = decoder.getHeight();
|
||||||
|
|
||||||
|
if (exifRotation != 0) {
|
||||||
|
// Adjust crop area to account for image rotation
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
matrix.setRotate(-exifRotation);
|
||||||
|
|
||||||
|
RectF adjusted = new RectF();
|
||||||
|
matrix.mapRect(adjusted, new RectF(rect));
|
||||||
|
|
||||||
|
// Adjust to account for origin at 0,0
|
||||||
|
adjusted.offset(adjusted.left < 0 ? width : 0, adjusted.top < 0 ? height : 0);
|
||||||
|
rect = new Rect((int) adjusted.left, (int) adjusted.top, (int) adjusted.right, (int) adjusted.bottom);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
croppedImage = decoder.decodeRegion(rect, new BitmapFactory.Options());
|
||||||
|
if (croppedImage != null && (rect.width() > outWidth || rect.height() > outHeight)) {
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
matrix.postScale((float) outWidth / rect.width(), (float) outHeight / rect.height());
|
||||||
|
croppedImage = Bitmap.createBitmap(croppedImage, 0, 0, croppedImage.getWidth(), croppedImage.getHeight(), matrix, true);
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// Rethrow with some extra information
|
||||||
|
throw new IllegalArgumentException("Rectangle " + rect + " is outside of the image ("
|
||||||
|
+ width + "," + height + "," + exifRotation + ")", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Error cropping image: " + e.getMessage(), e);
|
||||||
|
setResultException(e);
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
Log.e("OOM cropping image: " + e.getMessage(), e);
|
||||||
|
setResultException(e);
|
||||||
|
} finally {
|
||||||
|
CropUtil.closeSilently(is);
|
||||||
|
}
|
||||||
|
return croppedImage;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void clearImageView() {
|
||||||
|
imageView.clear();
|
||||||
|
if (rotateBitmap != null) {
|
||||||
|
rotateBitmap.recycle();
|
||||||
|
}
|
||||||
|
System.gc();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void saveOutput(Bitmap croppedImage) {
|
||||||
|
if (saveUri != null) {
|
||||||
|
OutputStream outputStream = null;
|
||||||
|
try {
|
||||||
|
outputStream = getContentResolver().openOutputStream(saveUri);
|
||||||
|
if (outputStream != null) {
|
||||||
|
croppedImage.compress(saveAsPng ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG,
|
||||||
|
90, // note: quality is ignored when using PNG
|
||||||
|
outputStream);
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
setResultException(e);
|
||||||
|
Log.e("Cannot open file: " + saveUri, e);
|
||||||
|
} finally {
|
||||||
|
CropUtil.closeSilently(outputStream);
|
||||||
|
}
|
||||||
|
|
||||||
|
CropUtil.copyExifRotation(
|
||||||
|
CropUtil.getFromMediaUri(this, getContentResolver(), sourceUri),
|
||||||
|
CropUtil.getFromMediaUri(this, getContentResolver(), saveUri)
|
||||||
|
);
|
||||||
|
|
||||||
|
setResultUri(saveUri);
|
||||||
|
}
|
||||||
|
|
||||||
|
final Bitmap b = croppedImage;
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
imageView.clear();
|
||||||
|
b.recycle();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
if (rotateBitmap != null) {
|
||||||
|
rotateBitmap.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onSearchRequested() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSaving() {
|
||||||
|
return isSaving;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setResultUri(Uri uri) {
|
||||||
|
setResult(RESULT_OK, new Intent().putExtra(MediaStore.EXTRA_OUTPUT, uri));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setResultException(Throwable throwable) {
|
||||||
|
setResult(Crop.RESULT_ERROR, new Intent().putExtra(Crop.Extra.ERROR, throwable));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,195 @@
|
|||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
public class CropImageView extends ImageViewTouchBase {
|
||||||
|
|
||||||
|
ArrayList<HighlightView> highlightViews = new ArrayList<HighlightView>();
|
||||||
|
HighlightView motionHighlightView;
|
||||||
|
Context context;
|
||||||
|
|
||||||
|
private float lastX;
|
||||||
|
private float lastY;
|
||||||
|
private int motionEdge;
|
||||||
|
private int validPointerId;
|
||||||
|
|
||||||
|
public CropImageView(Context context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropImageView(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
}
|
||||||
|
|
||||||
|
public CropImageView(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
if (bitmapDisplayed.getBitmap() != null) {
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
|
||||||
|
hv.matrix.set(getUnrotatedMatrix());
|
||||||
|
hv.invalidate();
|
||||||
|
if (hv.hasFocus()) {
|
||||||
|
centerBasedOnHighlightView(hv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void zoomTo(float scale, float centerX, float centerY) {
|
||||||
|
super.zoomTo(scale, centerX, centerY);
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
hv.matrix.set(getUnrotatedMatrix());
|
||||||
|
hv.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void zoomIn() {
|
||||||
|
super.zoomIn();
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
hv.matrix.set(getUnrotatedMatrix());
|
||||||
|
hv.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void zoomOut() {
|
||||||
|
super.zoomOut();
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
hv.matrix.set(getUnrotatedMatrix());
|
||||||
|
hv.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void postTranslate(float deltaX, float deltaY) {
|
||||||
|
super.postTranslate(deltaX, deltaY);
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
hv.matrix.postTranslate(deltaX, deltaY);
|
||||||
|
hv.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(@NonNull MotionEvent event) {
|
||||||
|
CropImageActivity cropImageActivity = (CropImageActivity) context;
|
||||||
|
if (cropImageActivity.isSaving()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.getAction()) {
|
||||||
|
case MotionEvent.ACTION_DOWN:
|
||||||
|
for (HighlightView hv : highlightViews) {
|
||||||
|
int edge = hv.getHit(event.getX(), event.getY());
|
||||||
|
if (edge != HighlightView.GROW_NONE) {
|
||||||
|
motionEdge = edge;
|
||||||
|
motionHighlightView = hv;
|
||||||
|
lastX = event.getX();
|
||||||
|
lastY = event.getY();
|
||||||
|
// Prevent multiple touches from interfering with crop area re-sizing
|
||||||
|
validPointerId = event.getPointerId(event.getActionIndex());
|
||||||
|
motionHighlightView.setMode((edge == HighlightView.MOVE)
|
||||||
|
? HighlightView.ModifyMode.Move
|
||||||
|
: HighlightView.ModifyMode.Grow);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
if (motionHighlightView != null) {
|
||||||
|
centerBasedOnHighlightView(motionHighlightView);
|
||||||
|
motionHighlightView.setMode(HighlightView.ModifyMode.None);
|
||||||
|
}
|
||||||
|
motionHighlightView = null;
|
||||||
|
center();
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
if (motionHighlightView != null && event.getPointerId(event.getActionIndex()) == validPointerId) {
|
||||||
|
motionHighlightView.handleMotion(motionEdge, event.getX()
|
||||||
|
- lastX, event.getY() - lastY);
|
||||||
|
lastX = event.getX();
|
||||||
|
lastY = event.getY();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're not zoomed then there's no point in even allowing the user to move the image around.
|
||||||
|
// This call to center puts it back to the normalized location.
|
||||||
|
if (getScale() == 1F) {
|
||||||
|
center();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pan the displayed image to make sure the cropping rectangle is visible.
|
||||||
|
private void ensureVisible(HighlightView hv) {
|
||||||
|
Rect r = hv.drawRect;
|
||||||
|
|
||||||
|
int panDeltaX1 = Math.max(0, getLeft() - r.left);
|
||||||
|
int panDeltaX2 = Math.min(0, getRight() - r.right);
|
||||||
|
|
||||||
|
int panDeltaY1 = Math.max(0, getTop() - r.top);
|
||||||
|
int panDeltaY2 = Math.min(0, getBottom() - r.bottom);
|
||||||
|
|
||||||
|
int panDeltaX = panDeltaX1 != 0 ? panDeltaX1 : panDeltaX2;
|
||||||
|
int panDeltaY = panDeltaY1 != 0 ? panDeltaY1 : panDeltaY2;
|
||||||
|
|
||||||
|
if (panDeltaX != 0 || panDeltaY != 0) {
|
||||||
|
panBy(panDeltaX, panDeltaY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the cropping rectangle's size changed significantly, change the
|
||||||
|
// view's center and scale according to the cropping rectangle.
|
||||||
|
private void centerBasedOnHighlightView(HighlightView hv) {
|
||||||
|
Rect drawRect = hv.drawRect;
|
||||||
|
|
||||||
|
float width = drawRect.width();
|
||||||
|
float height = drawRect.height();
|
||||||
|
|
||||||
|
float thisWidth = getWidth();
|
||||||
|
float thisHeight = getHeight();
|
||||||
|
|
||||||
|
float z1 = thisWidth / width * .6F;
|
||||||
|
float z2 = thisHeight / height * .6F;
|
||||||
|
|
||||||
|
float zoom = Math.min(z1, z2);
|
||||||
|
zoom = zoom * this.getScale();
|
||||||
|
zoom = Math.max(1F, zoom);
|
||||||
|
|
||||||
|
if ((Math.abs(zoom - getScale()) / zoom) > .1) {
|
||||||
|
float[] coordinates = new float[] { hv.cropRect.centerX(), hv.cropRect.centerY() };
|
||||||
|
getUnrotatedMatrix().mapPoints(coordinates);
|
||||||
|
zoomTo(zoom, coordinates[0], coordinates[1], 300F);
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureVisible(hv);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(@NonNull Canvas canvas) {
|
||||||
|
super.onDraw(canvas);
|
||||||
|
for (HighlightView highlightView : highlightViews) {
|
||||||
|
highlightView.draw(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void add(HighlightView hv) {
|
||||||
|
highlightViews.add(hv);
|
||||||
|
invalidate();
|
||||||
|
}
|
||||||
|
}
|
227
libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java
Normal file
@@ -0,0 +1,227 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.app.ProgressDialog;
|
||||||
|
import android.content.ContentResolver;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.media.ExifInterface;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.os.ParcelFileDescriptor;
|
||||||
|
import android.provider.MediaStore;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.io.Closeable;
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.FileInputStream;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from original in AOSP.
|
||||||
|
*/
|
||||||
|
class CropUtil {
|
||||||
|
|
||||||
|
private static final String SCHEME_FILE = "file";
|
||||||
|
private static final String SCHEME_CONTENT = "content";
|
||||||
|
|
||||||
|
public static void closeSilently(@Nullable Closeable c) {
|
||||||
|
if (c == null) return;
|
||||||
|
try {
|
||||||
|
c.close();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getExifRotation(File imageFile) {
|
||||||
|
if (imageFile == null) return 0;
|
||||||
|
try {
|
||||||
|
ExifInterface exif = new ExifInterface(imageFile.getAbsolutePath());
|
||||||
|
// We only recognize a subset of orientation tag values
|
||||||
|
switch (exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_UNDEFINED)) {
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_90:
|
||||||
|
return 90;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_180:
|
||||||
|
return 180;
|
||||||
|
case ExifInterface.ORIENTATION_ROTATE_270:
|
||||||
|
return 270;
|
||||||
|
default:
|
||||||
|
return ExifInterface.ORIENTATION_UNDEFINED;
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Error getting Exif data", e);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean copyExifRotation(File sourceFile, File destFile) {
|
||||||
|
if (sourceFile == null || destFile == null) return false;
|
||||||
|
try {
|
||||||
|
ExifInterface exifSource = new ExifInterface(sourceFile.getAbsolutePath());
|
||||||
|
ExifInterface exifDest = new ExifInterface(destFile.getAbsolutePath());
|
||||||
|
exifDest.setAttribute(ExifInterface.TAG_ORIENTATION, exifSource.getAttribute(ExifInterface.TAG_ORIENTATION));
|
||||||
|
exifDest.saveAttributes();
|
||||||
|
return true;
|
||||||
|
} catch (IOException e) {
|
||||||
|
Log.e("Error copying Exif data", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static File getFromMediaUri(Context context, ContentResolver resolver, Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
|
||||||
|
if (SCHEME_FILE.equals(uri.getScheme())) {
|
||||||
|
return new File(uri.getPath());
|
||||||
|
} else if (SCHEME_CONTENT.equals(uri.getScheme())) {
|
||||||
|
final String[] filePathColumn = { MediaStore.MediaColumns.DATA, MediaStore.MediaColumns.DISPLAY_NAME };
|
||||||
|
Cursor cursor = null;
|
||||||
|
try {
|
||||||
|
cursor = resolver.query(uri, filePathColumn, null, null, null);
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
final int columnIndex = (uri.toString().startsWith("content://com.google.android.gallery3d")) ?
|
||||||
|
cursor.getColumnIndex(MediaStore.MediaColumns.DISPLAY_NAME) :
|
||||||
|
cursor.getColumnIndex(MediaStore.MediaColumns.DATA);
|
||||||
|
// Picasa images on API 13+
|
||||||
|
if (columnIndex != -1) {
|
||||||
|
String filePath = cursor.getString(columnIndex);
|
||||||
|
if (!TextUtils.isEmpty(filePath)) {
|
||||||
|
return new File(filePath);
|
||||||
|
}
|
||||||
|
} else if (TextUtils.equals(uri.getAuthority(), UriUtil.getFileProviderName(context))) {
|
||||||
|
//这里修复拍照自定义的uri获取路径失败的问题
|
||||||
|
String path = UriUtil.parseOwnUri(context, uri);
|
||||||
|
if (!TextUtils.isEmpty(path)) {
|
||||||
|
File file = new File(path);
|
||||||
|
if (file.exists()) {
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (IllegalArgumentException e) {
|
||||||
|
// Google Drive images
|
||||||
|
return getFromMediaUriPfd(context, resolver, uri);
|
||||||
|
} catch (SecurityException ignored) {
|
||||||
|
// Nothing we can do
|
||||||
|
} finally {
|
||||||
|
if (cursor != null) cursor.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getTempFilename(Context context) throws IOException {
|
||||||
|
File outputDir = context.getCacheDir();
|
||||||
|
File outputFile = File.createTempFile("image", "tmp", outputDir);
|
||||||
|
return outputFile.getAbsolutePath();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static File getFromMediaUriPfd(Context context, ContentResolver resolver, Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
|
||||||
|
FileInputStream input = null;
|
||||||
|
FileOutputStream output = null;
|
||||||
|
try {
|
||||||
|
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
|
||||||
|
FileDescriptor fd = pfd.getFileDescriptor();
|
||||||
|
input = new FileInputStream(fd);
|
||||||
|
|
||||||
|
String tempFilename = getTempFilename(context);
|
||||||
|
output = new FileOutputStream(tempFilename);
|
||||||
|
|
||||||
|
int read;
|
||||||
|
byte[] bytes = new byte[4096];
|
||||||
|
while ((read = input.read(bytes)) != -1) {
|
||||||
|
output.write(bytes, 0, read);
|
||||||
|
}
|
||||||
|
return new File(tempFilename);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
// Nothing we can do
|
||||||
|
} finally {
|
||||||
|
closeSilently(input);
|
||||||
|
closeSilently(output);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void startBackgroundJob(MonitoredActivity activity,
|
||||||
|
String title, String message, Runnable job, Handler handler) {
|
||||||
|
// Make the progress dialog uncancelable, so that we can guarantee
|
||||||
|
// the thread will be done before the activity getting destroyed
|
||||||
|
ProgressDialog dialog = ProgressDialog.show(
|
||||||
|
activity, title, message, true, false);
|
||||||
|
new Thread(new BackgroundJob(activity, job, dialog, handler)).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class BackgroundJob extends MonitoredActivity.LifeCycleAdapter implements Runnable {
|
||||||
|
|
||||||
|
private final MonitoredActivity activity;
|
||||||
|
private final ProgressDialog dialog;
|
||||||
|
private final Runnable job;
|
||||||
|
private final Handler handler;
|
||||||
|
private final Runnable cleanupRunner = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
activity.removeLifeCycleListener(BackgroundJob.this);
|
||||||
|
if (dialog.getWindow() != null) dialog.dismiss();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public BackgroundJob(MonitoredActivity activity, Runnable job,
|
||||||
|
ProgressDialog dialog, Handler handler) {
|
||||||
|
this.activity = activity;
|
||||||
|
this.dialog = dialog;
|
||||||
|
this.job = job;
|
||||||
|
this.activity.addLifeCycleListener(this);
|
||||||
|
this.handler = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
try {
|
||||||
|
job.run();
|
||||||
|
} finally {
|
||||||
|
handler.post(cleanupRunner);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityDestroyed(MonitoredActivity activity) {
|
||||||
|
// We get here only when the onDestroyed being called before
|
||||||
|
// the cleanupRunner. So, run it now and remove it from the queue
|
||||||
|
cleanupRunner.run();
|
||||||
|
handler.removeCallbacks(cleanupRunner);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStopped(MonitoredActivity activity) {
|
||||||
|
dialog.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onActivityStarted(MonitoredActivity activity) {
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,395 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2007 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.TypedArray;
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.Region;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.util.TypedValue;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from version in AOSP.
|
||||||
|
*
|
||||||
|
* This class is used to display a highlighted cropping rectangle
|
||||||
|
* overlayed on the image. There are two coordinate spaces in use. One is
|
||||||
|
* image, another is screen. computeLayout() uses matrix to map from image
|
||||||
|
* space to screen space.
|
||||||
|
*/
|
||||||
|
class HighlightView {
|
||||||
|
|
||||||
|
public static final int GROW_NONE = (1 << 0);
|
||||||
|
public static final int GROW_LEFT_EDGE = (1 << 1);
|
||||||
|
public static final int GROW_RIGHT_EDGE = (1 << 2);
|
||||||
|
public static final int GROW_TOP_EDGE = (1 << 3);
|
||||||
|
public static final int GROW_BOTTOM_EDGE = (1 << 4);
|
||||||
|
public static final int MOVE = (1 << 5);
|
||||||
|
|
||||||
|
private static final int DEFAULT_HIGHLIGHT_COLOR = 0xFF33B5E5;
|
||||||
|
private static final float HANDLE_RADIUS_DP = 12f;
|
||||||
|
private static final float OUTLINE_DP = 2f;
|
||||||
|
|
||||||
|
enum ModifyMode { None, Move, Grow }
|
||||||
|
enum HandleMode { Changing, Always, Never }
|
||||||
|
|
||||||
|
RectF cropRect; // Image space
|
||||||
|
Rect drawRect; // Screen space
|
||||||
|
Matrix matrix;
|
||||||
|
private RectF imageRect; // Image space
|
||||||
|
|
||||||
|
private final Paint outsidePaint = new Paint();
|
||||||
|
private final Paint outlinePaint = new Paint();
|
||||||
|
private final Paint handlePaint = new Paint();
|
||||||
|
|
||||||
|
private View viewContext; // View displaying image
|
||||||
|
private boolean showThirds;
|
||||||
|
private boolean showCircle;
|
||||||
|
private int highlightColor;
|
||||||
|
|
||||||
|
private ModifyMode modifyMode = ModifyMode.None;
|
||||||
|
private HandleMode handleMode = HandleMode.Changing;
|
||||||
|
private boolean maintainAspectRatio;
|
||||||
|
private float initialAspectRatio;
|
||||||
|
private float handleRadius;
|
||||||
|
private float outlineWidth;
|
||||||
|
private boolean isFocused;
|
||||||
|
|
||||||
|
public HighlightView(View context) {
|
||||||
|
viewContext = context;
|
||||||
|
initStyles(context.getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initStyles(Context context) {
|
||||||
|
TypedValue outValue = new TypedValue();
|
||||||
|
context.getTheme().resolveAttribute(R.attr.cropImageStyle, outValue, true);
|
||||||
|
TypedArray attributes = context.obtainStyledAttributes(outValue.resourceId, R.styleable.CropImageView);
|
||||||
|
try {
|
||||||
|
showThirds = attributes.getBoolean(R.styleable.CropImageView_showThirds, false);
|
||||||
|
showCircle = attributes.getBoolean(R.styleable.CropImageView_showCircle, false);
|
||||||
|
highlightColor = attributes.getColor(R.styleable.CropImageView_highlightColor,
|
||||||
|
DEFAULT_HIGHLIGHT_COLOR);
|
||||||
|
handleMode = HandleMode.values()[attributes.getInt(R.styleable.CropImageView_showHandles, 0)];
|
||||||
|
} finally {
|
||||||
|
attributes.recycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setup(Matrix m, Rect imageRect, RectF cropRect, boolean maintainAspectRatio) {
|
||||||
|
matrix = new Matrix(m);
|
||||||
|
|
||||||
|
this.cropRect = cropRect;
|
||||||
|
this.imageRect = new RectF(imageRect);
|
||||||
|
this.maintainAspectRatio = maintainAspectRatio;
|
||||||
|
|
||||||
|
initialAspectRatio = this.cropRect.width() / this.cropRect.height();
|
||||||
|
drawRect = computeLayout();
|
||||||
|
|
||||||
|
outsidePaint.setARGB(125, 50, 50, 50);
|
||||||
|
outlinePaint.setStyle(Paint.Style.STROKE);
|
||||||
|
outlinePaint.setAntiAlias(true);
|
||||||
|
outlineWidth = dpToPx(OUTLINE_DP);
|
||||||
|
|
||||||
|
handlePaint.setColor(highlightColor);
|
||||||
|
handlePaint.setStyle(Paint.Style.FILL);
|
||||||
|
handlePaint.setAntiAlias(true);
|
||||||
|
handleRadius = dpToPx(HANDLE_RADIUS_DP);
|
||||||
|
|
||||||
|
modifyMode = ModifyMode.None;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float dpToPx(float dp) {
|
||||||
|
return dp * viewContext.getResources().getDisplayMetrics().density;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void draw(Canvas canvas) {
|
||||||
|
canvas.save();
|
||||||
|
Path path = new Path();
|
||||||
|
outlinePaint.setStrokeWidth(outlineWidth);
|
||||||
|
if (!hasFocus()) {
|
||||||
|
outlinePaint.setColor(Color.BLACK);
|
||||||
|
canvas.drawRect(drawRect, outlinePaint);
|
||||||
|
} else {
|
||||||
|
Rect viewDrawingRect = new Rect();
|
||||||
|
viewContext.getDrawingRect(viewDrawingRect);
|
||||||
|
|
||||||
|
path.addRect(new RectF(drawRect), Path.Direction.CW);
|
||||||
|
outlinePaint.setColor(highlightColor);
|
||||||
|
|
||||||
|
if (isClipPathSupported(canvas)) {
|
||||||
|
canvas.clipPath(path, Region.Op.DIFFERENCE);
|
||||||
|
canvas.drawRect(viewDrawingRect, outsidePaint);
|
||||||
|
} else {
|
||||||
|
drawOutsideFallback(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.restore();
|
||||||
|
canvas.drawPath(path, outlinePaint);
|
||||||
|
|
||||||
|
if (showThirds) {
|
||||||
|
drawThirds(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showCircle) {
|
||||||
|
drawCircle(canvas);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handleMode == HandleMode.Always ||
|
||||||
|
(handleMode == HandleMode.Changing && modifyMode == ModifyMode.Grow)) {
|
||||||
|
drawHandles(canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fall back to naive method for darkening outside crop area
|
||||||
|
*/
|
||||||
|
private void drawOutsideFallback(Canvas canvas) {
|
||||||
|
canvas.drawRect(0, 0, canvas.getWidth(), drawRect.top, outsidePaint);
|
||||||
|
canvas.drawRect(0, drawRect.bottom, canvas.getWidth(), canvas.getHeight(), outsidePaint);
|
||||||
|
canvas.drawRect(0, drawRect.top, drawRect.left, drawRect.bottom, outsidePaint);
|
||||||
|
canvas.drawRect(drawRect.right, drawRect.top, canvas.getWidth(), drawRect.bottom, outsidePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Clip path is broken, unreliable or not supported on:
|
||||||
|
* - JellyBean MR1
|
||||||
|
* - ICS & ICS MR1 with hardware acceleration turned on
|
||||||
|
*/
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
private boolean isClipPathSupported(Canvas canvas) {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.JELLY_BEAN_MR1) {
|
||||||
|
return false;
|
||||||
|
} else if ((Build.VERSION.SDK_INT < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
|
||||||
|
|| Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return !canvas.isHardwareAccelerated();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawHandles(Canvas canvas) {
|
||||||
|
int xMiddle = drawRect.left + ((drawRect.right - drawRect.left) / 2);
|
||||||
|
int yMiddle = drawRect.top + ((drawRect.bottom - drawRect.top) / 2);
|
||||||
|
|
||||||
|
canvas.drawCircle(drawRect.left, yMiddle, handleRadius, handlePaint);
|
||||||
|
canvas.drawCircle(xMiddle, drawRect.top, handleRadius, handlePaint);
|
||||||
|
canvas.drawCircle(drawRect.right, yMiddle, handleRadius, handlePaint);
|
||||||
|
canvas.drawCircle(xMiddle, drawRect.bottom, handleRadius, handlePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawThirds(Canvas canvas) {
|
||||||
|
outlinePaint.setStrokeWidth(1);
|
||||||
|
float xThird = (drawRect.right - drawRect.left) / 3;
|
||||||
|
float yThird = (drawRect.bottom - drawRect.top) / 3;
|
||||||
|
|
||||||
|
canvas.drawLine(drawRect.left + xThird, drawRect.top,
|
||||||
|
drawRect.left + xThird, drawRect.bottom, outlinePaint);
|
||||||
|
canvas.drawLine(drawRect.left + xThird * 2, drawRect.top,
|
||||||
|
drawRect.left + xThird * 2, drawRect.bottom, outlinePaint);
|
||||||
|
canvas.drawLine(drawRect.left, drawRect.top + yThird,
|
||||||
|
drawRect.right, drawRect.top + yThird, outlinePaint);
|
||||||
|
canvas.drawLine(drawRect.left, drawRect.top + yThird * 2,
|
||||||
|
drawRect.right, drawRect.top + yThird * 2, outlinePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void drawCircle(Canvas canvas) {
|
||||||
|
outlinePaint.setStrokeWidth(1);
|
||||||
|
canvas.drawOval(new RectF(drawRect), outlinePaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMode(ModifyMode mode) {
|
||||||
|
if (mode != modifyMode) {
|
||||||
|
modifyMode = mode;
|
||||||
|
viewContext.invalidate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determines which edges are hit by touching at (x, y)
|
||||||
|
public int getHit(float x, float y) {
|
||||||
|
Rect r = computeLayout();
|
||||||
|
final float hysteresis = 20F;
|
||||||
|
int retval = GROW_NONE;
|
||||||
|
|
||||||
|
// verticalCheck makes sure the position is between the top and
|
||||||
|
// the bottom edge (with some tolerance). Similar for horizCheck.
|
||||||
|
boolean verticalCheck = (y >= r.top - hysteresis)
|
||||||
|
&& (y < r.bottom + hysteresis);
|
||||||
|
boolean horizCheck = (x >= r.left - hysteresis)
|
||||||
|
&& (x < r.right + hysteresis);
|
||||||
|
|
||||||
|
// Check whether the position is near some edge(s)
|
||||||
|
if ((Math.abs(r.left - x) < hysteresis) && verticalCheck) {
|
||||||
|
retval |= GROW_LEFT_EDGE;
|
||||||
|
}
|
||||||
|
if ((Math.abs(r.right - x) < hysteresis) && verticalCheck) {
|
||||||
|
retval |= GROW_RIGHT_EDGE;
|
||||||
|
}
|
||||||
|
if ((Math.abs(r.top - y) < hysteresis) && horizCheck) {
|
||||||
|
retval |= GROW_TOP_EDGE;
|
||||||
|
}
|
||||||
|
if ((Math.abs(r.bottom - y) < hysteresis) && horizCheck) {
|
||||||
|
retval |= GROW_BOTTOM_EDGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not near any edge but inside the rectangle: move
|
||||||
|
if (retval == GROW_NONE && r.contains((int) x, (int) y)) {
|
||||||
|
retval = MOVE;
|
||||||
|
}
|
||||||
|
return retval;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles motion (dx, dy) in screen space.
|
||||||
|
// The "edge" parameter specifies which edges the user is dragging.
|
||||||
|
void handleMotion(int edge, float dx, float dy) {
|
||||||
|
Rect r = computeLayout();
|
||||||
|
if (edge == MOVE) {
|
||||||
|
// Convert to image space before sending to moveBy()
|
||||||
|
moveBy(dx * (cropRect.width() / r.width()),
|
||||||
|
dy * (cropRect.height() / r.height()));
|
||||||
|
} else {
|
||||||
|
if (((GROW_LEFT_EDGE | GROW_RIGHT_EDGE) & edge) == 0) {
|
||||||
|
dx = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (((GROW_TOP_EDGE | GROW_BOTTOM_EDGE) & edge) == 0) {
|
||||||
|
dy = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert to image space before sending to growBy()
|
||||||
|
float xDelta = dx * (cropRect.width() / r.width());
|
||||||
|
float yDelta = dy * (cropRect.height() / r.height());
|
||||||
|
growBy((((edge & GROW_LEFT_EDGE) != 0) ? -1 : 1) * xDelta,
|
||||||
|
(((edge & GROW_TOP_EDGE) != 0) ? -1 : 1) * yDelta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grows the cropping rectangle by (dx, dy) in image space
|
||||||
|
void moveBy(float dx, float dy) {
|
||||||
|
Rect invalRect = new Rect(drawRect);
|
||||||
|
|
||||||
|
cropRect.offset(dx, dy);
|
||||||
|
|
||||||
|
// Put the cropping rectangle inside image rectangle
|
||||||
|
cropRect.offset(
|
||||||
|
Math.max(0, imageRect.left - cropRect.left),
|
||||||
|
Math.max(0, imageRect.top - cropRect.top));
|
||||||
|
|
||||||
|
cropRect.offset(
|
||||||
|
Math.min(0, imageRect.right - cropRect.right),
|
||||||
|
Math.min(0, imageRect.bottom - cropRect.bottom));
|
||||||
|
|
||||||
|
drawRect = computeLayout();
|
||||||
|
invalRect.union(drawRect);
|
||||||
|
invalRect.inset(-(int) handleRadius, -(int) handleRadius);
|
||||||
|
viewContext.invalidate(invalRect);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grows the cropping rectangle by (dx, dy) in image space.
|
||||||
|
void growBy(float dx, float dy) {
|
||||||
|
if (maintainAspectRatio) {
|
||||||
|
if (dx != 0) {
|
||||||
|
dy = dx / initialAspectRatio;
|
||||||
|
} else if (dy != 0) {
|
||||||
|
dx = dy * initialAspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Don't let the cropping rectangle grow too fast.
|
||||||
|
// Grow at most half of the difference between the image rectangle and
|
||||||
|
// the cropping rectangle.
|
||||||
|
RectF r = new RectF(cropRect);
|
||||||
|
if (dx > 0F && r.width() + 2 * dx > imageRect.width()) {
|
||||||
|
dx = (imageRect.width() - r.width()) / 2F;
|
||||||
|
if (maintainAspectRatio) {
|
||||||
|
dy = dx / initialAspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (dy > 0F && r.height() + 2 * dy > imageRect.height()) {
|
||||||
|
dy = (imageRect.height() - r.height()) / 2F;
|
||||||
|
if (maintainAspectRatio) {
|
||||||
|
dx = dy * initialAspectRatio;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
r.inset(-dx, -dy);
|
||||||
|
|
||||||
|
// Don't let the cropping rectangle shrink too fast
|
||||||
|
final float widthCap = 25F;
|
||||||
|
if (r.width() < widthCap) {
|
||||||
|
r.inset(-(widthCap - r.width()) / 2F, 0F);
|
||||||
|
}
|
||||||
|
float heightCap = maintainAspectRatio
|
||||||
|
? (widthCap / initialAspectRatio)
|
||||||
|
: widthCap;
|
||||||
|
if (r.height() < heightCap) {
|
||||||
|
r.inset(0F, -(heightCap - r.height()) / 2F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put the cropping rectangle inside the image rectangle
|
||||||
|
if (r.left < imageRect.left) {
|
||||||
|
r.offset(imageRect.left - r.left, 0F);
|
||||||
|
} else if (r.right > imageRect.right) {
|
||||||
|
r.offset(-(r.right - imageRect.right), 0F);
|
||||||
|
}
|
||||||
|
if (r.top < imageRect.top) {
|
||||||
|
r.offset(0F, imageRect.top - r.top);
|
||||||
|
} else if (r.bottom > imageRect.bottom) {
|
||||||
|
r.offset(0F, -(r.bottom - imageRect.bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
cropRect.set(r);
|
||||||
|
drawRect = computeLayout();
|
||||||
|
viewContext.invalidate();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the cropping rectangle in image space with specified scale
|
||||||
|
public Rect getScaledCropRect(float scale) {
|
||||||
|
return new Rect((int) (cropRect.left * scale), (int) (cropRect.top * scale),
|
||||||
|
(int) (cropRect.right * scale), (int) (cropRect.bottom * scale));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps the cropping rectangle from image space to screen space
|
||||||
|
private Rect computeLayout() {
|
||||||
|
RectF r = new RectF(cropRect.left, cropRect.top,
|
||||||
|
cropRect.right, cropRect.bottom);
|
||||||
|
matrix.mapRect(r);
|
||||||
|
return new Rect(Math.round(r.left), Math.round(r.top),
|
||||||
|
Math.round(r.right), Math.round(r.bottom));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void invalidate() {
|
||||||
|
drawRect = computeLayout();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasFocus() {
|
||||||
|
return isFocused;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFocus(boolean isFocused) {
|
||||||
|
this.isFocused = isFocused;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,400 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
import android.graphics.RectF;
|
||||||
|
import android.graphics.drawable.Drawable;
|
||||||
|
import android.os.Handler;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.KeyEvent;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from original in AOSP.
|
||||||
|
*/
|
||||||
|
abstract class ImageViewTouchBase extends ImageView {
|
||||||
|
|
||||||
|
private static final float SCALE_RATE = 1.25F;
|
||||||
|
|
||||||
|
// This is the base transformation which is used to show the image
|
||||||
|
// initially. The current computation for this shows the image in
|
||||||
|
// it's entirety, letterboxing as needed. One could choose to
|
||||||
|
// show the image as cropped instead.
|
||||||
|
//
|
||||||
|
// This matrix is recomputed when we go from the thumbnail image to
|
||||||
|
// the full size image.
|
||||||
|
protected Matrix baseMatrix = new Matrix();
|
||||||
|
|
||||||
|
// This is the supplementary transformation which reflects what
|
||||||
|
// the user has done in terms of zooming and panning.
|
||||||
|
//
|
||||||
|
// This matrix remains the same when we go from the thumbnail image
|
||||||
|
// to the full size image.
|
||||||
|
protected Matrix suppMatrix = new Matrix();
|
||||||
|
|
||||||
|
// This is the final matrix which is computed as the concatentation
|
||||||
|
// of the base matrix and the supplementary matrix.
|
||||||
|
private final Matrix displayMatrix = new Matrix();
|
||||||
|
|
||||||
|
// Temporary buffer used for getting the values out of a matrix.
|
||||||
|
private final float[] matrixValues = new float[9];
|
||||||
|
|
||||||
|
// The current bitmap being displayed.
|
||||||
|
protected final RotateBitmap bitmapDisplayed = new RotateBitmap(null, 0);
|
||||||
|
|
||||||
|
int thisWidth = -1;
|
||||||
|
int thisHeight = -1;
|
||||||
|
|
||||||
|
float maxZoom;
|
||||||
|
|
||||||
|
private Runnable onLayoutRunnable;
|
||||||
|
|
||||||
|
protected Handler handler = new Handler();
|
||||||
|
|
||||||
|
// ImageViewTouchBase will pass a Bitmap to the Recycler if it has finished
|
||||||
|
// its use of that Bitmap
|
||||||
|
public interface Recycler {
|
||||||
|
public void recycle(Bitmap b);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Recycler recycler;
|
||||||
|
|
||||||
|
public ImageViewTouchBase(Context context) {
|
||||||
|
super(context);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageViewTouchBase(Context context, AttributeSet attrs) {
|
||||||
|
super(context, attrs);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImageViewTouchBase(Context context, AttributeSet attrs, int defStyle) {
|
||||||
|
super(context, attrs, defStyle);
|
||||||
|
init();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRecycler(Recycler recycler) {
|
||||||
|
this.recycler = recycler;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
|
||||||
|
super.onLayout(changed, left, top, right, bottom);
|
||||||
|
thisWidth = right - left;
|
||||||
|
thisHeight = bottom - top;
|
||||||
|
Runnable r = onLayoutRunnable;
|
||||||
|
if (r != null) {
|
||||||
|
onLayoutRunnable = null;
|
||||||
|
r.run();
|
||||||
|
}
|
||||||
|
if (bitmapDisplayed.getBitmap() != null) {
|
||||||
|
getProperBaseMatrix(bitmapDisplayed, baseMatrix, true);
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyDown(int keyCode, KeyEvent event) {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) {
|
||||||
|
event.startTracking();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return super.onKeyDown(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onKeyUp(int keyCode, KeyEvent event) {
|
||||||
|
if (keyCode == KeyEvent.KEYCODE_BACK && event.isTracking() && !event.isCanceled()) {
|
||||||
|
if (getScale() > 1.0f) {
|
||||||
|
// If we're zoomed in, pressing Back jumps out to show the
|
||||||
|
// entire image, otherwise Back returns the user to the gallery
|
||||||
|
zoomTo(1.0f);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.onKeyUp(keyCode, event);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setImageBitmap(Bitmap bitmap) {
|
||||||
|
setImageBitmap(bitmap, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setImageBitmap(Bitmap bitmap, int rotation) {
|
||||||
|
super.setImageBitmap(bitmap);
|
||||||
|
Drawable d = getDrawable();
|
||||||
|
if (d != null) {
|
||||||
|
d.setDither(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Bitmap old = bitmapDisplayed.getBitmap();
|
||||||
|
bitmapDisplayed.setBitmap(bitmap);
|
||||||
|
bitmapDisplayed.setRotation(rotation);
|
||||||
|
|
||||||
|
if (old != null && old != bitmap && recycler != null) {
|
||||||
|
recycler.recycle(old);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void clear() {
|
||||||
|
setImageBitmapResetBase(null, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// This function changes bitmap, reset base matrix according to the size
|
||||||
|
// of the bitmap, and optionally reset the supplementary matrix
|
||||||
|
public void setImageBitmapResetBase(final Bitmap bitmap, final boolean resetSupp) {
|
||||||
|
setImageRotateBitmapResetBase(new RotateBitmap(bitmap, 0), resetSupp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageRotateBitmapResetBase(final RotateBitmap bitmap, final boolean resetSupp) {
|
||||||
|
final int viewWidth = getWidth();
|
||||||
|
|
||||||
|
if (viewWidth <= 0) {
|
||||||
|
onLayoutRunnable = new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
setImageRotateBitmapResetBase(bitmap, resetSupp);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bitmap.getBitmap() != null) {
|
||||||
|
getProperBaseMatrix(bitmap, baseMatrix, true);
|
||||||
|
setImageBitmap(bitmap.getBitmap(), bitmap.getRotation());
|
||||||
|
} else {
|
||||||
|
baseMatrix.reset();
|
||||||
|
setImageBitmap(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resetSupp) {
|
||||||
|
suppMatrix.reset();
|
||||||
|
}
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
maxZoom = calculateMaxZoom();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Center as much as possible in one or both axis. Centering is defined as follows:
|
||||||
|
// * If the image is scaled down below the view's dimensions then center it.
|
||||||
|
// * If the image is scaled larger than the view and is translated out of view then translate it back into view.
|
||||||
|
protected void center() {
|
||||||
|
final Bitmap bitmap = bitmapDisplayed.getBitmap();
|
||||||
|
if (bitmap == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Matrix m = getImageViewMatrix();
|
||||||
|
|
||||||
|
RectF rect = new RectF(0, 0, bitmap.getWidth(), bitmap.getHeight());
|
||||||
|
m.mapRect(rect);
|
||||||
|
|
||||||
|
float height = rect.height();
|
||||||
|
float width = rect.width();
|
||||||
|
|
||||||
|
float deltaX = 0, deltaY = 0;
|
||||||
|
|
||||||
|
deltaY = centerVertical(rect, height, deltaY);
|
||||||
|
deltaX = centerHorizontal(rect, width, deltaX);
|
||||||
|
|
||||||
|
postTranslate(deltaX, deltaY);
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
private float centerVertical(RectF rect, float height, float deltaY) {
|
||||||
|
int viewHeight = getHeight();
|
||||||
|
if (height < viewHeight) {
|
||||||
|
deltaY = (viewHeight - height) / 2 - rect.top;
|
||||||
|
} else if (rect.top > 0) {
|
||||||
|
deltaY = -rect.top;
|
||||||
|
} else if (rect.bottom < viewHeight) {
|
||||||
|
deltaY = getHeight() - rect.bottom;
|
||||||
|
}
|
||||||
|
return deltaY;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float centerHorizontal(RectF rect, float width, float deltaX) {
|
||||||
|
int viewWidth = getWidth();
|
||||||
|
if (width < viewWidth) {
|
||||||
|
deltaX = (viewWidth - width) / 2 - rect.left;
|
||||||
|
} else if (rect.left > 0) {
|
||||||
|
deltaX = -rect.left;
|
||||||
|
} else if (rect.right < viewWidth) {
|
||||||
|
deltaX = viewWidth - rect.right;
|
||||||
|
}
|
||||||
|
return deltaX;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void init() {
|
||||||
|
setScaleType(ImageView.ScaleType.MATRIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float getValue(Matrix matrix, int whichValue) {
|
||||||
|
matrix.getValues(matrixValues);
|
||||||
|
return matrixValues[whichValue];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the scale factor out of the matrix.
|
||||||
|
protected float getScale(Matrix matrix) {
|
||||||
|
return getValue(matrix, Matrix.MSCALE_X);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float getScale() {
|
||||||
|
return getScale(suppMatrix);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the base matrix so that the image is centered and scaled properly.
|
||||||
|
private void getProperBaseMatrix(RotateBitmap bitmap, Matrix matrix, boolean includeRotation) {
|
||||||
|
float viewWidth = getWidth();
|
||||||
|
float viewHeight = getHeight();
|
||||||
|
|
||||||
|
float w = bitmap.getWidth();
|
||||||
|
float h = bitmap.getHeight();
|
||||||
|
matrix.reset();
|
||||||
|
|
||||||
|
// We limit up-scaling to 3x otherwise the result may look bad if it's a small icon
|
||||||
|
float widthScale = Math.min(viewWidth / w, 3.0f);
|
||||||
|
float heightScale = Math.min(viewHeight / h, 3.0f);
|
||||||
|
float scale = Math.min(widthScale, heightScale);
|
||||||
|
|
||||||
|
if (includeRotation) {
|
||||||
|
matrix.postConcat(bitmap.getRotateMatrix());
|
||||||
|
}
|
||||||
|
matrix.postScale(scale, scale);
|
||||||
|
matrix.postTranslate((viewWidth - w * scale) / 2F, (viewHeight - h * scale) / 2F);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine the base matrix and the supp matrix to make the final matrix
|
||||||
|
protected Matrix getImageViewMatrix() {
|
||||||
|
// The final matrix is computed as the concatentation of the base matrix
|
||||||
|
// and the supplementary matrix
|
||||||
|
displayMatrix.set(baseMatrix);
|
||||||
|
displayMatrix.postConcat(suppMatrix);
|
||||||
|
return displayMatrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Matrix getUnrotatedMatrix(){
|
||||||
|
Matrix unrotated = new Matrix();
|
||||||
|
getProperBaseMatrix(bitmapDisplayed, unrotated, false);
|
||||||
|
unrotated.postConcat(suppMatrix);
|
||||||
|
return unrotated;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected float calculateMaxZoom() {
|
||||||
|
if (bitmapDisplayed.getBitmap() == null) {
|
||||||
|
return 1F;
|
||||||
|
}
|
||||||
|
|
||||||
|
float fw = (float) bitmapDisplayed.getWidth() / (float) thisWidth;
|
||||||
|
float fh = (float) bitmapDisplayed.getHeight() / (float) thisHeight;
|
||||||
|
return Math.max(fw, fh) * 4; // 400%
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomTo(float scale, float centerX, float centerY) {
|
||||||
|
if (scale > maxZoom) {
|
||||||
|
scale = maxZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
float oldScale = getScale();
|
||||||
|
float deltaScale = scale / oldScale;
|
||||||
|
|
||||||
|
suppMatrix.postScale(deltaScale, deltaScale, centerX, centerY);
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
center();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomTo(final float scale, final float centerX,
|
||||||
|
final float centerY, final float durationMs) {
|
||||||
|
final float incrementPerMs = (scale - getScale()) / durationMs;
|
||||||
|
final float oldScale = getScale();
|
||||||
|
final long startTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
handler.post(new Runnable() {
|
||||||
|
public void run() {
|
||||||
|
long now = System.currentTimeMillis();
|
||||||
|
float currentMs = Math.min(durationMs, now - startTime);
|
||||||
|
float target = oldScale + (incrementPerMs * currentMs);
|
||||||
|
zoomTo(target, centerX, centerY);
|
||||||
|
|
||||||
|
if (currentMs < durationMs) {
|
||||||
|
handler.post(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomTo(float scale) {
|
||||||
|
float cx = getWidth() / 2F;
|
||||||
|
float cy = getHeight() / 2F;
|
||||||
|
zoomTo(scale, cx, cy);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomIn() {
|
||||||
|
zoomIn(SCALE_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomOut() {
|
||||||
|
zoomOut(SCALE_RATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomIn(float rate) {
|
||||||
|
if (getScale() >= maxZoom) {
|
||||||
|
return; // Don't let the user zoom into the molecular level
|
||||||
|
}
|
||||||
|
if (bitmapDisplayed.getBitmap() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float cx = getWidth() / 2F;
|
||||||
|
float cy = getHeight() / 2F;
|
||||||
|
|
||||||
|
suppMatrix.postScale(rate, rate, cx, cy);
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void zoomOut(float rate) {
|
||||||
|
if (bitmapDisplayed.getBitmap() == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
float cx = getWidth() / 2F;
|
||||||
|
float cy = getHeight() / 2F;
|
||||||
|
|
||||||
|
// Zoom out to at most 1x
|
||||||
|
Matrix tmp = new Matrix(suppMatrix);
|
||||||
|
tmp.postScale(1F / rate, 1F / rate, cx, cy);
|
||||||
|
|
||||||
|
if (getScale(tmp) < 1F) {
|
||||||
|
suppMatrix.setScale(1F, 1F, cx, cy);
|
||||||
|
} else {
|
||||||
|
suppMatrix.postScale(1F / rate, 1F / rate, cx, cy);
|
||||||
|
}
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
center();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void postTranslate(float dx, float dy) {
|
||||||
|
suppMatrix.postTranslate(dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void panBy(float dx, float dy) {
|
||||||
|
postTranslate(dx, dy);
|
||||||
|
setImageMatrix(getImageViewMatrix());
|
||||||
|
}
|
||||||
|
}
|
15
libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
class Log {
|
||||||
|
|
||||||
|
private static final String TAG = "android-crop";
|
||||||
|
|
||||||
|
public static void e(String msg) {
|
||||||
|
android.util.Log.e(TAG, msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void e(String msg, Throwable e) {
|
||||||
|
android.util.Log.e(TAG, msg, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from original in AOSP.
|
||||||
|
*/
|
||||||
|
abstract class MonitoredActivity extends Activity {
|
||||||
|
|
||||||
|
private final ArrayList<LifeCycleListener> listeners = new ArrayList<LifeCycleListener>();
|
||||||
|
|
||||||
|
public static interface LifeCycleListener {
|
||||||
|
public void onActivityCreated(MonitoredActivity activity);
|
||||||
|
public void onActivityDestroyed(MonitoredActivity activity);
|
||||||
|
public void onActivityStarted(MonitoredActivity activity);
|
||||||
|
public void onActivityStopped(MonitoredActivity activity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class LifeCycleAdapter implements LifeCycleListener {
|
||||||
|
public void onActivityCreated(MonitoredActivity activity) {}
|
||||||
|
public void onActivityDestroyed(MonitoredActivity activity) {}
|
||||||
|
public void onActivityStarted(MonitoredActivity activity) {}
|
||||||
|
public void onActivityStopped(MonitoredActivity activity) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void addLifeCycleListener(LifeCycleListener listener) {
|
||||||
|
if (listeners.contains(listener)) return;
|
||||||
|
listeners.add(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeLifeCycleListener(LifeCycleListener listener) {
|
||||||
|
listeners.remove(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
for (LifeCycleListener listener : listeners) {
|
||||||
|
listener.onActivityCreated(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
for (LifeCycleListener listener : listeners) {
|
||||||
|
listener.onActivityDestroyed(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStart() {
|
||||||
|
super.onStart();
|
||||||
|
for (LifeCycleListener listener : listeners) {
|
||||||
|
listener.onActivityStarted(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onStop() {
|
||||||
|
super.onStop();
|
||||||
|
for (LifeCycleListener listener : listeners) {
|
||||||
|
listener.onActivityStopped(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2009 The Android Open Source Project
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.graphics.Bitmap;
|
||||||
|
import android.graphics.Matrix;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Modified from original in AOSP.
|
||||||
|
*/
|
||||||
|
class RotateBitmap {
|
||||||
|
|
||||||
|
private Bitmap bitmap;
|
||||||
|
private int rotation;
|
||||||
|
|
||||||
|
public RotateBitmap(Bitmap bitmap, int rotation) {
|
||||||
|
this.bitmap = bitmap;
|
||||||
|
this.rotation = rotation % 360;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setRotation(int rotation) {
|
||||||
|
this.rotation = rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getRotation() {
|
||||||
|
return rotation;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Bitmap getBitmap() {
|
||||||
|
return bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBitmap(Bitmap bitmap) {
|
||||||
|
this.bitmap = bitmap;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Matrix getRotateMatrix() {
|
||||||
|
// By default this is an identity matrix
|
||||||
|
Matrix matrix = new Matrix();
|
||||||
|
if (bitmap != null && rotation != 0) {
|
||||||
|
// We want to do the rotation at origin, but since the bounding
|
||||||
|
// rectangle will be changed after rotation, so the delta values
|
||||||
|
// are based on old & new width/height respectively.
|
||||||
|
int cx = bitmap.getWidth() / 2;
|
||||||
|
int cy = bitmap.getHeight() / 2;
|
||||||
|
matrix.preTranslate(-cx, -cy);
|
||||||
|
matrix.postRotate(rotation);
|
||||||
|
matrix.postTranslate(getWidth() / 2, getHeight() / 2);
|
||||||
|
}
|
||||||
|
return matrix;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isOrientationChanged() {
|
||||||
|
return (rotation / 90) % 2 != 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getHeight() {
|
||||||
|
if (bitmap == null) return 0;
|
||||||
|
if (isOrientationChanged()) {
|
||||||
|
return bitmap.getWidth();
|
||||||
|
} else {
|
||||||
|
return bitmap.getHeight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getWidth() {
|
||||||
|
if (bitmap == null) return 0;
|
||||||
|
if (isOrientationChanged()) {
|
||||||
|
return bitmap.getHeight();
|
||||||
|
} else {
|
||||||
|
return bitmap.getWidth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void recycle() {
|
||||||
|
if (bitmap != null) {
|
||||||
|
bitmap.recycle();
|
||||||
|
bitmap = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
48
libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package com.soundcloud.crop;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.os.Environment;
|
||||||
|
import android.text.TextUtils;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
|
||||||
|
public class UriUtil {
|
||||||
|
|
||||||
|
public final static String getFileProviderName(Context context){
|
||||||
|
return context.getPackageName()+".fileprovider";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将TakePhoto 提供的Uri 解析出文件绝对路径
|
||||||
|
*
|
||||||
|
* @param uri
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String parseOwnUri(Context context, Uri uri) {
|
||||||
|
if (uri == null) return null;
|
||||||
|
String path;
|
||||||
|
if (TextUtils.equals(uri.getAuthority(), getFileProviderName(context))) {
|
||||||
|
String target_text_camera_photos = "camera_photos/";
|
||||||
|
if (uri.getPath() != null && uri.getPath().contains(target_text_camera_photos)) {
|
||||||
|
path = new File(uri.getPath().replace(target_text_camera_photos, ""))
|
||||||
|
.getAbsolutePath();
|
||||||
|
} else {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
|
path = new File(Environment.getExternalStorageDirectory(),
|
||||||
|
uri.getPath())
|
||||||
|
.getAbsolutePath();
|
||||||
|
} else {
|
||||||
|
path = uri.getPath();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
path = uri.getPath();
|
||||||
|
}
|
||||||
|
return path;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 112 B |
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 240 B |
BIN
libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 230 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 112 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 196 B |
BIN
libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 204 B |
@@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<ripple xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:color="@color/crop__selector_pressed">
|
||||||
|
<item android:drawable="@color/crop__button_bar" />
|
||||||
|
</ripple>
|
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png
Normal file
After Width: | Height: | Size: 119 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp
Normal file
After Width: | Height: | Size: 270 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp
Normal file
After Width: | Height: | Size: 304 B |
BIN
libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp
Normal file
After Width: | Height: | Size: 88 B |
@@ -0,0 +1,20 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:exitFadeDuration="@android:integer/config_mediumAnimTime">
|
||||||
|
|
||||||
|
<item android:state_pressed="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/crop__selector_pressed" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:state_focused="true">
|
||||||
|
<shape>
|
||||||
|
<solid android:color="@color/crop__selector_focused" />
|
||||||
|
</shape>
|
||||||
|
</item>
|
||||||
|
|
||||||
|
<item android:drawable="@android:color/transparent" />
|
||||||
|
|
||||||
|
</selector>
|
5
libs/lib_crop/src/main/res/drawable/crop__texture.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<bitmap xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:src="@drawable/crop__tile"
|
||||||
|
android:tileMode="repeat" />
|
19
libs/lib_crop/src/main/res/layout/crop__activity_crop.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent">
|
||||||
|
|
||||||
|
<include
|
||||||
|
android:id="@+id/done_cancel_bar"
|
||||||
|
layout="@layout/crop__layout_done_cancel" />
|
||||||
|
|
||||||
|
<com.soundcloud.crop.CropImageView
|
||||||
|
android:id="@+id/crop_image"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@drawable/crop__texture"
|
||||||
|
android:layout_below="@id/done_cancel_bar" />
|
||||||
|
|
||||||
|
</RelativeLayout>
|
@@ -0,0 +1,16 @@
|
|||||||
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
style="@style/Crop.DoneCancelBar">
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/btn_cancel"
|
||||||
|
style="@style/Crop.ActionButton">
|
||||||
|
<TextView style="@style/Crop.ActionButtonText.Cancel" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
<FrameLayout
|
||||||
|
android:id="@+id/btn_done"
|
||||||
|
style="@style/Crop.ActionButton">
|
||||||
|
<TextView style="@style/Crop.ActionButtonText.Done" />
|
||||||
|
</FrameLayout>
|
||||||
|
|
||||||
|
</LinearLayout>
|
8
libs/lib_crop/src/main/res/values-ar/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string name="crop__saving">جارٍ حفظ الصورة...</string>
|
||||||
|
<string name="crop__wait">يرجى الانتظار...</string>
|
||||||
|
<string name="crop__pick_error">لا يوجد مصدر صور متاح</string>
|
||||||
|
|
||||||
|
<string name="crop__done">تم</string>
|
||||||
|
<string name="crop__cancel" tools:ignore="ButtonCase">إلغاء</string>
|
||||||
|
</resources>
|
5
libs/lib_crop/src/main/res/values-v21/colors.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<color name="crop__selector_pressed">#aaaaaa</color>
|
||||||
|
|
||||||
|
</resources>
|
8
libs/lib_crop/src/main/res/values-zh-rTW/strings.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
<string name="crop__saving">正在儲存相片…</string>
|
||||||
|
<string name="crop__wait">請稍候…</string>
|
||||||
|
<string name="crop__pick_error">沒有可用的圖片來源</string>
|
||||||
|
|
||||||
|
<string name="crop__done">完成</string>
|
||||||
|
<string name="crop__cancel" tools:ignore="ButtonCase">取消</string>
|
||||||
|
</resources>
|
16
libs/lib_crop/src/main/res/values/attrs.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<attr name="cropImageStyle" format="reference" />
|
||||||
|
|
||||||
|
<declare-styleable name="CropImageView">
|
||||||
|
<attr name="highlightColor" format="reference|color" />
|
||||||
|
<attr name="showThirds" format="boolean" />
|
||||||
|
<attr name="showCircle" format="boolean" />
|
||||||
|
<attr name="showHandles">
|
||||||
|
<enum name="changing" value="0" />
|
||||||
|
<enum name="always" value="1" />
|
||||||
|
<enum name="never" value="2" />
|
||||||
|
</attr>
|
||||||
|
</declare-styleable>
|
||||||
|
|
||||||
|
</resources>
|
8
libs/lib_crop/src/main/res/values/colors.xml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<color name="crop__button_bar">#f3f3f3</color>
|
||||||
|
<color name="crop__button_text">#666666</color>
|
||||||
|
<color name="crop__selector_pressed">#1a000000</color>
|
||||||
|
<color name="crop__selector_focused">#77000000</color>
|
||||||
|
|
||||||
|
</resources>
|
5
libs/lib_crop/src/main/res/values/dimens.xml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<resources>
|
||||||
|
|
||||||
|
<dimen name="crop__bar_height">56dp</dimen>
|
||||||
|
|
||||||
|
</resources>
|
10
libs/lib_crop/src/main/res/values/strings.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<string name="crop__saving">Saving photo…</string>
|
||||||
|
<string name="crop__wait">Please wait…</string>
|
||||||
|
<string name="crop__pick_error">No image source available</string>
|
||||||
|
|
||||||
|
<string name="crop__done">Done</string>
|
||||||
|
<string name="crop__cancel" tools:ignore="ButtonCase">Cancel</string>
|
||||||
|
|
||||||
|
</resources>
|
44
libs/lib_crop/src/main/res/values/styles.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
<style name="Crop"></style>
|
||||||
|
|
||||||
|
<style name="Crop.DoneCancelBar">
|
||||||
|
<item name="android:layout_width">match_parent</item>
|
||||||
|
<item name="android:layout_height">@dimen/crop__bar_height</item>
|
||||||
|
<item name="android:orientation">horizontal</item>
|
||||||
|
<item name="android:divider">@drawable/crop__divider</item>
|
||||||
|
<item name="android:showDividers" tools:ignore="NewApi">middle</item>
|
||||||
|
<item name="android:dividerPadding" tools:ignore="NewApi">12dp</item>
|
||||||
|
<item name="android:background">@color/crop__button_bar</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Crop.ActionButton">
|
||||||
|
<item name="android:layout_width">0dp</item>
|
||||||
|
<item name="android:layout_height">match_parent</item>
|
||||||
|
<item name="android:layout_weight">1</item>
|
||||||
|
<item name="android:background">@drawable/crop__selectable_background</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Crop.ActionButtonText">
|
||||||
|
<item name="android:layout_width">wrap_content</item>
|
||||||
|
<item name="android:layout_height">wrap_content</item>
|
||||||
|
<item name="android:layout_gravity">center</item>
|
||||||
|
<item name="android:gravity">center_vertical</item>
|
||||||
|
<item name="android:paddingEnd">20dp</item> <!-- Offsets left drawable -->
|
||||||
|
<item name="android:drawablePadding">8dp</item>
|
||||||
|
<item name="android:textColor">@color/crop__button_text</item>
|
||||||
|
<item name="android:textStyle">bold</item>
|
||||||
|
<item name="android:textSize">13sp</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Crop.ActionButtonText.Done">
|
||||||
|
<item name="android:drawableStart">@drawable/crop__ic_done</item>
|
||||||
|
<item name="android:text">@string/crop__done</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style name="Crop.ActionButtonText.Cancel">
|
||||||
|
<item name="android:drawableStart">@drawable/crop__ic_cancel</item>
|
||||||
|
<item name="android:text">@string/crop__cancel</item>
|
||||||
|
</style>
|
||||||
|
|
||||||
|
</resources>
|
1
libs/lib_encipher/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
15
libs/lib_encipher/build.gradle
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
apply from : "../lib_standard.gradle"
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace 'com.example.lib_encipher'
|
||||||
|
|
||||||
|
sourceSets{
|
||||||
|
main{
|
||||||
|
jniLibs.srcDirs = ['libs']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
api "androidx.core:core-ktx:1.9.0"
|
||||||
|
}
|
0
libs/lib_encipher/consumer-rules.pro
Normal file
BIN
libs/lib_encipher/libs/arm64-v8a/libencipher.so
Executable file
BIN
libs/lib_encipher/libs/armeabi-v7a/libencipher.so
Executable file
BIN
libs/lib_encipher/libs/x86/libencipher.so
Executable file
BIN
libs/lib_encipher/libs/x86_64/libencipher.so
Executable file
21
libs/lib_encipher/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_encipher/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>
|
@@ -0,0 +1,58 @@
|
|||||||
|
package com.secure.encipher
|
||||||
|
|
||||||
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
object EncipherLib {
|
||||||
|
|
||||||
|
init {
|
||||||
|
System.loadLibrary("encipher")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptText(enc: String, key: String): String {
|
||||||
|
return replaceBlank(encrypt(replaceBlank(enc), key) ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptText(dec: String, key: String): String {
|
||||||
|
return decrypt(dec, key) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptTextDef1(enc: String): String {
|
||||||
|
return replaceBlank(encryptDef(replaceBlank(enc)) ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptTextDef1(dec: String): String {
|
||||||
|
return decryptDef(dec) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
fun encryptTextDef2(enc: String): String {
|
||||||
|
return replaceBlank(encryptDef2(replaceBlank(enc)) ?: "")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decryptTextDef2(dec: String): String {
|
||||||
|
return decryptDef2(dec) ?: ""
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun replaceBlank(str: String): String {
|
||||||
|
val p = Pattern.compile("\\s*|\t|\r|\n")
|
||||||
|
val m = p.matcher(str)
|
||||||
|
return m.replaceAll("")
|
||||||
|
}
|
||||||
|
|
||||||
|
// DES
|
||||||
|
private external fun encrypt(enc: String, key: String): String?
|
||||||
|
|
||||||
|
// DES
|
||||||
|
private external fun decrypt(dec: String, key: String): String?
|
||||||
|
|
||||||
|
// DES-默认KEY1
|
||||||
|
private external fun encryptDef(enc: String): String?
|
||||||
|
|
||||||
|
// DES-默认KEY1(1ea53d26)
|
||||||
|
private external fun decryptDef(dec: String): String?
|
||||||
|
|
||||||
|
// DES-默认KEY2
|
||||||
|
private external fun encryptDef2(enc: String): String?
|
||||||
|
|
||||||
|
// DES-默认KEY2(9fa73e66)
|
||||||
|
private external fun decryptDef2(dec: String): String?
|
||||||
|
}
|
40
libs/lib_standard.gradle
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
apply plugin: "com.android.library"
|
||||||
|
apply plugin: "org.jetbrains.kotlin.android"
|
||||||
|
apply plugin: "kotlin-android"
|
||||||
|
apply plugin: "kotlin-kapt"
|
||||||
|
|
||||||
|
android {
|
||||||
|
compileSdkVersion COMPILE_SDK_VERSION.toInteger()
|
||||||
|
defaultConfig {
|
||||||
|
minSdkVersion MIN_SDK_VERSION.toInteger()
|
||||||
|
targetSdkVersion TARGET_SDK_VERSION.toInteger()
|
||||||
|
versionCode 1
|
||||||
|
versionName "1.0.0"
|
||||||
|
consumerProguardFiles 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
buildTypes {
|
||||||
|
release {
|
||||||
|
minifyEnabled true
|
||||||
|
zipAlignEnabled true
|
||||||
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
|
}
|
||||||
|
debug {
|
||||||
|
debuggable true
|
||||||
|
minifyEnabled false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility JavaVersion.VERSION_11
|
||||||
|
targetCompatibility JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = '11'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
|
}
|
||||||
|
|
1
libs/lib_utils/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
/build
|
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
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
@@ -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
@@ -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
@@ -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
@@ -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()
|
||||||
|
}
|
||||||
|
}
|