diff --git a/libs/lib_core/.gitignore b/libs/lib_core/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lib_core/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/lib_core/build.gradle b/libs/lib_core/build.gradle new file mode 100644 index 0000000..f7b67d6 --- /dev/null +++ b/libs/lib_core/build.gradle @@ -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' // 检查最新版本 + +} \ No newline at end of file diff --git a/libs/lib_core/consumer-rules.pro b/libs/lib_core/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_core/proguard-rules.pro b/libs/lib_core/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/libs/lib_core/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/libs/lib_core/src/main/AndroidManifest.xml b/libs/lib_core/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/libs/lib_core/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt b/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt new file mode 100644 index 0000000..c9a5391 --- /dev/null +++ b/libs/lib_core/src/main/java/com/example/lib_core/LifecycleCleared.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt b/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt new file mode 100644 index 0000000..14d3247 --- /dev/null +++ b/libs/lib_core/src/main/java/com/example/lib_core/component/SuperBottomSheetDialog.kt @@ -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() + } +} \ No newline at end of file diff --git a/libs/lib_crop/.gitignore b/libs/lib_crop/.gitignore new file mode 100644 index 0000000..36af493 --- /dev/null +++ b/libs/lib_crop/.gitignore @@ -0,0 +1,3 @@ +/build +*.iml +*.DS_Store \ No newline at end of file diff --git a/libs/lib_crop/build.gradle b/libs/lib_crop/build.gradle new file mode 100644 index 0000000..fe71656 --- /dev/null +++ b/libs/lib_crop/build.gradle @@ -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() +} + diff --git a/libs/lib_crop/src/main/AndroidManifest.xml b/libs/lib_crop/src/main/AndroidManifest.xml new file mode 100644 index 0000000..abc38ad --- /dev/null +++ b/libs/lib_crop/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java new file mode 100644 index 0000000..f3fcc2b --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/Crop.java @@ -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(); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java new file mode 100644 index 0000000..782cfe5 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageActivity.java @@ -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)); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java new file mode 100644 index 0000000..4ad26c7 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropImageView.java @@ -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 highlightViews = new ArrayList(); + 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(); + } +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java new file mode 100644 index 0000000..2f945e3 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/CropUtil.java @@ -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(); + } + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java new file mode 100644 index 0000000..ec0b764 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/HighlightView.java @@ -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; + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java new file mode 100644 index 0000000..64dc841 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/ImageViewTouchBase.java @@ -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()); + } +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java new file mode 100644 index 0000000..0522a62 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/Log.java @@ -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); + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java new file mode 100644 index 0000000..91e6e72 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/MonitoredActivity.java @@ -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 listeners = new ArrayList(); + + 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); + } + } + +} diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java new file mode 100644 index 0000000..1b1f9a2 --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/RotateBitmap.java @@ -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; + } + } +} + diff --git a/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java new file mode 100644 index 0000000..271202f --- /dev/null +++ b/libs/lib_crop/src/main/java/com/soundcloud/crop/UriUtil.java @@ -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; + } + +} diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png new file mode 100644 index 0000000..373e5d8 Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__divider.9.png differ diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp new file mode 100644 index 0000000..b929a2b Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_cancel.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp new file mode 100644 index 0000000..5719d1f Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-hdpi/crop__ic_done.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png new file mode 100644 index 0000000..373e5d8 Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__divider.9.png differ diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp new file mode 100644 index 0000000..54dd1eb Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_cancel.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp new file mode 100644 index 0000000..6be409d Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-mdpi/crop__ic_done.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml new file mode 100644 index 0000000..2b25609 --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable-v21/crop__selectable_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png new file mode 100644 index 0000000..b3da354 Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__divider.9.png differ diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp new file mode 100644 index 0000000..a76ecfe Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_cancel.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp new file mode 100644 index 0000000..b6230d2 Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__ic_done.webp differ diff --git a/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp new file mode 100644 index 0000000..8060775 Binary files /dev/null and b/libs/lib_crop/src/main/res/drawable-xhdpi/crop__tile.webp differ diff --git a/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml new file mode 100644 index 0000000..83c4a7d --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable/crop__selectable_background.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/drawable/crop__texture.xml b/libs/lib_crop/src/main/res/drawable/crop__texture.xml new file mode 100644 index 0000000..23ec469 --- /dev/null +++ b/libs/lib_crop/src/main/res/drawable/crop__texture.xml @@ -0,0 +1,5 @@ + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml new file mode 100644 index 0000000..c133df0 --- /dev/null +++ b/libs/lib_crop/src/main/res/layout/crop__activity_crop.xml @@ -0,0 +1,19 @@ + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml new file mode 100644 index 0000000..fe07063 --- /dev/null +++ b/libs/lib_crop/src/main/res/layout/crop__layout_done_cancel.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + diff --git a/libs/lib_crop/src/main/res/values-ar/strings.xml b/libs/lib_crop/src/main/res/values-ar/strings.xml new file mode 100644 index 0000000..b10c50a --- /dev/null +++ b/libs/lib_crop/src/main/res/values-ar/strings.xml @@ -0,0 +1,8 @@ + + جارٍ حفظ الصورة... + يرجى الانتظار... + لا يوجد مصدر صور متاح + + تم + إلغاء + diff --git a/libs/lib_crop/src/main/res/values-v21/colors.xml b/libs/lib_crop/src/main/res/values-v21/colors.xml new file mode 100644 index 0000000..55a5350 --- /dev/null +++ b/libs/lib_crop/src/main/res/values-v21/colors.xml @@ -0,0 +1,5 @@ + + + #aaaaaa + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..0eb32d2 --- /dev/null +++ b/libs/lib_crop/src/main/res/values-zh-rTW/strings.xml @@ -0,0 +1,8 @@ + + 正在儲存相片… + 請稍候… + 沒有可用的圖片來源 + + 完成 + 取消 + diff --git a/libs/lib_crop/src/main/res/values/attrs.xml b/libs/lib_crop/src/main/res/values/attrs.xml new file mode 100644 index 0000000..86fed35 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/attrs.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/colors.xml b/libs/lib_crop/src/main/res/values/colors.xml new file mode 100644 index 0000000..455f448 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/colors.xml @@ -0,0 +1,8 @@ + + + #f3f3f3 + #666666 + #1a000000 + #77000000 + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/dimens.xml b/libs/lib_crop/src/main/res/values/dimens.xml new file mode 100644 index 0000000..9051a35 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/dimens.xml @@ -0,0 +1,5 @@ + + + 56dp + + \ No newline at end of file diff --git a/libs/lib_crop/src/main/res/values/strings.xml b/libs/lib_crop/src/main/res/values/strings.xml new file mode 100644 index 0000000..7543813 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/strings.xml @@ -0,0 +1,10 @@ + + + Saving photo… + Please wait… + No image source available + + Done + Cancel + + diff --git a/libs/lib_crop/src/main/res/values/styles.xml b/libs/lib_crop/src/main/res/values/styles.xml new file mode 100644 index 0000000..2ec6171 --- /dev/null +++ b/libs/lib_crop/src/main/res/values/styles.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/libs/lib_encipher/.gitignore b/libs/lib_encipher/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lib_encipher/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/lib_encipher/build.gradle b/libs/lib_encipher/build.gradle new file mode 100644 index 0000000..59196ae --- /dev/null +++ b/libs/lib_encipher/build.gradle @@ -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" +} \ No newline at end of file diff --git a/libs/lib_encipher/consumer-rules.pro b/libs/lib_encipher/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_encipher/libs/arm64-v8a/libencipher.so b/libs/lib_encipher/libs/arm64-v8a/libencipher.so new file mode 100755 index 0000000..017da66 Binary files /dev/null and b/libs/lib_encipher/libs/arm64-v8a/libencipher.so differ diff --git a/libs/lib_encipher/libs/armeabi-v7a/libencipher.so b/libs/lib_encipher/libs/armeabi-v7a/libencipher.so new file mode 100755 index 0000000..b99d083 Binary files /dev/null and b/libs/lib_encipher/libs/armeabi-v7a/libencipher.so differ diff --git a/libs/lib_encipher/libs/x86/libencipher.so b/libs/lib_encipher/libs/x86/libencipher.so new file mode 100755 index 0000000..7806d5d Binary files /dev/null and b/libs/lib_encipher/libs/x86/libencipher.so differ diff --git a/libs/lib_encipher/libs/x86_64/libencipher.so b/libs/lib_encipher/libs/x86_64/libencipher.so new file mode 100755 index 0000000..44b3e10 Binary files /dev/null and b/libs/lib_encipher/libs/x86_64/libencipher.so differ diff --git a/libs/lib_encipher/proguard-rules.pro b/libs/lib_encipher/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/libs/lib_encipher/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/libs/lib_encipher/src/main/AndroidManifest.xml b/libs/lib_encipher/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/libs/lib_encipher/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt b/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt new file mode 100644 index 0000000..e7a715f --- /dev/null +++ b/libs/lib_encipher/src/main/java/com/secure/encipher/EncipherLib.kt @@ -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? +} \ No newline at end of file diff --git a/libs/lib_standard.gradle b/libs/lib_standard.gradle new file mode 100644 index 0000000..ada148d --- /dev/null +++ b/libs/lib_standard.gradle @@ -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']) +} + diff --git a/libs/lib_utils/.gitignore b/libs/lib_utils/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/libs/lib_utils/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libs/lib_utils/build.gradle b/libs/lib_utils/build.gradle new file mode 100644 index 0000000..c62ad86 --- /dev/null +++ b/libs/lib_utils/build.gradle @@ -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' +} \ No newline at end of file diff --git a/libs/lib_utils/consumer-rules.pro b/libs/lib_utils/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/libs/lib_utils/proguard-rules.pro b/libs/lib_utils/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/libs/lib_utils/proguard-rules.pro @@ -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 \ No newline at end of file diff --git a/libs/lib_utils/src/main/AndroidManifest.xml b/libs/lib_utils/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/libs/lib_utils/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java b/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java new file mode 100644 index 0000000..c836fbf --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/AppUtils.java @@ -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; + +/** + *
+ *     author:
+ *                                      ___           ___           ___         ___
+ *         _____                       /  /\         /__/\         /__/|       /  /\
+ *        /  /::\                     /  /::\        \  \:\       |  |:|      /  /:/
+ *       /  /:/\:\    ___     ___    /  /:/\:\        \  \:\      |  |:|     /__/::\
+ *      /  /:/~/::\  /__/\   /  /\  /  /:/~/::\   _____\__\:\   __|  |:|     \__\/\:\
+ *     /__/:/ /:/\:| \  \:\ /  /:/ /__/:/ /:/\:\ /__/::::::::\ /__/\_|:|____    \  \:\
+ *     \  \:\/:/~/:/  \  \:\  /:/  \  \:\/:/__\/ \  \:\~~\~~\/ \  \:\/:::::/     \__\:\
+ *      \  \::/ /:/    \  \:\/:/    \  \::/       \  \:\  ~~~   \  \::/~~~~      /  /:/
+ *       \  \:\/:/      \  \::/      \  \:\        \  \:\        \  \:\         /__/:/
+ *        \  \::/        \__\/        \  \:\        \  \:\        \  \:\        \__\/
+ *         \__\/                       \__\/         \__\/         \__\/
+ *     blog  : http://blankj.com
+ *     time  : 16/12/08
+ *     desc  : utils about initialization
+ * 
+ */ +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. + *

Init it in the class of Application.

+ * + * @param context context + */ + public static void init(final Context context) { + if (context == null) { + init(getApplicationByReflect()); + return; + } + init((Application) context.getApplicationContext()); + } + + /** + * Init utils. + *

Init it in the class of Application.

+ * + * @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 Task doAsync(final Task 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 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 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 mCallback; + + public Task(final Callback 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 { + 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); + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java b/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java new file mode 100644 index 0000000..e732063 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/FileUtils2.java @@ -0,0 +1,1294 @@ +package com.example.lib_utils; + + +import android.content.Intent; +import android.net.Uri; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileFilter; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.security.DigestInputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import javax.net.ssl.HttpsURLConnection; + +/** + *
+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     user_ic_time  : 2016/05/03
+ *     desc  : utils home_ic_about_us file
+ * 
+ * https://github.com/Blankj/AndroidUtilCode/blob/master/lib/utilcode/src/main/java/com/blankj/utilcode/util/FileUtils.java + */ +public final class FileUtils2 { + + private static final String LINE_SEP = System.getProperty("line.separator"); + + private FileUtils2() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * Return the file by path. + * + * @param filePath The path of file. + * @return the file + */ + public static File getFileByPath(final String filePath) { + return isSpace(filePath) ? null : new File(filePath); + } + + /** + * Return whether the file exists. + * + * @param filePath The path of file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isFileExists(final String filePath) { + return isFileExists(getFileByPath(filePath)); + } + + /** + * Return whether the file exists. + * + * @param file The file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isFileExists(final File file) { + return file != null && file.exists(); + } + + /** + * Rename the file. + * + * @param filePath The path of file. + * @param newName The new name of file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean rename(final String filePath, final String newName) { + return rename(getFileByPath(filePath), newName); + } + + /** + * Rename the file. + * + * @param file The file. + * @param newName The new name of file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean rename(final File file, final String newName) { + // file is null then return false + if (file == null) return false; + // file doesn't exist then return false + if (!file.exists()) return false; + // the new name is space then return false + if (isSpace(newName)) return false; + // the new name equals old name then return true + if (newName.equals(file.getName())) return true; + File newFile = new File(file.getParent() + File.separator + newName); + // the new name of file exists then return false + return !newFile.exists() + && file.renameTo(newFile); + } + + /** + * Return whether it is a directory. + * + * @param dirPath The path of directory. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isDir(final String dirPath) { + return isDir(getFileByPath(dirPath)); + } + + /** + * Return whether it is a directory. + * + * @param file The file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isDir(final File file) { + return file != null && file.exists() && file.isDirectory(); + } + + /** + * Return whether it is a file. + * + * @param filePath The path of file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isFile(final String filePath) { + return isFile(getFileByPath(filePath)); + } + + /** + * Return whether it is a file. + * + * @param file The file. + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isFile(final File file) { + return file != null && file.exists() && file.isFile(); + } + + /** + * Create a directory if it doesn't exist, otherwise do nothing. + * + * @param dirPath The path of directory. + * @return {@code true}: exists or creates successfully
{@code false}: otherwise + */ + public static boolean createOrExistsDir(final String dirPath) { + return createOrExistsDir(getFileByPath(dirPath)); + } + + /** + * Create a directory if it doesn't exist, otherwise do nothing. + * + * @param file The file. + * @return {@code true}: exists or creates successfully
{@code false}: otherwise + */ + public static boolean createOrExistsDir(final File file) { + return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); + } + + /** + * Create a file if it doesn't exist, otherwise do nothing. + * + * @param filePath The path of file. + * @return {@code true}: exists or creates successfully
{@code false}: otherwise + */ + public static boolean createOrExistsFile(final String filePath) { + return createOrExistsFile(getFileByPath(filePath)); + } + + /** + * Create a file if it doesn't exist, otherwise do nothing. + * + * @param file The file. + * @return {@code true}: exists or creates successfully
{@code false}: otherwise + */ + public static boolean createOrExistsFile(final File file) { + if (file == null) return false; + if (file.exists()) return file.isFile(); + if (!createOrExistsDir(file.getParentFile())) return false; + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Create a file if it doesn't exist, otherwise delete old file before creating. + * + * @param filePath The path of file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean createFileByDeleteOldFile(final String filePath) { + return createFileByDeleteOldFile(getFileByPath(filePath)); + } + + /** + * Create a file if it doesn't exist, otherwise delete old file before creating. + * + * @param file The file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean createFileByDeleteOldFile(final File file) { + if (file == null) return false; + // file exists and unsuccessfully delete then return false + if (file.exists() && !file.delete()) return false; + if (!createOrExistsDir(file.getParentFile())) return false; + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Copy the directory. + * + * @param srcDirPath The path of source directory. + * @param destDirPath The path of destination directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyDir(final String srcDirPath, + final String destDirPath) { + return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath)); + } + + /** + * Copy the directory. + * + * @param srcDirPath The path of source directory. + * @param destDirPath The path of destination directory. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyDir(final String srcDirPath, + final String destDirPath, + final OnReplaceListener listener) { + return copyDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener); + } + + /** + * Copy the directory. + * + * @param srcDir The source directory. + * @param destDir The destination directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyDir(final File srcDir, + final File destDir) { + return copyOrMoveDir(srcDir, destDir, false); + } + + /** + * Copy the directory. + * + * @param srcDir The source directory. + * @param destDir The destination directory. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyDir(final File srcDir, + final File destDir, + final OnReplaceListener listener) { + return copyOrMoveDir(srcDir, destDir, listener, false); + } + + /** + * Copy the file. + * + * @param srcFilePath The path of source file. + * @param destFilePath The path of destination file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyFile(final String srcFilePath, + final String destFilePath) { + return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath)); + } + + /** + * Copy the file. + * + * @param srcFilePath The path of source file. + * @param destFilePath The path of destination file. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyFile(final String srcFilePath, + final String destFilePath, + final OnReplaceListener listener) { + return copyFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener); + } + + /** + * Copy the file. + * + * @param srcFile The source file. + * @param destFile The destination file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyFile(final File srcFile, + final File destFile) { + return copyOrMoveFile(srcFile, destFile, false); + } + + /** + * Copy the file. + * + * @param srcFile The source file. + * @param destFile The destination file. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean copyFile(final File srcFile, + final File destFile, + final OnReplaceListener listener) { + return copyOrMoveFile(srcFile, destFile, listener, false); + } + + /** + * Move the directory. + * + * @param srcDirPath The path of source directory. + * @param destDirPath The path of destination directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveDir(final String srcDirPath, + final String destDirPath) { + return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath)); + } + + /** + * Move the directory. + * + * @param srcDirPath The path of source directory. + * @param destDirPath The path of destination directory. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveDir(final String srcDirPath, + final String destDirPath, + final OnReplaceListener listener) { + return moveDir(getFileByPath(srcDirPath), getFileByPath(destDirPath), listener); + } + + /** + * Move the directory. + * + * @param srcDir The source directory. + * @param destDir The destination directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveDir(final File srcDir, + final File destDir) { + return copyOrMoveDir(srcDir, destDir, true); + } + + /** + * Move the directory. + * + * @param srcDir The source directory. + * @param destDir The destination directory. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveDir(final File srcDir, + final File destDir, + final OnReplaceListener listener) { + return copyOrMoveDir(srcDir, destDir, listener, true); + } + + /** + * Move the file. + * + * @param srcFilePath The path of source file. + * @param destFilePath The path of destination file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveFile(final String srcFilePath, + final String destFilePath) { + return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath)); + } + + /** + * Move the file. + * + * @param srcFilePath The path of source file. + * @param destFilePath The path of destination file. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveFile(final String srcFilePath, + final String destFilePath, + final OnReplaceListener listener) { + return moveFile(getFileByPath(srcFilePath), getFileByPath(destFilePath), listener); + } + + /** + * Move the file. + * + * @param srcFile The source file. + * @param destFile The destination file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveFile(final File srcFile, + final File destFile) { + return copyOrMoveFile(srcFile, destFile, true); + } + + /** + * Move the file. + * + * @param srcFile The source file. + * @param destFile The destination file. + * @param listener The replace listener. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean moveFile(final File srcFile, + final File destFile, + final OnReplaceListener listener) { + return copyOrMoveFile(srcFile, destFile, listener, true); + } + + private static boolean copyOrMoveDir(final File srcDir, + final File destDir, + final boolean isMove) { + return copyOrMoveDir(srcDir, destDir, new OnReplaceListener() { + @Override + public boolean onReplace() { + return true; + } + }, isMove); + } + + private static boolean copyOrMoveDir(final File srcDir, + final File destDir, + final OnReplaceListener listener, + final boolean isMove) { + if (srcDir == null || destDir == null) return false; + // destDir's path locate in srcDir's path then return false + String srcPath = srcDir.getPath() + File.separator; + String destPath = destDir.getPath() + File.separator; + if (destPath.contains(srcPath)) return false; + if (!srcDir.exists() || !srcDir.isDirectory()) return false; + if (destDir.exists()) { + if (listener == null || listener.onReplace()) {// require delete the old directory + if (!deleteAllInDir(destDir)) {// unsuccessfully delete then return false + return false; + } + } else { + return true; + } + } + if (!createOrExistsDir(destDir)) return false; + File[] files = srcDir.listFiles(); + for (File file : files) { + File oneDestFile = new File(destPath + file.getName()); + if (file.isFile()) { + if (!copyOrMoveFile(file, oneDestFile, listener, isMove)) return false; + } else if (file.isDirectory()) { + if (!copyOrMoveDir(file, oneDestFile, listener, isMove)) return false; + } + } + return !isMove || deleteDir(srcDir); + } + + private static boolean copyOrMoveFile(final File srcFile, + final File destFile, + final boolean isMove) { + return copyOrMoveFile(srcFile, destFile, new OnReplaceListener() { + @Override + public boolean onReplace() { + return true; + } + }, isMove); + } + + private static boolean copyOrMoveFile(final File srcFile, + final File destFile, + final OnReplaceListener listener, + final boolean isMove) { + if (srcFile == null || destFile == null) return false; + // srcFile equals destFile then return false + if (srcFile.equals(destFile)) return false; + // srcFile doesn't exist or isn't a file then return false + if (!srcFile.exists() || !srcFile.isFile()) return false; + if (destFile.exists()) { + if (listener == null || listener.onReplace()) {// require delete the old file + if (!destFile.delete()) {// unsuccessfully delete then return false + return false; + } + } else { + return true; + } + } + if (!createOrExistsDir(destFile.getParentFile())) return false; + try { + return writeFileFromIS(destFile, new FileInputStream(srcFile)) + && !(isMove && !deleteFile(srcFile)); + } catch (FileNotFoundException e) { + e.printStackTrace(); + return false; + } + } + + /** + * Delete the directory. + * + * @param filePath The path of file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean delete(final String filePath) { + return delete(getFileByPath(filePath)); + } + + /** + * Delete the directory. + * + * @param file The file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean delete(final File file) { + if (file == null) return false; + if (file.isDirectory()) { + return deleteDir(file); + } + return deleteFile(file); + } + + /** + * Delete the directory. + * + * @param dirPath The path of directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteDir(final String dirPath) { + return deleteDir(getFileByPath(dirPath)); + } + + /** + * Delete the directory. + * + * @param dir The directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteDir(final File dir) { + if (dir == null) return false; + // dir doesn't exist then return true + if (!dir.exists()) return true; + // dir isn't a directory then return false + if (!dir.isDirectory()) return false; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.isFile()) { + if (!file.delete()) return false; + } else if (file.isDirectory()) { + if (!deleteDir(file)) return false; + } + } + } + return dir.delete(); + } + + /** + * Delete the file. + * + * @param srcFilePath The path of source file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFile(final String srcFilePath) { + return deleteFile(getFileByPath(srcFilePath)); + } + + /** + * Delete the file. + * + * @param file The file. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFile(final File file) { + return file != null && (!file.exists() || file.isFile() && file.delete()); + } + + /** + * Delete the all in directory. + * + * @param dirPath The path of directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteAllInDir(final String dirPath) { + return deleteAllInDir(getFileByPath(dirPath)); + } + + /** + * Delete the all in directory. + * + * @param dir The directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteAllInDir(final File dir) { + return deleteFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }); + } + + /** + * Delete all files in directory. + * + * @param dirPath The path of directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFilesInDir(final String dirPath) { + return deleteFilesInDir(getFileByPath(dirPath)); + } + + /** + * Delete all files in directory. + * + * @param dir The directory. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFilesInDir(final File dir) { + return deleteFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return pathname.isFile(); + } + }); + } + + /** + * Delete all files that satisfy the filter in directory. + * + * @param dirPath The path of directory. + * @param filter The filter. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFilesInDirWithFilter(final String dirPath, + final FileFilter filter) { + return deleteFilesInDirWithFilter(getFileByPath(dirPath), filter); + } + + /** + * Delete all files that satisfy the filter in directory. + * + * @param dir The directory. + * @param filter The filter. + * @return {@code true}: success
{@code false}: fail + */ + public static boolean deleteFilesInDirWithFilter(final File dir, final FileFilter filter) { + if (dir == null) return false; + // dir doesn't exist then return true + if (!dir.exists()) return true; + // dir isn't a directory then return false + if (!dir.isDirectory()) return false; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (filter.accept(file)) { + if (file.isFile()) { + if (!file.delete()) return false; + } else if (file.isDirectory()) { + if (!deleteDir(file)) return false; + } + } + } + } + return true; + } + + /** + * Return the files in directory. + *

Doesn't traverse subdirectories

+ * + * @param dirPath The path of directory. + * @return the files in directory + */ + public static List listFilesInDir(final String dirPath) { + return listFilesInDir(dirPath, false); + } + + /** + * Return the files in directory. + *

Doesn't traverse subdirectories

+ * + * @param dir The directory. + * @return the files in directory + */ + public static List listFilesInDir(final File dir) { + return listFilesInDir(dir, false); + } + + /** + * Return the files in directory. + * + * @param dirPath The path of directory. + * @param isRecursive True to traverse subdirectories, false otherwise. + * @return the files in directory + */ + public static List listFilesInDir(final String dirPath, final boolean isRecursive) { + return listFilesInDir(getFileByPath(dirPath), isRecursive); + } + + /** + * Return the files in directory. + * + * @param dir The directory. + * @param isRecursive True to traverse subdirectories, false otherwise. + * @return the files in directory + */ + public static List listFilesInDir(final File dir, final boolean isRecursive) { + return listFilesInDirWithFilter(dir, new FileFilter() { + @Override + public boolean accept(File pathname) { + return true; + } + }, isRecursive); + } + + /** + * Return the files that satisfy the filter in directory. + *

Doesn't traverse subdirectories

+ * + * @param dirPath The path of directory. + * @param filter The filter. + * @return the files that satisfy the filter in directory + */ + public static List listFilesInDirWithFilter(final String dirPath, + final FileFilter filter) { + return listFilesInDirWithFilter(getFileByPath(dirPath), filter, false); + } + + /** + * Return the files that satisfy the filter in directory. + *

Doesn't traverse subdirectories

+ * + * @param dir The directory. + * @param filter The filter. + * @return the files that satisfy the filter in directory + */ + public static List listFilesInDirWithFilter(final File dir, + final FileFilter filter) { + return listFilesInDirWithFilter(dir, filter, false); + } + + /** + * Return the files that satisfy the filter in directory. + * + * @param dirPath The path of directory. + * @param filter The filter. + * @param isRecursive True to traverse subdirectories, false otherwise. + * @return the files that satisfy the filter in directory + */ + public static List listFilesInDirWithFilter(final String dirPath, + final FileFilter filter, + final boolean isRecursive) { + return listFilesInDirWithFilter(getFileByPath(dirPath), filter, isRecursive); + } + + /** + * Return the files that satisfy the filter in directory. + * + * @param dir The directory. + * @param filter The filter. + * @param isRecursive True to traverse subdirectories, false otherwise. + * @return the files that satisfy the filter in directory + */ + public static List listFilesInDirWithFilter(final File dir, + final FileFilter filter, + final boolean isRecursive) { + if (!isDir(dir)) return null; + List list = new ArrayList<>(); + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (filter.accept(file)) { + list.add(file); + } + if (isRecursive && file.isDirectory()) { + //noinspection ConstantConditions + list.addAll(listFilesInDirWithFilter(file, filter, true)); + } + } + } + return list; + } + + /** + * Return the user_ic_time that the file was last modified. + * + * @param filePath The path of file. + * @return the user_ic_time that the file was last modified + */ + + public static long getFileLastModified(final String filePath) { + return getFileLastModified(getFileByPath(filePath)); + } + + /** + * Return the user_ic_time that the file was last modified. + * + * @param file The file. + * @return the user_ic_time that the file was last modified + */ + public static long getFileLastModified(final File file) { + if (file == null) return -1; + return file.lastModified(); + } + + /** + * Return the charset of file simply. + * + * @param filePath The path of file. + * @return the charset of file simply + */ + public static String getFileCharsetSimple(final String filePath) { + return getFileCharsetSimple(getFileByPath(filePath)); + } + + /** + * Return the charset of file simply. + * + * @param file The file. + * @return the charset of file simply + */ + public static String getFileCharsetSimple(final File file) { + int p = 0; + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + p = (is.read() << 8) + is.read(); + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + switch (p) { + case 0xefbb: + return "UTF-8"; + case 0xfffe: + return "Unicode"; + case 0xfeff: + return "UTF-16BE"; + default: + return "GBK"; + } + } + + /** + * Return the number of lines of file. + * + * @param filePath The path of file. + * @return the number of lines of file + */ + public static int getFileLines(final String filePath) { + return getFileLines(getFileByPath(filePath)); + } + + /** + * Return the number of lines of file. + * + * @param file The file. + * @return the number of lines of file + */ + public static int getFileLines(final File file) { + int count = 1; + InputStream is = null; + try { + is = new BufferedInputStream(new FileInputStream(file)); + byte[] buffer = new byte[1024]; + int readChars; + if (LINE_SEP.endsWith("\n")) { + while ((readChars = is.read(buffer, 0, 1024)) != -1) { + for (int i = 0; i < readChars; ++i) { + if (buffer[i] == '\n') ++count; + } + } + } else { + while ((readChars = is.read(buffer, 0, 1024)) != -1) { + for (int i = 0; i < readChars; ++i) { + if (buffer[i] == '\r') ++count; + } + } + } + } catch (IOException e) { + e.printStackTrace(); + } finally { + try { + if (is != null) { + is.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return count; + } + + /** + * Return the size of directory. + * + * @param dirPath The path of directory. + * @return the size of directory + */ + public static String getDirSize(final String dirPath) { + return getDirSize(getFileByPath(dirPath)); + } + + /** + * Return the size of directory. + * + * @param dir The directory. + * @return the size of directory + */ + public static String getDirSize(final File dir) { + long len = getDirLength(dir); + return len == -1 ? "" : byte2FitMemorySize(len); + } + + /** + * Return the length of file. + * + * @param filePath The path of file. + * @return the length of file + */ + public static String getFileSize(final String filePath) { + long len = getFileLength(filePath); + return len == -1 ? "" : byte2FitMemorySize(len); + } + + /** + * Return the length of file. + * + * @param file The file. + * @return the length of file + */ + public static String getFileSize(final File file) { + long len = getFileLength(file); + return len == -1 ? "" : byte2FitMemorySize(len); + } + + /** + * Return the length of directory. + * + * @param dirPath The path of directory. + * @return the length of directory + */ + public static long getDirLength(final String dirPath) { + return getDirLength(getFileByPath(dirPath)); + } + + /** + * Return the length of directory. + * + * @param dir The directory. + * @return the length of directory + */ + public static long getDirLength(final File dir) { + if (!isDir(dir)) return -1; + long len = 0; + File[] files = dir.listFiles(); + if (files != null && files.length != 0) { + for (File file : files) { + if (file.isDirectory()) { + len += getDirLength(file); + } else { + len += file.length(); + } + } + } + return len; + } + + /** + * Return the length of file. + * + * @param filePath The path of file. + * @return the length of file + */ + public static long getFileLength(final String filePath) { + boolean isURL = filePath.matches("[a-zA-z]+://[^\\s]*"); + if (isURL) { + try { + HttpsURLConnection conn = (HttpsURLConnection) new URL(filePath).openConnection(); + conn.setRequestProperty("Accept-Encoding", "identity"); + conn.connect(); + if (conn.getResponseCode() == 200) { + return conn.getContentLength(); + } + return -1; + } catch (IOException e) { + e.printStackTrace(); + } + } + return getFileLength(getFileByPath(filePath)); + } + + /** + * Return the length of file. + * + * @param file The file. + * @return the length of file + */ + public static long getFileLength(final File file) { + if (!isFile(file)) return -1; + return file.length(); + } + + /** + * Return the MD5 of file. + * + * @param filePath The path of file. + * @return the md5 of file + */ + public static String getFileMD5ToString(final String filePath) { + File file = isSpace(filePath) ? null : new File(filePath); + return getFileMD5ToString(file); + } + + /** + * Return the MD5 of file. + * + * @param file The file. + * @return the md5 of file + */ + public static String getFileMD5ToString(final File file) { + return bytes2HexString(getFileMD5(file)); + } + + /** + * Return the MD5 of file. + * + * @param filePath The path of file. + * @return the md5 of file + */ + public static byte[] getFileMD5(final String filePath) { + return getFileMD5(getFileByPath(filePath)); + } + + /** + * Return the MD5 of file. + * + * @param file The file. + * @return the md5 of file + */ + public static byte[] getFileMD5(final File file) { + if (file == null) return null; + DigestInputStream dis = null; + try { + FileInputStream fis = new FileInputStream(file); + MessageDigest md = MessageDigest.getInstance("MD5"); + dis = new DigestInputStream(fis, md); + byte[] buffer = new byte[1024 * 256]; + while (true) { + if (!(dis.read(buffer) > 0)) break; + } + md = dis.getMessageDigest(); + return md.digest(); + } catch (NoSuchAlgorithmException | IOException e) { + e.printStackTrace(); + } finally { + try { + if (dis != null) { + dis.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + return null; + } + + /** + * Return the file's path of directory. + * + * @param file The file. + * @return the file's path of directory + */ + public static String getDirName(final File file) { + if (file == null) return ""; + return getDirName(file.getAbsolutePath()); + } + + /** + * Return the file's path of directory. + * + * @param filePath The path of file. + * @return the file's path of directory + */ + public static String getDirName(final String filePath) { + if (isSpace(filePath)) return ""; + int lastSep = filePath.lastIndexOf(File.separator); + return lastSep == -1 ? "" : filePath.substring(0, lastSep + 1); + } + + /** + * Return the name of file. + * + * @param file The file. + * @return the name of file + */ + public static String getFileName(final File file) { + if (file == null) return ""; + return getFileName(file.getAbsolutePath()); + } + + /** + * Return the name of file. + * + * @param filePath The path of file. + * @return the name of file + */ + public static String getFileName(final String filePath) { + if (isSpace(filePath)) return ""; + int lastSep = filePath.lastIndexOf(File.separator); + return lastSep == -1 ? filePath : filePath.substring(lastSep + 1); + } + + /** + * Return the name of file without extension. + * + * @param file The file. + * @return the name of file without extension + */ + public static String getFileNameNoExtension(final File file) { + if (file == null) return ""; + return getFileNameNoExtension(file.getPath()); + } + + /** + * Return the name of file without extension. + * + * @param filePath The path of file. + * @return the name of file without extension + */ + public static String getFileNameNoExtension(final String filePath) { + if (isSpace(filePath)) return ""; + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastSep == -1) { + return (lastPoi == -1 ? filePath : filePath.substring(0, lastPoi)); + } + if (lastPoi == -1 || lastSep > lastPoi) { + return filePath.substring(lastSep + 1); + } + return filePath.substring(lastSep + 1, lastPoi); + } + + /** + * Return the extension of file. + * + * @param file The file. + * @return the extension of file + */ + public static String getFileExtension(final File file) { + if (file == null) return ""; + return getFileExtension(file.getPath()); + } + + /** + * Return the extension of file. + * + * @param filePath The path of file. + * @return the extension of file + */ + public static String getFileExtension(final String filePath) { + if (isSpace(filePath)) return ""; + int lastPoi = filePath.lastIndexOf('.'); + int lastSep = filePath.lastIndexOf(File.separator); + if (lastPoi == -1 || lastSep >= lastPoi) return ""; + return filePath.substring(lastPoi + 1); + } + + /** + * Notify system to scan the file. + * + * @param file The file. + */ + public static void notifySystemToScan(final File file) { + if (file == null || !file.exists()) return; + Intent intent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); + Uri uri = Uri.fromFile(file); + intent.setData(uri); + AppUtils.getApp().sendBroadcast(intent); + } + + /** + * Notify system to scan the file. + * + * @param filePath The path of file. + */ + public static void notifySystemToScan(final String filePath) { + notifySystemToScan(getFileByPath(filePath)); + } + + /////////////////////////////////////////////////////////////////////////// + // interface + /////////////////////////////////////////////////////////////////////////// + + public interface OnReplaceListener { + boolean onReplace(); + } + + /////////////////////////////////////////////////////////////////////////// + // other utils methods + /////////////////////////////////////////////////////////////////////////// + + private static final char[] HEX_DIGITS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + + private static String bytes2HexString(final byte[] bytes) { + if (bytes == null) return ""; + int len = bytes.length; + if (len <= 0) return ""; + char[] ret = new char[len << 1]; + for (int i = 0, j = 0; i < len; i++) { + ret[j++] = HEX_DIGITS[bytes[i] >> 4 & 0x0f]; + ret[j++] = HEX_DIGITS[bytes[i] & 0x0f]; + } + return new String(ret); + } + + public static String byte2FitMemorySize(final long byteNum) { + if (byteNum < 0) { + return "shouldn't be less than zero!"; + } else if (byteNum < 1024) { + return String.format(Locale.getDefault(), "%.3fB", (double) byteNum); + } else if (byteNum < 1048576) { + return String.format(Locale.getDefault(), "%.3fKB", (double) byteNum / 1024); + } else if (byteNum < 1073741824) { + return String.format(Locale.getDefault(), "%.3fMB", (double) byteNum / 1048576); + } else { + return String.format(Locale.getDefault(), "%.3fGB", (double) byteNum / 1073741824); + } + } + + private static boolean isSpace(final String s) { + if (s == null) return true; + for (int i = 0, len = s.length(); i < len; ++i) { + if (!Character.isWhitespace(s.charAt(i))) { + return false; + } + } + return true; + } + + private static boolean writeFileFromIS(final File file, + final InputStream is) { + OutputStream os = null; + try { + os = new BufferedOutputStream(new FileOutputStream(file)); + byte[] data = new byte[8192]; + int len; + while ((len = is.read(data, 0, 8192)) != -1) { + os.write(data, 0, len); + } + return true; + } catch (IOException e) { + e.printStackTrace(); + return false; + } finally { + try { + is.close(); + } catch (IOException e) { + e.printStackTrace(); + } + try { + if (os != null) { + os.close(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt new file mode 100644 index 0000000..4268077 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ICleared.kt @@ -0,0 +1,14 @@ +package com.example.lib_utils + + +/** + * Created by Max on 2023/10/26 11:50 + * Desc:清除释放统一接口 + **/ +interface ICleared { + + /** + * 清除/释放 + */ + fun onCleared() {} +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt new file mode 100644 index 0000000..3f8a3ea --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/PathUtils.kt @@ -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之下的文件或子文件夹

+ * + * @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 + } +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt new file mode 100644 index 0000000..3dbf085 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ServiceTime.kt @@ -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() + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt new file mode 100644 index 0000000..2a6258b --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ShapeViewOutlineProvider.kt @@ -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) + } + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt new file mode 100644 index 0000000..ca71030 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/SolveEditTextScrollClash.kt @@ -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 + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt new file mode 100644 index 0000000..0562645 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/StringUtils2.kt @@ -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() + } + } +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt new file mode 100644 index 0000000..210b4ba --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/TelephonyUtils.kt @@ -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" + } + } + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt new file mode 100644 index 0000000..e8fb503 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/UiUtils.kt @@ -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 + } +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt new file mode 100644 index 0000000..ddb3255 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ContextKtx.kt @@ -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 + } + } +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt new file mode 100644 index 0000000..9e65bae --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/EditTextKtx.kt @@ -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 +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt new file mode 100644 index 0000000..8cfb5d5 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ImageToAlbumKtx.kt @@ -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 +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt new file mode 100644 index 0000000..2eace68 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ResourcesKtx.kt @@ -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 +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt new file mode 100644 index 0000000..5636663 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/UiKtx.kt @@ -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()) +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt new file mode 100644 index 0000000..a60a15b --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/ktx/ViewKtx.kt @@ -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.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.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.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.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() + } +} + diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt new file mode 100644 index 0000000..a2b5400 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/AndroidLogPrinter.kt @@ -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) + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt new file mode 100644 index 0000000..d9de4c0 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/ILog.kt @@ -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) + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt new file mode 100644 index 0000000..73664cb --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogPrinter.kt @@ -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) +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt new file mode 100644 index 0000000..931017d --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/log/LogUtil.kt @@ -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) + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java new file mode 100644 index 0000000..a10f22b --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/IconTextSpan.java @@ -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); + } +} diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt new file mode 100644 index 0000000..d1810e8 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/RoundBackgroundColorSpan.kt @@ -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) + } +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt new file mode 100644 index 0000000..544d7fa --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/SpannableTextBuilder.kt @@ -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) : 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) : 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) +} \ No newline at end of file diff --git a/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt new file mode 100644 index 0000000..515d651 --- /dev/null +++ b/libs/lib_utils/src/main/java/com/example/lib_utils/spannable/VerticalImageSpan.kt @@ -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() + } +} \ No newline at end of file