diff --git a/app/build.gradle b/app/build.gradle index 982ca67..6a98846 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -62,8 +62,11 @@ dependencies { implementation 'com.sackcentury:shinebutton:1.0.0' //工具类 implementation 'com.blankj:utilcodex:1.28.4' + //图库选择 + //implementation 'com.github.LuckSiege.PictureSelector:picture_library:v2.5.6' //测试用例 testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test.ext:junit:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + implementation project(path: ':picture_library') } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 534079e..b8529c4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -8,6 +8,7 @@ + diff --git a/app/src/main/java/com/yuxihan/sdu/comm/Const.java b/app/src/main/java/com/yuxihan/sdu/comm/Const.java index 269eb9e..7160563 100644 --- a/app/src/main/java/com/yuxihan/sdu/comm/Const.java +++ b/app/src/main/java/com/yuxihan/sdu/comm/Const.java @@ -6,4 +6,9 @@ public class Const { public static final String LOGIN_USER_NAME = "LoginUserName"; public static final String LOGIN_NICKNAME = "LoginNickname"; public static final String LOGIN_HEAD_URL = "LoginHeadUrl"; + + public static final int CAMERA_OK = 1; + public static final int PIC_OK = 2; + + } diff --git a/app/src/main/java/com/yuxihan/sdu/comm/util/GlideEngine.java b/app/src/main/java/com/yuxihan/sdu/comm/util/GlideEngine.java new file mode 100644 index 0000000..7f4e7dd --- /dev/null +++ b/app/src/main/java/com/yuxihan/sdu/comm/util/GlideEngine.java @@ -0,0 +1,236 @@ +package com.yuxihan.sdu.comm.util; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.drawable.Drawable; +import android.view.View; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.graphics.drawable.RoundedBitmapDrawable; +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.request.RequestOptions; +import com.bumptech.glide.request.target.BitmapImageViewTarget; +import com.bumptech.glide.request.target.ImageViewTarget; +import com.luck.picture.lib.engine.ImageEngine; +import com.luck.picture.lib.listener.OnImageCompleteCallback; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.widget.longimage.ImageSource; +import com.luck.picture.lib.widget.longimage.ImageViewState; +import com.luck.picture.lib.widget.longimage.SubsamplingScaleImageView; +import com.yuxihan.sdu.R; + +/** + * @describe:Glide加载引擎 + */ +public class GlideEngine implements ImageEngine { + + /** + * 加载图片 + * + * @param context + * @param url + * @param imageView + */ + @Override + public void loadImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView) { + Glide.with(context) + .load(url) + .into(imageView); + } + + /** + * 加载网络图片适配长图方案 + * # 注意:此方法只有加载网络图片才会回调 + * + * @param context + * @param url + * @param imageView + * @param longImageView + * @param callback 网络图片加载回调监听 {link after version 2.5.1 Please use the + * #OnImageCompleteCallback#} + */ + @Override + public void loadImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView, + SubsamplingScaleImageView longImageView, + OnImageCompleteCallback callback) { + Glide.with(context) + .asBitmap() + .load(url) + .into(new ImageViewTarget(imageView) { + @Override + public void onLoadStarted(@Nullable Drawable placeholder) { + super.onLoadStarted(placeholder); + if (callback != null) { + callback.onShowLoading(); + } + } + + @Override + public void onLoadFailed(@Nullable Drawable errorDrawable) { + super.onLoadFailed(errorDrawable); + if (callback != null) { + callback.onHideLoading(); + } + } + + @Override + protected void setResource(@Nullable Bitmap resource) { + if (callback != null) { + callback.onHideLoading(); + } + if (resource != null) { + boolean eqLongImage = MediaUtils.isLongImg(resource.getWidth(), + resource.getHeight()); + longImageView.setVisibility(eqLongImage ? View.VISIBLE : View.GONE); + imageView.setVisibility(eqLongImage ? View.GONE : View.VISIBLE); + if (eqLongImage) { + // 加载长图 + longImageView.setQuickScaleEnabled(true); + longImageView.setZoomEnabled(true); + longImageView.setPanEnabled(true); + longImageView.setDoubleTapZoomDuration(100); + longImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); + longImageView.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER); + longImageView.setImage(ImageSource.bitmap(resource), + new ImageViewState(0, new PointF(0, 0), 0)); + } else { + // 普通图片 + imageView.setImageBitmap(resource); + } + } + } + }); + } + + /** + * 加载网络图片适配长图方案 + * # 注意:此方法只有加载网络图片才会回调 + * + * @param context + * @param url + * @param imageView + * @param longImageView + * @ 已废弃 + */ + @Override + public void loadImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView, + SubsamplingScaleImageView longImageView) { + Glide.with(context) + .asBitmap() + .load(url) + .into(new ImageViewTarget(imageView) { + @Override + protected void setResource(@Nullable Bitmap resource) { + if (resource != null) { + boolean eqLongImage = MediaUtils.isLongImg(resource.getWidth(), + resource.getHeight()); + longImageView.setVisibility(eqLongImage ? View.VISIBLE : View.GONE); + imageView.setVisibility(eqLongImage ? View.GONE : View.VISIBLE); + if (eqLongImage) { + // 加载长图 + longImageView.setQuickScaleEnabled(true); + longImageView.setZoomEnabled(true); + longImageView.setPanEnabled(true); + longImageView.setDoubleTapZoomDuration(100); + longImageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); + longImageView.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER); + longImageView.setImage(ImageSource.bitmap(resource), + new ImageViewState(0, new PointF(0, 0), 0)); + } else { + // 普通图片 + imageView.setImageBitmap(resource); + } + } + } + }); + } + + /** + * 加载相册目录 + * + * @param context 上下文 + * @param url 图片路径 + * @param imageView 承载图片ImageView + */ + @Override + public void loadFolderImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView) { + Glide.with(context) + .asBitmap() + .load(url) + .override(180, 180) + .centerCrop() + .sizeMultiplier(0.5f) + .apply(new RequestOptions().placeholder(R.drawable.picture_image_placeholder)) + .into(new BitmapImageViewTarget(imageView) { + @Override + protected void setResource(Bitmap resource) { + RoundedBitmapDrawable circularBitmapDrawable = + RoundedBitmapDrawableFactory. + create(context.getResources(), resource); + circularBitmapDrawable.setCornerRadius(8); + imageView.setImageDrawable(circularBitmapDrawable); + } + }); + } + + + /** + * 加载gif + * + * @param context 上下文 + * @param url 图片路径 + * @param imageView 承载图片ImageView + */ + @Override + public void loadAsGifImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView) { + Glide.with(context) + .asGif() + .load(url) + .into(imageView); + } + + /** + * 加载图片列表图片 + * + * @param context 上下文 + * @param url 图片路径 + * @param imageView 承载图片ImageView + */ + @Override + public void loadGridImage(@NonNull Context context, @NonNull String url, + @NonNull ImageView imageView) { + Glide.with(context) + .load(url) + .override(200, 200) + .centerCrop() + .apply(new RequestOptions().placeholder(R.drawable.picture_image_placeholder)) + .into(imageView); + } + + + private GlideEngine() { + } + + private static GlideEngine instance; + + public static GlideEngine createGlideEngine() { + if (null == instance) { + synchronized (GlideEngine.class) { + if (null == instance) { + instance = new GlideEngine(); + } + } + } + return instance; + } +} diff --git a/app/src/main/java/com/yuxihan/sdu/ui/info/InfoEditActivity.java b/app/src/main/java/com/yuxihan/sdu/ui/info/InfoEditActivity.java index 48e20db..b02480b 100644 --- a/app/src/main/java/com/yuxihan/sdu/ui/info/InfoEditActivity.java +++ b/app/src/main/java/com/yuxihan/sdu/ui/info/InfoEditActivity.java @@ -1,17 +1,108 @@ package com.yuxihan.sdu.ui.info; -import androidx.appcompat.app.AppCompatActivity; - +import android.content.Context; import android.os.Bundle; +import android.view.View; +import com.bumptech.glide.Glide; +import com.luck.picture.lib.PictureSelector; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; import com.yuxihan.sdu.R; import com.yuxihan.sdu.comm.BaseActivity; +import com.yuxihan.sdu.comm.Const; +import com.yuxihan.sdu.comm.util.GlideEngine; +import com.yuxihan.sdu.comm.widget.ActionSheetDialog; public class InfoEditActivity extends BaseActivity { + private Context mContext = InfoEditActivity.this; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_info_edit); + initView(); + } + + private void initView() { + String[] fileFrom = new String[]{"相机", "图库"}; + //头像控件点击事件 + findViewById(R.id.rl_head_icon).setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + ActionSheetDialog actionSheetDialog = new ActionSheetDialog( + mContext).builder().setCancelable(false) + .setCanceledOnTouchOutside(true); + for (int i = 0; i < fileFrom.length; i++) { + actionSheetDialog.addSheetItem(fileFrom[i], + ActionSheetDialog.SheetItemColor.Blue, + new ActionSheetDialog.OnSheetItemClickListener() { + @Override + public void onClick(int which) { + getFile(which); + } + }); + } + actionSheetDialog.show(); + } + }); + } + + + //处理点击事件 + private void getFile(int index) { + switch (index) { + case 1: + takePic(); + break; + case 2: + gallery(); + break; + } + } + + + //去图库选择 + private void gallery() { + PictureSelector.create(this) + .openGallery(PictureMimeType.ofImage())// 全部.PictureMimeType.ofAll()、图片.ofImage() + // 、视频.ofVideo()、音频.ofAudio() + .theme(R.style.picture_white_style)// 主题样式设置 具体参考 values/styles 用法:R.style + // .picture_white_style + .maxSelectNum(1)// 最大图片选择数量 + .minSelectNum(1)// 最小选择数量 + .isCamera(false)// 是否显示拍照按钮 + .imageSpanCount(5)// 每行显示个数 + .compress(false)// 是否压缩 true or false + .maxSelectNum(1)// 最大图片选择数量 + .isZoomAnim(true)// 图片列表点击 缩放效果 默认true + .synOrAsy(true)//同步true或异步false 压缩 默认同步 + .glideOverride(120, 120)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度 + //.selectionMedia(picList)// 是否传入已选图片 + .minimumCompressSize(100)// 小于100kb的图片不压缩 + .loadImageEngine(GlideEngine.createGlideEngine()) + .forResult(Const.PIC_OK);//结果回调onActivityResult code + } + + + //拍照 + private void takePic() { + //单独拍照 + PictureSelector.create(this) + .openCamera(PictureMimeType.ofImage()) + .theme(R.style.picture_white_style)// 主题样式设置 具体参考 values/styles + .minSelectNum(1)// 最小选择数量 + .compress(false) + .selectionMode(PictureConfig.MULTIPLE)// 多选 or 单选 + .isCamera(false)// 是否显示拍照按钮 + .glideOverride(120, 120)// glide 加载宽高,越小图片列表越流畅,但会影响列表图片浏览的清晰度 + //.selectionMedia(picList)// 是否传入已选图片 + .imageSpanCount(5)// 每行显示个数 + .maxSelectNum(1)// 最大图片选择数量 + .isZoomAnim(true)// 图片列表点击 缩放效果 默认true + .synOrAsy(true)//同步true或异步false 压缩 默认同步 + .minimumCompressSize(100)// 小于100kb的图片不压缩 + .forResult(Const.CAMERA_OK); } } diff --git a/app/src/main/res/color/picture_list_sina_text_color.xml b/app/src/main/res/color/picture_list_sina_text_color.xml new file mode 100644 index 0000000..7c54bec --- /dev/null +++ b/app/src/main/res/color/picture_list_sina_text_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/picture_preview_sina_text_color.xml b/app/src/main/res/color/picture_preview_sina_text_color.xml new file mode 100644 index 0000000..7c54bec --- /dev/null +++ b/app/src/main/res/color/picture_preview_sina_text_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkbox_num_selector.xml b/app/src/main/res/drawable/checkbox_num_selector.xml new file mode 100644 index 0000000..51ca89f --- /dev/null +++ b/app/src/main/res/drawable/checkbox_num_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/checkbox_selector.xml b/app/src/main/res/drawable/checkbox_selector.xml new file mode 100644 index 0000000..4401c58 --- /dev/null +++ b/app/src/main/res/drawable/checkbox_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/custom_qq_cb.xml b/app/src/main/res/drawable/custom_qq_cb.xml new file mode 100644 index 0000000..58fdbdd --- /dev/null +++ b/app/src/main/res/drawable/custom_qq_cb.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_add_image.png b/app/src/main/res/drawable/ic_add_image.png new file mode 100644 index 0000000..38e38da Binary files /dev/null and b/app/src/main/res/drawable/ic_add_image.png differ diff --git a/app/src/main/res/drawable/ic_back_arrow.png b/app/src/main/res/drawable/ic_back_arrow.png new file mode 100644 index 0000000..639cc47 Binary files /dev/null and b/app/src/main/res/drawable/ic_back_arrow.png differ diff --git a/app/src/main/res/drawable/ic_delete_menu.png b/app/src/main/res/drawable/ic_delete_menu.png new file mode 100644 index 0000000..2025f26 Binary files /dev/null and b/app/src/main/res/drawable/ic_delete_menu.png differ diff --git a/app/src/main/res/drawable/ic_let_go_delete.png b/app/src/main/res/drawable/ic_let_go_delete.png new file mode 100644 index 0000000..89bb56a Binary files /dev/null and b/app/src/main/res/drawable/ic_let_go_delete.png differ diff --git a/app/src/main/res/drawable/ic_minus.png b/app/src/main/res/drawable/ic_minus.png new file mode 100644 index 0000000..3f9a031 Binary files /dev/null and b/app/src/main/res/drawable/ic_minus.png differ diff --git a/app/src/main/res/drawable/ic_orange_arrow_down.png b/app/src/main/res/drawable/ic_orange_arrow_down.png new file mode 100644 index 0000000..dfaae6a Binary files /dev/null and b/app/src/main/res/drawable/ic_orange_arrow_down.png differ diff --git a/app/src/main/res/drawable/ic_orange_arrow_up.png b/app/src/main/res/drawable/ic_orange_arrow_up.png new file mode 100644 index 0000000..f7c5811 Binary files /dev/null and b/app/src/main/res/drawable/ic_orange_arrow_up.png differ diff --git a/app/src/main/res/drawable/ic_play.png b/app/src/main/res/drawable/ic_play.png new file mode 100644 index 0000000..c221b36 Binary files /dev/null and b/app/src/main/res/drawable/ic_play.png differ diff --git a/app/src/main/res/drawable/ic_plus.png b/app/src/main/res/drawable/ic_plus.png new file mode 100644 index 0000000..3441911 Binary files /dev/null and b/app/src/main/res/drawable/ic_plus.png differ diff --git a/app/src/main/res/drawable/message_oval_orange.xml b/app/src/main/res/drawable/message_oval_orange.xml new file mode 100644 index 0000000..aff8f29 --- /dev/null +++ b/app/src/main/res/drawable/message_oval_orange.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/message_oval_white.xml b/app/src/main/res/drawable/message_oval_white.xml new file mode 100644 index 0000000..be974bf --- /dev/null +++ b/app/src/main/res/drawable/message_oval_white.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_oval_blue.xml b/app/src/main/res/drawable/num_oval_blue.xml new file mode 100644 index 0000000..3ddee52 --- /dev/null +++ b/app/src/main/res/drawable/num_oval_blue.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_oval_blue_def.xml b/app/src/main/res/drawable/num_oval_blue_def.xml new file mode 100644 index 0000000..be974bf --- /dev/null +++ b/app/src/main/res/drawable/num_oval_blue_def.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/num_oval_orange.xml b/app/src/main/res/drawable/num_oval_orange.xml new file mode 100644 index 0000000..ce0993d --- /dev/null +++ b/app/src/main/res/drawable/num_oval_orange.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/picture_new_item_select_bg.xml b/app/src/main/res/drawable/picture_new_item_select_bg.xml new file mode 100644 index 0000000..5df58cc --- /dev/null +++ b/app/src/main/res/drawable/picture_new_item_select_bg.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_info_edit.xml b/app/src/main/res/layout/activity_info_edit.xml index f247b60..de4e00c 100644 --- a/app/src/main/res/layout/activity_info_edit.xml +++ b/app/src/main/res/layout/activity_info_edit.xml @@ -44,6 +44,7 @@ diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index e02a4aa..4303f4e 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -14,4 +14,21 @@ #5EB9F8 #5CC2F8 #888888 + + + #393a3e + #000000 + #f6f6f6 + #fafafa + #B6B6B6 + #f94c51 + #43c117 + #53575e + #00000000 + #FFFFFF + #E0DBDBDB + #7D7DFF + #9b9b9b + #E0FF6100 + #FF0000 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 277295f..cc137f1 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - @@ -41,6 +41,421 @@ true @style/ActionSheetDialogAnimation + + + + + + + + + + + + + - + + + + + + + - + + diff --git a/build.gradle b/build.gradle index 97d7be1..3ce9058 100644 --- a/build.gradle +++ b/build.gradle @@ -1,4 +1,5 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. +apply from: "config.gradle" buildscript { repositories { diff --git a/config.gradle b/config.gradle new file mode 100644 index 0000000..5de406b --- /dev/null +++ b/config.gradle @@ -0,0 +1,38 @@ +ext { + cfgs = [ + compileSdkVersion : 29, + buildToolsVersion : "28.0.3", + minSdkVersion : 19, + targetSdkVersion : 29, + versionCode : 26, + versionName : "2.5.6", + ucropVersionCode : 24, + ucropVersionName : "2.2.4-non-native", + + //open version control + androidSupportVersion : "27.0.2", + version_recyclerview : "1.1.0", + version_appcompat : "1.1.0", + localbroadcastmanager : "1.0.0", + + camerax_view : "1.0.0-alpha10", + camerax_version : "1.0.0-beta03", + camera_core_version : "1.0.0-beta03", + futures_version : "1.0.0", + + // ucrop + androidx_appcompat_version : "1.1.0", + androidx_exifinterface_version: "1.3.0-alpha01", + androidx_transition_version : "1.3.1", + + // okio + okio_version : "2.6.0", + + // glide + glide_version : "4.11.0", + + // picasso + picasso_version : "2.71828" + + ] +} \ No newline at end of file diff --git a/picture_library/.gitignore b/picture_library/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/picture_library/.gitignore @@ -0,0 +1 @@ +/build diff --git a/picture_library/build.gradle b/picture_library/build.gradle new file mode 100644 index 0000000..4089001 --- /dev/null +++ b/picture_library/build.gradle @@ -0,0 +1,36 @@ +apply plugin: 'com.android.library' + +android { + compileSdkVersion cfgs.compileSdkVersion + + defaultConfig { + minSdkVersion cfgs.minSdkVersion + targetSdkVersion cfgs.targetSdkVersion + versionCode cfgs.versionCode + versionName cfgs.versionName + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(include: ['*.jar'], dir: 'libs') + implementation "androidx.appcompat:appcompat:${cfgs.version_appcompat}" + implementation "androidx.recyclerview:recyclerview:${cfgs.version_recyclerview}" + implementation "androidx.concurrent:concurrent-futures:${cfgs.futures_version}" + implementation "com.squareup.okio:okio:${cfgs.okio_version}" + implementation "androidx.camera:camera-lifecycle:${cfgs.camerax_version}" + implementation "androidx.localbroadcastmanager:localbroadcastmanager:${cfgs.localbroadcastmanager}" + implementation "androidx.camera:camera-view:${cfgs.camerax_view}" + api project(':ucrop') +} diff --git a/picture_library/proguard-rules.pro b/picture_library/proguard-rules.pro new file mode 100644 index 0000000..e0a9b68 --- /dev/null +++ b/picture_library/proguard-rules.pro @@ -0,0 +1,25 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in /Users/luck/Documents/android-sdk-macosx-2/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# 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 diff --git a/picture_library/src/main/AndroidManifest.xml b/picture_library/src/main/AndroidManifest.xml new file mode 100644 index 0000000..16304c6 --- /dev/null +++ b/picture_library/src/main/AndroidManifest.xml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureBaseActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureBaseActivity.java new file mode 100644 index 0000000..849f674 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureBaseActivity.java @@ -0,0 +1,1093 @@ +package com.luck.picture.lib; + +import android.Manifest; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.view.View; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; + +import com.luck.picture.lib.app.PictureAppMaster; +import com.luck.picture.lib.compress.Luban; +import com.luck.picture.lib.compress.OnCompressListener; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.dialog.PictureCustomDialog; +import com.luck.picture.lib.dialog.PictureLoadingDialog; +import com.luck.picture.lib.engine.ImageEngine; +import com.luck.picture.lib.engine.PictureSelectorEngine; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.immersive.ImmersiveManage; +import com.luck.picture.lib.immersive.NavBarUtils; +import com.luck.picture.lib.language.PictureLanguageUtils; +import com.luck.picture.lib.listener.OnResultCallbackListener; +import com.luck.picture.lib.model.LocalMediaPageLoader; +import com.luck.picture.lib.permissions.PermissionChecker; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.AndroidQTransformUtils; +import com.luck.picture.lib.tools.AttrsUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.DoubleUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.StringUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.VoiceUtils; +import com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.model.CutInfo; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + + +/** + * @author:luck + * @data:2018/3/28 下午1:00 + * @describe: BaseActivity + */ +public abstract class PictureBaseActivity extends AppCompatActivity { + protected PictureSelectionConfig config; + protected boolean openWhiteStatusBar, numComplete; + protected int colorPrimary, colorPrimaryDark; + protected PictureLoadingDialog mLoadingDialog; + protected List selectionMedias; + protected Handler mHandler; + protected View container; + /** + * if there more + */ + protected boolean isHasMore = true; + /** + * page + */ + protected int mPage = 1; + /** + * is onSaveInstanceState + */ + protected boolean isOnSaveInstanceState; + + /** + * Whether to use immersion, subclasses copy the method to determine whether to use immersion + * + * @return + */ + @Override + public boolean isImmersive() { + return true; + } + + /** + * Whether to change the screen direction + * + * @return + */ + public boolean isRequestedOrientation() { + return true; + } + + + public void immersive() { + ImmersiveManage.immersiveAboveAPI23(this + , colorPrimaryDark + , colorPrimary + , openWhiteStatusBar); + } + + + /** + * get Layout Resources Id + * + * @return + */ + public abstract int getResourceId(); + + /** + * init Views + */ + protected void initWidgets() { + + } + + /** + * init PictureSelector Style + */ + protected void initPictureSelectorStyle() { + + } + + /** + * Set CompleteText + */ + protected void initCompleteText(int startCount) { + + } + + /** + * Set CompleteText + */ + protected void initCompleteText(List list) { + + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + if (savedInstanceState != null) { + config = savedInstanceState.getParcelable(PictureConfig.EXTRA_CONFIG); + } + if (config == null) { + config = getIntent() != null ? getIntent().getParcelableExtra(PictureConfig.EXTRA_CONFIG) : config; + } + checkConfigNull(); + PictureLanguageUtils.setAppLanguage(getContext(), config.language); + if (!config.camera) { + setTheme(config.themeStyleId == 0 ? R.style.picture_default_style : config.themeStyleId); + } + super.onCreate(savedInstanceState == null ? new Bundle() : savedInstanceState); + newCreateEngine(); + newCreateResultCallbackListener(); + if (isRequestedOrientation()) { + setNewRequestedOrientation(); + } + mHandler = new Handler(Looper.getMainLooper()); + initConfig(); + if (isImmersive()) { + immersive(); + } + if (config.style != null && config.style.pictureNavBarColor != 0) { + NavBarUtils.setNavBarColor(this, config.style.pictureNavBarColor); + } + int layoutResID = getResourceId(); + if (layoutResID != 0) { + setContentView(layoutResID); + } + initWidgets(); + initPictureSelectorStyle(); + isOnSaveInstanceState = false; + } + + /** + * Get the image loading engine again, provided that the user implements the IApp interface in the Application + */ + private void newCreateEngine() { + if (PictureSelectionConfig.imageEngine == null) { + PictureSelectorEngine baseEngine = PictureAppMaster.getInstance().getPictureSelectorEngine(); + if (baseEngine != null) { + ImageEngine engine = baseEngine.createEngine(); + PictureSelectionConfig.imageEngine = engine; + } + } + } + + /** + * Retrieve the result callback listener, provided that the user implements the IApp interface in the Application + */ + private void newCreateResultCallbackListener() { + if (config.isCallbackMode) { + if (PictureSelectionConfig.listener == null) { + PictureSelectorEngine baseEngine = PictureAppMaster.getInstance().getPictureSelectorEngine(); + if (baseEngine != null) { + OnResultCallbackListener listener = baseEngine.getResultCallbackListener(); + PictureSelectionConfig.listener = listener; + } + } + } + } + + @Override + protected void attachBaseContext(Context newBase) { + if (config == null) { + super.attachBaseContext(newBase); + } else { + super.attachBaseContext(PictureContextWrapper.wrap(newBase, config.language)); + } + } + + /** + * CheckConfigNull + */ + private void checkConfigNull() { + if (config == null) { + config = PictureSelectionConfig.getInstance(); + } + } + + /** + * setNewRequestedOrientation + */ + protected void setNewRequestedOrientation() { + if (config != null && !config.camera) { + setRequestedOrientation(config.requestedOrientation); + } + } + + /** + * get Context + * + * @return this + */ + protected Context getContext() { + return this; + } + + /** + * init Config + */ + private void initConfig() { + selectionMedias = config.selectionMedias == null ? new ArrayList<>() : config.selectionMedias; + if (config.style != null) { + openWhiteStatusBar = config.style.isChangeStatusBarFontColor; + if (config.style.pictureTitleBarBackgroundColor != 0) { + colorPrimary = config.style.pictureTitleBarBackgroundColor; + } + if (config.style.pictureStatusBarColor != 0) { + colorPrimaryDark = config.style.pictureStatusBarColor; + } + numComplete = config.style.isOpenCompletedNumStyle; + config.checkNumMode = config.style.isOpenCheckNumStyle; + } else { + openWhiteStatusBar = config.isChangeStatusBarFontColor; + if (!openWhiteStatusBar) { + openWhiteStatusBar = AttrsUtils.getTypeValueBoolean(this, R.attr.picture_statusFontColor); + } + + numComplete = config.isOpenStyleNumComplete; + if (!numComplete) { + numComplete = AttrsUtils.getTypeValueBoolean(this, R.attr.picture_style_numComplete); + } + + config.checkNumMode = config.isOpenStyleCheckNumMode; + if (!config.checkNumMode) { + config.checkNumMode = AttrsUtils.getTypeValueBoolean(this, R.attr.picture_style_checkNumMode); + } + + if (config.titleBarBackgroundColor != 0) { + colorPrimary = config.titleBarBackgroundColor; + } else { + colorPrimary = AttrsUtils.getTypeValueColor(this, R.attr.colorPrimary); + } + + if (config.pictureStatusBarColor != 0) { + colorPrimaryDark = config.pictureStatusBarColor; + } else { + colorPrimaryDark = AttrsUtils.getTypeValueColor(this, R.attr.colorPrimaryDark); + } + } + + if (config.openClickSound) { + VoiceUtils.getInstance().init(getContext()); + } + } + + @Override + protected void onSaveInstanceState(@NotNull Bundle outState) { + super.onSaveInstanceState(outState); + isOnSaveInstanceState = true; + outState.putParcelable(PictureConfig.EXTRA_CONFIG, config); + } + + /** + * loading dialog + */ + protected void showPleaseDialog() { + try { + if (!isFinishing()) { + if (mLoadingDialog == null) { + mLoadingDialog = new PictureLoadingDialog(getContext()); + } + if (mLoadingDialog.isShowing()) { + mLoadingDialog.dismiss(); + } + mLoadingDialog.show(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * dismiss dialog + */ + protected void dismissDialog() { + if (!isFinishing()) { + try { + if (mLoadingDialog != null + && mLoadingDialog.isShowing()) { + mLoadingDialog.dismiss(); + } + } catch (Exception e) { + mLoadingDialog = null; + e.printStackTrace(); + } + } + } + + + /** + * compressImage + */ + protected void compressImage(final List result) { + showPleaseDialog(); + if (PictureSelectionConfig.cacheResourcesEngine != null) { + // 在Android 10上通过图片加载引擎的缓存来获得沙盒内的图片 + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + + @Override + public List doInBackground() { + int size = result.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = result.get(i); + if (media == null) { + continue; + } + if (!PictureMimeType.isHasHttp(media.getPath())) { + String cachePath = PictureSelectionConfig.cacheResourcesEngine.onCachePath(getContext(), media.getPath()); + media.setAndroidQToPath(cachePath); + } + } + return result; + } + + @Override + public void onSuccess(List result) { + compressToLuban(result); + } + }); + } else { + compressToLuban(result); + } + } + + /** + * compress + * + * @param result + */ + private void compressToLuban(List result) { + if (config.synOrAsy) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + + @Override + public List doInBackground() throws Exception { + return Luban.with(getContext()) + .loadMediaData(result) + .isCamera(config.camera) + .setTargetDir(config.compressSavePath) + .setCompressQuality(config.compressQuality) + .setFocusAlpha(config.focusAlpha) + .setNewCompressFileName(config.renameCompressFileName) + .ignoreBy(config.minimumCompressSize).get(); + } + + @Override + public void onSuccess(List files) { + if (files != null && files.size() > 0 && files.size() == result.size()) { + handleCompressCallBack(result, files); + } else { + onResult(result); + } + } + }); + } else { + Luban.with(this) + .loadMediaData(result) + .ignoreBy(config.minimumCompressSize) + .isCamera(config.camera) + .setCompressQuality(config.compressQuality) + .setTargetDir(config.compressSavePath) + .setFocusAlpha(config.focusAlpha) + .setNewCompressFileName(config.renameCompressFileName) + .setCompressListener(new OnCompressListener() { + @Override + public void onStart() { + } + + @Override + public void onSuccess(List list) { + onResult(list); + } + + @Override + public void onError(Throwable e) { + onResult(result); + } + }).launch(); + } + } + + /** + * handleCompressCallBack + * + * @param images + * @param files + */ + private void handleCompressCallBack(List images, List files) { + if (images == null || files == null) { + closeActivity(); + return; + } + boolean isAndroidQ = SdkVersionUtils.checkedAndroid_Q(); + int size = images.size(); + if (files.size() == size) { + for (int i = 0, j = size; i < j; i++) { + File file = files.get(i); + if (file == null) { + continue; + } + String path = file.getAbsolutePath(); + LocalMedia image = images.get(i); + boolean http = PictureMimeType.isHasHttp(path); + boolean flag = !TextUtils.isEmpty(path) && http; + boolean isHasVideo = PictureMimeType.isHasVideo(image.getMimeType()); + image.setCompressed(!isHasVideo && !flag); + image.setCompressPath(isHasVideo || flag ? null : path); + if (isAndroidQ) { + image.setAndroidQToPath(image.getCompressPath()); + } + } + } + onResult(images); + } + + /** + * crop + * + * @param originalPath + * @param mimeType + */ + protected void startCrop(String originalPath, String mimeType) { + if (DoubleUtils.isFastDoubleClick()) { + return; + } + if (TextUtils.isEmpty(originalPath)) { + ToastUtils.s(this, getString(R.string.picture_not_crop_data)); + return; + } + UCrop.Options options = basicOptions(); + if (PictureSelectionConfig.cacheResourcesEngine != null) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + @Override + public String doInBackground() { + return PictureSelectionConfig.cacheResourcesEngine.onCachePath(getContext(), originalPath); + } + + @Override + public void onSuccess(String result) { + startSingleCropActivity(originalPath, result, mimeType, options); + } + }); + } else { + startSingleCropActivity(originalPath, null, mimeType, options); + } + } + + /** + * single crop + * + * @param originalPath + * @param cachePath + * @param mimeType + * @param options + */ + private void startSingleCropActivity(String originalPath, String cachePath, String mimeType, UCrop.Options options) { + boolean isHttp = PictureMimeType.isHasHttp(originalPath); + String suffix = mimeType.replace("image/", "."); + File file = new File(PictureFileUtils.getDiskCacheDir(getContext()), + TextUtils.isEmpty(config.renameCropFileName) ? DateUtils.getCreateFileName("IMG_CROP_") + suffix : config.renameCropFileName); + Uri uri; + if (!TextUtils.isEmpty(cachePath)) { + uri = Uri.fromFile(new File(cachePath)); + } else { + uri = isHttp || SdkVersionUtils.checkedAndroid_Q() ? Uri.parse(originalPath) : Uri.fromFile(new File(originalPath)); + } + UCrop.of(uri, Uri.fromFile(file)) + .withOptions(options) + .startAnimationActivity(this, config.windowAnimationStyle != null + ? config.windowAnimationStyle.activityCropEnterAnimation : R.anim.picture_anim_enter); + } + + /** + * multiple crop + * + * @param list + */ + private int index = 0; + + protected void startCrop(ArrayList list) { + if (DoubleUtils.isFastDoubleClick()) { + return; + } + if (list == null || list.size() == 0) { + ToastUtils.s(this, getString(R.string.picture_not_crop_data)); + return; + } + UCrop.Options options = basicOptions(list); + int size = list.size(); + index = 0; + if (config.chooseMode == PictureMimeType.ofAll() && config.isWithVideoImage) { + String mimeType = size > 0 ? list.get(index).getMimeType() : ""; + boolean isHasVideo = PictureMimeType.isHasVideo(mimeType); + if (isHasVideo) { + for (int i = 0; i < size; i++) { + CutInfo cutInfo = list.get(i); + if (cutInfo != null && PictureMimeType.isHasImage(cutInfo.getMimeType())) { + index = i; + break; + } + } + } + } + + if (PictureSelectionConfig.cacheResourcesEngine != null) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + + @Override + public List doInBackground() { + for (int i = 0; i < size; i++) { + CutInfo cutInfo = list.get(i); + String cachePath = PictureSelectionConfig.cacheResourcesEngine.onCachePath(getContext(), cutInfo.getPath()); + if (!TextUtils.isEmpty(cachePath)) { + cutInfo.setAndroidQToPath(cachePath); + } + } + return list; + } + + @Override + public void onSuccess(List list) { + if (index < size) { + startMultipleCropActivity(list.get(index), size, options); + } + } + }); + + } else { + if (index < size) { + startMultipleCropActivity(list.get(index), size, options); + } + } + } + + /** + * startMultipleCropActivity + * + * @param cutInfo + * @param options + */ + private void startMultipleCropActivity(CutInfo cutInfo, int count, UCrop.Options options) { + String path = cutInfo.getPath(); + String mimeType = cutInfo.getMimeType(); + boolean isHttp = PictureMimeType.isHasHttp(path); + Uri uri; + if (!TextUtils.isEmpty(cutInfo.getAndroidQToPath())) { + uri = Uri.fromFile(new File(cutInfo.getAndroidQToPath())); + } else { + uri = isHttp || SdkVersionUtils.checkedAndroid_Q() ? Uri.parse(path) : Uri.fromFile(new File(path)); + } + String suffix = mimeType.replace("image/", "."); + File file = new File(PictureFileUtils.getDiskCacheDir(this), + TextUtils.isEmpty(config.renameCropFileName) ? DateUtils.getCreateFileName("IMG_CROP_") + + suffix : config.camera || count == 1 ? config.renameCropFileName : StringUtils.rename(config.renameCropFileName)); + UCrop.of(uri, Uri.fromFile(file)) + .withOptions(options) + .startAnimationMultipleCropActivity(this, config.windowAnimationStyle != null + ? config.windowAnimationStyle.activityCropEnterAnimation : R.anim.picture_anim_enter); + } + + /** + * Set the crop style parameter + * + * @return + */ + private UCrop.Options basicOptions() { + return basicOptions(null); + } + + /** + * Set the crop style parameter + * + * @return + */ + private UCrop.Options basicOptions(ArrayList list) { + int toolbarColor = 0, statusColor = 0, titleColor = 0; + boolean isChangeStatusBarFontColor; + if (config.cropStyle != null) { + if (config.cropStyle.cropTitleBarBackgroundColor != 0) { + toolbarColor = config.cropStyle.cropTitleBarBackgroundColor; + } + if (config.cropStyle.cropStatusBarColorPrimaryDark != 0) { + statusColor = config.cropStyle.cropStatusBarColorPrimaryDark; + } + if (config.cropStyle.cropTitleColor != 0) { + titleColor = config.cropStyle.cropTitleColor; + } + isChangeStatusBarFontColor = config.cropStyle.isChangeStatusBarFontColor; + } else { + if (config.cropTitleBarBackgroundColor != 0) { + toolbarColor = config.cropTitleBarBackgroundColor; + } else { + toolbarColor = AttrsUtils.getTypeValueColor(this, R.attr.picture_crop_toolbar_bg); + } + if (config.cropStatusBarColorPrimaryDark != 0) { + statusColor = config.cropStatusBarColorPrimaryDark; + } else { + statusColor = AttrsUtils.getTypeValueColor(this, R.attr.picture_crop_status_color); + } + if (config.cropTitleColor != 0) { + titleColor = config.cropTitleColor; + } else { + titleColor = AttrsUtils.getTypeValueColor(this, R.attr.picture_crop_title_color); + } + + isChangeStatusBarFontColor = config.isChangeStatusBarFontColor; + if (!isChangeStatusBarFontColor) { + isChangeStatusBarFontColor = AttrsUtils.getTypeValueBoolean(this, R.attr.picture_statusFontColor); + } + } + UCrop.Options options = config.uCropOptions == null ? new UCrop.Options() : config.uCropOptions; + options.isOpenWhiteStatusBar(isChangeStatusBarFontColor); + options.setToolbarColor(toolbarColor); + options.setStatusBarColor(statusColor); + options.setToolbarWidgetColor(titleColor); + options.setCircleDimmedLayer(config.circleDimmedLayer); + options.setDimmedLayerColor(config.circleDimmedColor); + options.setDimmedLayerBorderColor(config.circleDimmedBorderColor); + options.setCircleStrokeWidth(config.circleStrokeWidth); + options.setShowCropFrame(config.showCropFrame); + options.setDragFrameEnabled(config.isDragFrame); + options.setShowCropGrid(config.showCropGrid); + options.setScaleEnabled(config.scaleEnabled); + options.setRotateEnabled(config.rotateEnabled); + options.isMultipleSkipCrop(config.isMultipleSkipCrop); + options.setHideBottomControls(config.hideBottomControls); + options.setCompressionQuality(config.cropCompressQuality); + options.setRenameCropFileName(config.renameCropFileName); + options.isCamera(config.camera); + options.setCutListData(list); + options.isWithVideoImage(config.isWithVideoImage); + options.setFreeStyleCropEnabled(config.freeStyleCropEnabled); + options.setCropExitAnimation(config.windowAnimationStyle != null + ? config.windowAnimationStyle.activityCropExitAnimation : 0); + options.setNavBarColor(config.cropStyle != null ? config.cropStyle.cropNavBarColor : 0); + options.withAspectRatio(config.aspect_ratio_x, config.aspect_ratio_y); + options.isMultipleRecyclerAnimation(config.isMultipleRecyclerAnimation); + if (config.cropWidth > 0 && config.cropHeight > 0) { + options.withMaxResultSize(config.cropWidth, config.cropHeight); + } + return options; + } + + /** + * compress or callback + * + * @param result + */ + protected void handlerResult(List result) { + if (config.isCompress + && !config.isCheckOriginalImage) { + compressImage(result); + } else { + onResult(result); + } + } + + + /** + * If you don't have any albums, first create a camera film folder to come out + * + * @param folders + */ + protected void createNewFolder(List folders) { + if (folders.size() == 0) { + // 没有相册 先创建一个最近相册出来 + LocalMediaFolder newFolder = new LocalMediaFolder(); + String folderName = config.chooseMode == PictureMimeType.ofAudio() ? + getString(R.string.picture_all_audio) : getString(R.string.picture_camera_roll); + newFolder.setName(folderName); + newFolder.setFirstImagePath(""); + newFolder.setCameraFolder(true); + newFolder.setBucketId(-1); + newFolder.setChecked(true); + folders.add(newFolder); + } + } + + /** + * Insert the image into the camera folder + * + * @param path + * @param imageFolders + * @return + */ + protected LocalMediaFolder getImageFolder(String path, String realPath, List imageFolders) { + File imageFile = new File(PictureMimeType.isContent(path) ? realPath : path); + File folderFile = imageFile.getParentFile(); + for (LocalMediaFolder folder : imageFolders) { + if (folderFile != null && folder.getName().equals(folderFile.getName())) { + return folder; + } + } + LocalMediaFolder newFolder = new LocalMediaFolder(); + newFolder.setName(folderFile != null ? folderFile.getName() : ""); + newFolder.setFirstImagePath(path); + imageFolders.add(newFolder); + return newFolder; + } + + /** + * return image result + * + * @param images + */ + protected void onResult(List images) { + boolean isAndroidQ = SdkVersionUtils.checkedAndroid_Q(); + if (isAndroidQ && config.isAndroidQTransform) { + showPleaseDialog(); + onResultToAndroidAsy(images); + } else { + dismissDialog(); + if (config.camera + && config.selectionMode == PictureConfig.MULTIPLE + && selectionMedias != null) { + images.addAll(images.size() > 0 ? images.size() - 1 : 0, selectionMedias); + } + if (config.isCheckOriginalImage) { + int size = images.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = images.get(i); + media.setOriginal(true); + media.setOriginalPath(media.getPath()); + } + } + if (PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onResult(images); + } else { + Intent intent = PictureSelector.putIntentResult(images); + setResult(RESULT_OK, intent); + } + closeActivity(); + } + } + + /** + * Android Q + * + * @param images + */ + private void onResultToAndroidAsy(List images) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + @Override + public List doInBackground() { + int size = images.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = images.get(i); + if (media == null || TextUtils.isEmpty(media.getPath())) { + continue; + } + boolean isCopyAndroidQToPath = !media.isCut() + && !media.isCompressed() + && TextUtils.isEmpty(media.getAndroidQToPath()); + if (isCopyAndroidQToPath && PictureMimeType.isContent(media.getPath())) { + if (!PictureMimeType.isHasHttp(media.getPath())) { + String AndroidQToPath = AndroidQTransformUtils.copyPathToAndroidQ(getContext(), + media.getPath(), media.getWidth(), media.getHeight(), media.getMimeType(), config.cameraFileName); + media.setAndroidQToPath(AndroidQToPath); + } + } else if (media.isCut() && media.isCompressed()) { + media.setAndroidQToPath(media.getCompressPath()); + } + if (config.isCheckOriginalImage) { + media.setOriginal(true); + media.setOriginalPath(media.getAndroidQToPath()); + } + } + return images; + } + + @Override + public void onSuccess(List images) { + dismissDialog(); + if (images != null) { + if (config.camera + && config.selectionMode == PictureConfig.MULTIPLE + && selectionMedias != null) { + images.addAll(images.size() > 0 ? images.size() - 1 : 0, selectionMedias); + } + if (PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onResult(images); + } else { + Intent intent = PictureSelector.putIntentResult(images); + setResult(RESULT_OK, intent); + } + closeActivity(); + } + } + }); + } + + /** + * Close Activity + */ + protected void closeActivity() { + finish(); + if (config.camera) { + overridePendingTransition(0, R.anim.picture_anim_fade_out); + } else { + overridePendingTransition(0, config.windowAnimationStyle != null + && config.windowAnimationStyle.activityExitAnimation != 0 ? + config.windowAnimationStyle.activityExitAnimation : R.anim.picture_anim_exit); + } + if (config.camera) { + if (getContext() instanceof PictureSelectorCameraEmptyActivity + || getContext() instanceof PictureCustomCameraActivity) { + releaseResultListener(); + } + } else { + if (getContext() instanceof PictureSelectorActivity) { + releaseResultListener(); + if (config.openClickSound) { + VoiceUtils.getInstance().releaseSoundPool(); + } + } + } + } + + @Override + protected void onDestroy() { + if (mLoadingDialog != null) { + mLoadingDialog.dismiss(); + mLoadingDialog = null; + } + super.onDestroy(); + } + + + /** + * get audio path + * + * @param data + */ + protected String getAudioPath(Intent data) { + if (data != null && config.chooseMode == PictureMimeType.ofAudio()) { + try { + Uri uri = data.getData(); + if (uri != null) { + return Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT ? uri.getPath() : MediaUtils.getAudioFilePathFromUri(getContext(), uri); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + return ""; + } + + + /** + * start to camera、preview、crop + */ + protected void startOpenCamera() { + Intent cameraIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); + if (cameraIntent.resolveActivity(getPackageManager()) != null) { + Uri imageUri; + if (SdkVersionUtils.checkedAndroid_Q()) { + imageUri = MediaUtils.createImageUri(getApplicationContext(), config.suffixType); + if (imageUri != null) { + config.cameraPath = imageUri.toString(); + } else { + ToastUtils.s(getContext(), "open is camera error,the uri is empty "); + if (config.camera) { + closeActivity(); + } + return; + } + } else { + int chooseMode = config.chooseMode == PictureConfig.TYPE_ALL ? PictureConfig.TYPE_IMAGE + : config.chooseMode; + String cameraFileName = ""; + if (!TextUtils.isEmpty(config.cameraFileName)) { + boolean isSuffixOfImage = PictureMimeType.isSuffixOfImage(config.cameraFileName); + config.cameraFileName = !isSuffixOfImage ? StringUtils.renameSuffix(config.cameraFileName, PictureMimeType.JPEG) : config.cameraFileName; + cameraFileName = config.camera ? config.cameraFileName : StringUtils.rename(config.cameraFileName); + } + + File cameraFile = PictureFileUtils.createCameraFile(getApplicationContext(), + chooseMode, cameraFileName, config.suffixType, config.outPutCameraPath); + if (cameraFile != null) { + config.cameraPath = cameraFile.getAbsolutePath(); + imageUri = PictureFileUtils.parUri(this, cameraFile); + } else { + ToastUtils.s(getContext(), "open is camera error,the uri is empty "); + if (config.camera) { + closeActivity(); + } + return; + } + } + config.cameraMimeType = PictureMimeType.ofImage(); + if (config.isCameraAroundState) { + cameraIntent.putExtra(PictureConfig.CAMERA_FACING, PictureConfig.CAMERA_BEFORE); + } + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri); + startActivityForResult(cameraIntent, PictureConfig.REQUEST_CAMERA); + } + } + + + /** + * start to camera、video + */ + protected void startOpenCameraVideo() { + Intent cameraIntent = new Intent(MediaStore.ACTION_VIDEO_CAPTURE); + if (cameraIntent.resolveActivity(getPackageManager()) != null) { + Uri videoUri; + if (SdkVersionUtils.checkedAndroid_Q()) { + videoUri = MediaUtils.createVideoUri(getApplicationContext(), config.suffixType); + if (videoUri != null) { + config.cameraPath = videoUri.toString(); + } else { + ToastUtils.s(getContext(), "open is camera error,the uri is empty "); + if (config.camera) { + closeActivity(); + } + return; + } + } else { + int chooseMode = config.chooseMode == + PictureConfig.TYPE_ALL ? PictureConfig.TYPE_VIDEO : config.chooseMode; + String cameraFileName = ""; + if (!TextUtils.isEmpty(config.cameraFileName)) { + boolean isSuffixOfImage = PictureMimeType.isSuffixOfImage(config.cameraFileName); + config.cameraFileName = isSuffixOfImage ? StringUtils.renameSuffix(config.cameraFileName, PictureMimeType.MP4) : config.cameraFileName; + cameraFileName = config.camera ? config.cameraFileName : StringUtils.rename(config.cameraFileName); + } + File cameraFile = PictureFileUtils.createCameraFile(getApplicationContext(), + chooseMode, cameraFileName, config.suffixType, config.outPutCameraPath); + if (cameraFile != null) { + config.cameraPath = cameraFile.getAbsolutePath(); + videoUri = PictureFileUtils.parUri(this, cameraFile); + } else { + ToastUtils.s(getContext(), "open is camera error,the uri is empty "); + if (config.camera) { + closeActivity(); + } + return; + } + } + config.cameraMimeType = PictureMimeType.ofVideo(); + cameraIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoUri); + if (config.isCameraAroundState) { + cameraIntent.putExtra(PictureConfig.CAMERA_FACING, PictureConfig.CAMERA_BEFORE); + } + cameraIntent.putExtra(PictureConfig.EXTRA_QUICK_CAPTURE, config.isQuickCapture); + cameraIntent.putExtra(MediaStore.EXTRA_DURATION_LIMIT, config.recordVideoSecond); + cameraIntent.putExtra(MediaStore.EXTRA_VIDEO_QUALITY, config.videoQuality); + startActivityForResult(cameraIntent, PictureConfig.REQUEST_CAMERA); + } + } + + /** + * start to camera audio + */ + public void startOpenCameraAudio() { + if (PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)) { + Intent cameraIntent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + if (cameraIntent.resolveActivity(getPackageManager()) != null) { + config.cameraMimeType = PictureMimeType.ofAudio(); + startActivityForResult(cameraIntent, PictureConfig.REQUEST_CAMERA); + } + } else { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, PictureConfig.APPLY_AUDIO_PERMISSIONS_CODE); + } + } + + /** + * Release listener + */ + private void releaseResultListener() { + if (config != null) { + PictureSelectionConfig.destroy(); + LocalMediaPageLoader.setInstanceNull(); + PictureThreadUtils.cancel(PictureThreadUtils.getIoPool()); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (requestCode == PictureConfig.APPLY_AUDIO_PERMISSIONS_CODE) { + if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { + Intent cameraIntent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION); + if (cameraIntent.resolveActivity(getPackageManager()) != null) { + startActivityForResult(cameraIntent, PictureConfig.REQUEST_CAMERA); + } + } else { + ToastUtils.s(getContext(), getString(R.string.picture_audio)); + } + } + } + + /** + * showPermissionsDialog + * + * @param isCamera + * @param errorMsg + */ + protected void showPermissionsDialog(boolean isCamera, String errorMsg) { + + } + + /** + * Dialog + * + * @param content + */ + protected void showPromptDialog(String content) { + if (!isFinishing()) { + PictureCustomDialog dialog = new PictureCustomDialog(getContext(), R.layout.picture_prompt_dialog); + TextView btnOk = dialog.findViewById(R.id.btnOk); + TextView tvContent = dialog.findViewById(R.id.tv_content); + tvContent.setText(content); + btnOk.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + }); + dialog.show(); + } + } + + + /** + * sort + * + * @param imageFolders + */ + protected void sortFolder(List imageFolders) { + Collections.sort(imageFolders, (lhs, rhs) -> { + if (lhs.getData() == null || rhs.getData() == null) { + return 0; + } + int lSize = lhs.getImageNum(); + int rSize = rhs.getImageNum(); + return Integer.compare(rSize, lSize); + }); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureContextWrapper.java b/picture_library/src/main/java/com/luck/picture/lib/PictureContextWrapper.java new file mode 100644 index 0000000..c4aa53b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureContextWrapper.java @@ -0,0 +1,23 @@ +package com.luck.picture.lib; + +import android.content.Context; +import android.content.ContextWrapper; + +import com.luck.picture.lib.language.PictureLanguageUtils; + +/** + * @author:luck + * @date:2019-12-15 19:34 + * @describe:ContextWrapper + */ +public class PictureContextWrapper extends ContextWrapper { + + public PictureContextWrapper(Context base) { + super(base); + } + + public static ContextWrapper wrap(Context context, int language) { + PictureLanguageUtils.setAppLanguage(context, language); + return new PictureContextWrapper(context); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureCustomCameraActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureCustomCameraActivity.java new file mode 100644 index 0000000..d38c4fe --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureCustomCameraActivity.java @@ -0,0 +1,286 @@ +package com.luck.picture.lib; + + +import android.Manifest; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Bundle; +import android.util.Log; +import android.view.WindowManager; +import android.widget.Button; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.core.CameraX; +import androidx.camera.view.CameraView; + +import com.luck.picture.lib.camera.CustomCameraView; +import com.luck.picture.lib.camera.listener.CameraListener; +import com.luck.picture.lib.camera.view.CaptureLayout; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.dialog.PictureCustomDialog; +import com.luck.picture.lib.permissions.PermissionChecker; + +import java.io.File; +import java.lang.ref.WeakReference; + +/** + * @author:luck + * @date:2020-01-04 14:05 + * @describe:Custom photos and videos + */ +public class PictureCustomCameraActivity extends PictureSelectorCameraEmptyActivity { + private final static String TAG = PictureCustomCameraActivity.class.getSimpleName(); + + private CustomCameraView mCameraView; + protected boolean isEnterSetting; + + @Override + public boolean isImmersive() { + return false; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + getWindow().setFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + WindowManager.LayoutParams lp = getWindow().getAttributes(); + lp.layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES; + getWindow().setAttributes(lp); + } + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + super.onCreate(savedInstanceState); + // 验证存储权限 + boolean isExternalStorage = PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (!isExternalStorage) { + PermissionChecker.requestPermissions(this, new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE); + return; + } + + // 验证相机权限和麦克风权限 + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.CAMERA)) { + boolean isRecordAudio = PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO); + if (isRecordAudio) { + createCameraView(); + } else { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE); + } + } else { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE); + } + } + + /** + * 创建CameraView + */ + private void createCameraView() { + if (mCameraView == null) { + mCameraView = new CustomCameraView(getContext()); + setContentView(mCameraView); + initView(); + } + } + + @Override + protected void onResume() { + super.onResume(); + // 这里只针对权限被手动拒绝后进入设置页面重新获取权限后的操作 + if (isEnterSetting) { + boolean isExternalStorage = PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE); + if (isExternalStorage) { + boolean isCameraPermissionChecker = PermissionChecker + .checkSelfPermission(this, Manifest.permission.CAMERA); + if (isCameraPermissionChecker) { + boolean isRecordAudio = PermissionChecker + .checkSelfPermission(this, Manifest.permission.RECORD_AUDIO); + if (isRecordAudio) { + createCameraView(); + } else { + showPermissionsDialog(false, getString(R.string.picture_audio)); + } + } else { + showPermissionsDialog(false, getString(R.string.picture_camera)); + } + } else { + showPermissionsDialog(false, getString(R.string.picture_jurisdiction)); + } + isEnterSetting = false; + } + } + + /** + * 初始化控件 + */ + protected void initView() { + mCameraView.setPictureSelectionConfig(config); + // 绑定生命周期 + mCameraView.setBindToLifecycle(new WeakReference<>(this).get()); + // 视频最大拍摄时长 + if (config.recordVideoSecond > 0) { + mCameraView.setRecordVideoMaxTime(config.recordVideoSecond); + } + // 视频最小拍摄时长 + if (config.recordVideoMinSecond > 0) { + mCameraView.setRecordVideoMinTime(config.recordVideoMinSecond); + } + // 获取CameraView + CameraView cameraView = mCameraView.getCameraView(); + if (cameraView != null && config.isCameraAroundState) { + cameraView.toggleCamera(); + } + // 获取录制按钮 + CaptureLayout captureLayout = mCameraView.getCaptureLayout(); + if (captureLayout != null) { + captureLayout.setButtonFeatures(config.buttonFeatures); + } + // 拍照预览 + mCameraView.setImageCallbackListener((file, imageView) -> { + if (config != null && PictureSelectionConfig.imageEngine != null && file != null) { + PictureSelectionConfig.imageEngine.loadImage(getContext(), file.getAbsolutePath(), imageView); + } + }); + // 设置拍照或拍视频回调监听 + mCameraView.setCameraListener(new CameraListener() { + @Override + public void onPictureSuccess(@NonNull File file) { + config.cameraMimeType = PictureMimeType.ofImage(); + Intent intent = new Intent(); + intent.putExtra(PictureConfig.EXTRA_MEDIA_PATH, file.getAbsolutePath()); + intent.putExtra(PictureConfig.EXTRA_CONFIG, config); + if (config.camera) { + dispatchHandleCamera(intent); + } else { + setResult(RESULT_OK, intent); + onBackPressed(); + } + } + + @Override + public void onRecordSuccess(@NonNull File file) { + config.cameraMimeType = PictureMimeType.ofVideo(); + Intent intent = new Intent(); + intent.putExtra(PictureConfig.EXTRA_MEDIA_PATH, file.getAbsolutePath()); + intent.putExtra(PictureConfig.EXTRA_CONFIG, config); + if (config.camera) { + dispatchHandleCamera(intent); + } else { + setResult(RESULT_OK, intent); + onBackPressed(); + } + } + + @Override + public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) { + Log.i(TAG, "onError: " + message); + } + }); + + //左边按钮点击事件 + mCameraView.setOnClickListener(() -> onBackPressed()); + } + + @Override + public void onBackPressed() { + if (config != null && config.camera && PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onCancel(); + } + closeActivity(); + } + + @SuppressLint("RestrictedApi") + @Override + protected void onDestroy() { + if (mCameraView != null) { + CameraX.unbindAll(); + mCameraView = null; + } + super.onDestroy(); + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + switch (requestCode) { + case PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE: + // 存储权限 + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE); + } else { + showPermissionsDialog(true, getString(R.string.picture_jurisdiction)); + } + break; + case PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE: + // 相机权限 + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + boolean isRecordAudio = PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO); + if (isRecordAudio) { + createCameraView(); + } else { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE); + } + } else { + showPermissionsDialog(true, getString(R.string.picture_camera)); + } + break; + case PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE: + // 录音权限 + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + createCameraView(); + } else { + showPermissionsDialog(false, getString(R.string.picture_audio)); + } + break; + } + } + + @Override + protected void showPermissionsDialog(boolean isCamera, String errorMsg) { + if (isFinishing()) { + return; + } + final PictureCustomDialog dialog = + new PictureCustomDialog(getContext(), R.layout.picture_wind_base_dialog); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + Button btn_cancel = dialog.findViewById(R.id.btn_cancel); + Button btn_commit = dialog.findViewById(R.id.btn_commit); + btn_commit.setText(getString(R.string.picture_go_setting)); + TextView tvTitle = dialog.findViewById(R.id.tvTitle); + TextView tv_content = dialog.findViewById(R.id.tv_content); + tvTitle.setText(getString(R.string.picture_prompt)); + tv_content.setText(errorMsg); + btn_cancel.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + closeActivity(); + }); + btn_commit.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + PermissionChecker.launchAppDetailsSettings(getContext()); + isEnterSetting = true; + }); + dialog.show(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureExternalPreviewActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureExternalPreviewActivity.java new file mode 100644 index 0000000..99a194f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureExternalPreviewActivity.java @@ -0,0 +1,640 @@ +package com.luck.picture.lib; + +import android.Manifest; +import android.content.ContentValues; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.PointF; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; +import androidx.viewpager.widget.ViewPager; + +import com.luck.picture.lib.broadcast.BroadcastAction; +import com.luck.picture.lib.broadcast.BroadcastManager; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.dialog.PictureCustomDialog; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnImageCompleteCallback; +import com.luck.picture.lib.permissions.PermissionChecker; +import com.luck.picture.lib.photoview.PhotoView; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.AttrsUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.JumpUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.ValueOf; +import com.luck.picture.lib.widget.PreviewViewPager; +import com.luck.picture.lib.widget.longimage.ImageSource; +import com.luck.picture.lib.widget.longimage.ImageViewState; +import com.luck.picture.lib.widget.longimage.SubsamplingScaleImageView; + +import java.io.File; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import okio.BufferedSource; +import okio.Okio; + +/** + * @author:luck + * @data:2017/01/18 下午1:00 + * @描述: 预览图片 + */ +public class PictureExternalPreviewActivity extends PictureBaseActivity implements View.OnClickListener { + + private ImageButton ibLeftBack; + private TextView tvTitle; + private PreviewViewPager viewPager; + private List images = new ArrayList<>(); + private int position = 0; + private SimpleFragmentAdapter adapter; + private String downloadPath; + private String mMimeType; + private ImageButton ibDelete; + private View titleViewBg; + + @Override + public int getResourceId() { + return R.layout.picture_activity_external_preview; + } + + @Override + protected void initWidgets() { + super.initWidgets(); + titleViewBg = findViewById(R.id.titleViewBg); + tvTitle = findViewById(R.id.picture_title); + ibLeftBack = findViewById(R.id.left_back); + ibDelete = findViewById(R.id.ib_delete); + viewPager = findViewById(R.id.preview_pager); + position = getIntent().getIntExtra(PictureConfig.EXTRA_POSITION, 0); + images = (List) getIntent().getSerializableExtra(PictureConfig.EXTRA_PREVIEW_SELECT_LIST); + ibLeftBack.setOnClickListener(this); + ibDelete.setOnClickListener(this); + ibDelete.setVisibility(config.style != null ? config.style.pictureExternalPreviewGonePreviewDelete + ? View.VISIBLE : View.GONE : View.GONE); + initViewPageAdapterData(); + } + + /** + * 设置样式 + */ + @Override + public void initPictureSelectorStyle() { + if (config.style != null) { + if (config.style.pictureTitleTextColor != 0) { + tvTitle.setTextColor(config.style.pictureTitleTextColor); + } + if (config.style.pictureTitleTextSize != 0) { + tvTitle.setTextSize(config.style.pictureTitleTextSize); + } + if (config.style.pictureLeftBackIcon != 0) { + ibLeftBack.setImageResource(config.style.pictureLeftBackIcon); + } + if (config.style.pictureExternalPreviewDeleteStyle != 0) { + ibDelete.setImageResource(config.style.pictureExternalPreviewDeleteStyle); + } + if (config.style.pictureTitleBarBackgroundColor != 0) { + titleViewBg.setBackgroundColor(colorPrimary); + } + } else { + int previewBgColor = AttrsUtils.getTypeValueColor(getContext(), R.attr.picture_ac_preview_title_bg); + if (previewBgColor != 0) { + titleViewBg.setBackgroundColor(previewBgColor); + } else { + titleViewBg.setBackgroundColor(colorPrimary); + } + } + } + + private void initViewPageAdapterData() { + tvTitle.setText(getString(R.string.picture_preview_image_num, + position + 1, images.size())); + adapter = new SimpleFragmentAdapter(); + viewPager.setAdapter(adapter); + viewPager.setCurrentItem(position); + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + + } + + @Override + public void onPageSelected(int index) { + tvTitle.setText(getString(R.string.picture_preview_image_num, + index + 1, images.size())); + position = index; + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.left_back) { + finish(); + exitAnimation(); + } else if (id == R.id.ib_delete) { + if (images != null && images.size() > 0) { + int currentItem = viewPager.getCurrentItem(); + images.remove(currentItem); + adapter.removeCacheView(currentItem); + // 删除通知用户更新 + Bundle bundle = new Bundle(); + bundle.putInt(PictureConfig.EXTRA_PREVIEW_DELETE_POSITION, currentItem); + BroadcastManager.getInstance(getContext()) + .action(BroadcastAction.ACTION_DELETE_PREVIEW_POSITION) + .extras(bundle).broadcast(); + if (images.size() == 0) { + onBackPressed(); + return; + } + tvTitle.setText(getString(R.string.picture_preview_image_num, + position + 1, images.size())); + position = currentItem; + adapter.notifyDataSetChanged(); + } + } + } + + public class SimpleFragmentAdapter extends PagerAdapter { + + /** + * 最大缓存图片数量 + */ + private static final int MAX_CACHE_SIZE = 20; + /** + * 缓存view + */ + private SparseArray mCacheView; + + private void clear() { + if (null != mCacheView) { + mCacheView.clear(); + mCacheView = null; + } + } + + public void removeCacheView(int position) { + if (mCacheView != null && position < mCacheView.size()) { + mCacheView.removeAt(position); + } + } + + public SimpleFragmentAdapter() { + super(); + this.mCacheView = new SparseArray<>(); + } + + @Override + public int getCount() { + return images != null ? images.size() : 0; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + (container).removeView((View) object); + if (mCacheView.size() > MAX_CACHE_SIZE) { + mCacheView.remove(position); + } + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + @Override + public boolean isViewFromObject(View view, Object object) { + return view == object; + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + View contentView = mCacheView.get(position); + if (contentView == null) { + contentView = LayoutInflater.from(container.getContext()) + .inflate(R.layout.picture_image_preview, container, false); + mCacheView.put(position, contentView); + } + // 常规图控件 + final PhotoView imageView = contentView.findViewById(R.id.preview_image); + // 长图控件 + final SubsamplingScaleImageView longImageView = contentView.findViewById(R.id.longImg); + // 视频播放按钮 + ImageView ivPlay = contentView.findViewById(R.id.iv_play); + LocalMedia media = images.get(position); + if (media != null) { + final String path; + if (media.isCut() && !media.isCompressed()) { + // 裁剪过 + path = media.getCutPath(); + } else if (media.isCompressed() || (media.isCut() && media.isCompressed())) { + // 压缩过,或者裁剪同时压缩过,以最终压缩过图片为准 + path = media.getCompressPath(); + } else if (!TextUtils.isEmpty(media.getAndroidQToPath())) { + // AndroidQ特有path + path = media.getAndroidQToPath(); + } else { + // 原图 + path = media.getPath(); + } + boolean isHttp = PictureMimeType.isHasHttp(path); + String mimeType = isHttp ? PictureMimeType.getImageMimeType(media.getPath()) : media.getMimeType(); + boolean isHasVideo = PictureMimeType.isHasVideo(mimeType); + ivPlay.setVisibility(isHasVideo ? View.VISIBLE : View.GONE); + boolean isGif = PictureMimeType.isGif(mimeType); + boolean eqLongImg = MediaUtils.isLongImg(media); + imageView.setVisibility(eqLongImg && !isGif ? View.GONE : View.VISIBLE); + longImageView.setVisibility(eqLongImg && !isGif ? View.VISIBLE : View.GONE); + // 压缩过的gif就不是gif了 + if (isGif && !media.isCompressed()) { + if (config != null && PictureSelectionConfig.imageEngine != null) { + PictureSelectionConfig.imageEngine.loadAsGifImage + (getContext(), path, imageView); + } + } else { + if (config != null && PictureSelectionConfig.imageEngine != null) { + if (isHttp) { + // 网络图片 + PictureSelectionConfig.imageEngine.loadImage(contentView.getContext(), path, + imageView, longImageView, new OnImageCompleteCallback() { + @Override + public void onShowLoading() { + showPleaseDialog(); + } + + @Override + public void onHideLoading() { + dismissDialog(); + } + }); + } else { + if (eqLongImg) { + displayLongPic(PictureMimeType.isContent(path) + ? Uri.parse(path) : Uri.fromFile(new File(path)), longImageView); + } else { + PictureSelectionConfig.imageEngine.loadImage(contentView.getContext(), path, imageView); + } + } + } + } + imageView.setOnViewTapListener((view, x, y) -> { + finish(); + exitAnimation(); + }); + longImageView.setOnClickListener(v -> { + finish(); + exitAnimation(); + }); + if (!isHasVideo) { + longImageView.setOnLongClickListener(v -> { + if (config.isNotPreviewDownload) { + if (PermissionChecker.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + downloadPath = path; + String currentMimeType = PictureMimeType.isHasHttp(path) ? PictureMimeType.getImageMimeType(media.getPath()) : media.getMimeType(); + mMimeType = PictureMimeType.isJPG(currentMimeType) ? PictureMimeType.MIME_TYPE_JPEG : currentMimeType; + showDownLoadDialog(); + } else { + PermissionChecker.requestPermissions(PictureExternalPreviewActivity.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE); + } + } + return true; + }); + } + if (!isHasVideo) { + imageView.setOnLongClickListener(v -> { + if (config.isNotPreviewDownload) { + if (PermissionChecker.checkSelfPermission(getContext(), Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + downloadPath = path; + String currentMimeType = PictureMimeType.isHasHttp(path) ? PictureMimeType.getImageMimeType(media.getPath()) : media.getMimeType(); + mMimeType = PictureMimeType.isJPG(currentMimeType) ? PictureMimeType.MIME_TYPE_JPEG : currentMimeType; + showDownLoadDialog(); + } else { + PermissionChecker.requestPermissions(PictureExternalPreviewActivity.this, + new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE); + } + } + return true; + }); + } + ivPlay.setOnClickListener(v -> { + if (PictureSelectionConfig.customVideoPlayCallback != null) { + PictureSelectionConfig.customVideoPlayCallback.startPlayVideo(media); + } else { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putString(PictureConfig.EXTRA_VIDEO_PATH, path); + intent.putExtras(bundle); + JumpUtils.startPictureVideoPlayActivity(container.getContext(), bundle, PictureConfig.PREVIEW_VIDEO_CODE); + } + }); + } + (container).addView(contentView, 0); + return contentView; + } + } + + /** + * 加载长图 + * + * @param uri + * @param longImg + */ + private void displayLongPic(Uri uri, SubsamplingScaleImageView longImg) { + longImg.setQuickScaleEnabled(true); + longImg.setZoomEnabled(true); + longImg.setPanEnabled(true); + longImg.setDoubleTapZoomDuration(100); + longImg.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); + longImg.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER); + longImg.setImage(ImageSource.uri(uri), new ImageViewState(0, new PointF(0, 0), 0)); + } + + /** + * 下载图片提示 + */ + private void showDownLoadDialog() { + if (!isFinishing() && !TextUtils.isEmpty(downloadPath)) { + final PictureCustomDialog dialog = + new PictureCustomDialog(getContext(), R.layout.picture_wind_base_dialog); + Button btn_cancel = dialog.findViewById(R.id.btn_cancel); + Button btn_commit = dialog.findViewById(R.id.btn_commit); + TextView tvTitle = dialog.findViewById(R.id.tvTitle); + TextView tv_content = dialog.findViewById(R.id.tv_content); + tvTitle.setText(getString(R.string.picture_prompt)); + tv_content.setText(getString(R.string.picture_prompt_content)); + btn_cancel.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + }); + btn_commit.setOnClickListener(view -> { + boolean isHttp = PictureMimeType.isHasHttp(downloadPath); + showPleaseDialog(); + if (isHttp) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + @Override + public String doInBackground() { + return showLoadingImage(downloadPath); + } + + @Override + public void onSuccess(String result) { + onSuccessful(result); + } + }); + } else { + // 有可能本地图片 + try { + if (PictureMimeType.isContent(downloadPath)) { + savePictureAlbumAndroidQ(PictureMimeType.isContent(downloadPath) ? Uri.parse(downloadPath) : Uri.fromFile(new File(downloadPath))); + } else { + // 把文件插入到系统图库 + savePictureAlbum(); + } + } catch (Exception e) { + ToastUtils.s(getContext(), getString(R.string.picture_save_error) + "\n" + e.getMessage()); + dismissDialog(); + e.printStackTrace(); + } + } + if (!isFinishing()) { + dialog.dismiss(); + } + }); + dialog.show(); + } + } + + /** + * 保存相片至本地相册 + * + * @throws Exception + */ + private void savePictureAlbum() throws Exception { + String suffix = PictureMimeType.getLastImgSuffix(mMimeType); + String state = Environment.getExternalStorageState(); + File rootDir = state.equals(Environment.MEDIA_MOUNTED) + ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + : getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (rootDir != null && !rootDir.exists() && rootDir.mkdirs()) { + } + File folderDir = new File(SdkVersionUtils.checkedAndroid_Q() || !state.equals(Environment.MEDIA_MOUNTED) + ? rootDir.getAbsolutePath() : rootDir.getAbsolutePath() + File.separator + PictureMimeType.CAMERA + File.separator); + if (folderDir != null && !folderDir.exists() && folderDir.mkdirs()) { + } + String fileName = DateUtils.getCreateFileName("IMG_") + suffix; + File file = new File(folderDir, fileName); + PictureFileUtils.copyFile(downloadPath, file.getAbsolutePath()); + onSuccessful(file.getAbsolutePath()); + } + + /** + * 图片保存成功 + * + * @param result + */ + private void onSuccessful(String result) { + dismissDialog(); + if (!TextUtils.isEmpty(result)) { + try { + if (!SdkVersionUtils.checkedAndroid_Q()) { + File file = new File(result); + MediaStore.Images.Media.insertImage(getContentResolver(), file.getAbsolutePath(), file.getName(), null); + new PictureMediaScannerConnection(getContext(), file.getAbsolutePath(), () -> { + }); + } + ToastUtils.s(getContext(), getString(R.string.picture_save_success) + "\n" + result); + } catch (Exception e) { + e.printStackTrace(); + } + } else { + ToastUtils.s(getContext(), getString(R.string.picture_save_error)); + } + } + + /** + * 保存图片到picture 目录,Android Q适配,最简单的做法就是保存到公共目录,不用SAF存储 + * + * @param inputUri + */ + private void savePictureAlbumAndroidQ(Uri inputUri) { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_")); + contentValues.put(MediaStore.Images.Media.DATE_TAKEN, ValueOf.toString(System.currentTimeMillis())); + contentValues.put(MediaStore.Images.Media.MIME_TYPE, mMimeType); + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM); + Uri uri = getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); + if (uri == null) { + ToastUtils.s(getContext(), getString(R.string.picture_save_error)); + return; + } + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public String doInBackground() { + BufferedSource buffer = null; + try { + buffer = Okio.buffer(Okio.source(Objects.requireNonNull(getContentResolver().openInputStream(inputUri)))); + OutputStream outputStream = getContentResolver().openOutputStream(uri); + boolean bufferCopy = PictureFileUtils.bufferCopy(buffer, outputStream); + if (bufferCopy) { + return PictureFileUtils.getPath(getContext(), uri); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (buffer != null && buffer.isOpen()) { + PictureFileUtils.close(buffer); + } + } + return ""; + } + + @Override + public void onSuccess(String result) { + PictureThreadUtils.cancel(PictureThreadUtils.getIoPool()); + onSuccessful(result); + } + }); + } + + + /** + * 针对Q版本创建uri + * + * @return + */ + private Uri createOutImageUri() { + ContentValues contentValues = new ContentValues(); + contentValues.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_")); + contentValues.put(MediaStore.Images.Media.DATE_TAKEN, ValueOf.toString(System.currentTimeMillis())); + contentValues.put(MediaStore.Images.Media.MIME_TYPE, mMimeType); + contentValues.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM); + + return getContentResolver().insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues); + } + + // 下载图片保存至手机 + public String showLoadingImage(String urlPath) { + Uri outImageUri = null; + OutputStream outputStream = null; + InputStream inputStream = null; + BufferedSource inBuffer = null; + try { + if (SdkVersionUtils.checkedAndroid_Q()) { + outImageUri = createOutImageUri(); + } else { + String suffix = PictureMimeType.getLastImgSuffix(mMimeType); + String state = Environment.getExternalStorageState(); + File rootDir = + state.equals(Environment.MEDIA_MOUNTED) + ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + : getContext().getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (rootDir != null) { + if (!rootDir.exists()) { + rootDir.mkdirs(); + } + File folderDir = new File(!state.equals(Environment.MEDIA_MOUNTED) + ? rootDir.getAbsolutePath() : rootDir.getAbsolutePath() + File.separator + PictureMimeType.CAMERA + File.separator); + if (!folderDir.exists() && folderDir.mkdirs()) { + } + String fileName = DateUtils.getCreateFileName("IMG_") + suffix; + File file = new File(folderDir, fileName); + outImageUri = Uri.fromFile(file); + } + } + if (outImageUri != null) { + outputStream = Objects.requireNonNull(getContentResolver().openOutputStream(outImageUri)); + URL u = new URL(urlPath); + inputStream = u.openStream(); + inBuffer = Okio.buffer(Okio.source(inputStream)); + boolean bufferCopy = PictureFileUtils.bufferCopy(inBuffer, outputStream); + if (bufferCopy) { + return PictureFileUtils.getPath(this, outImageUri); + } + } + } catch (Exception e) { + if (outImageUri != null && SdkVersionUtils.checkedAndroid_Q()) { + getContentResolver().delete(outImageUri, null, null); + } + } finally { + PictureFileUtils.close(inputStream); + PictureFileUtils.close(outputStream); + PictureFileUtils.close(inBuffer); + } + return null; + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + finish(); + exitAnimation(); + } + + private void exitAnimation() { + overridePendingTransition(R.anim.picture_anim_fade_in, config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewExitAnimation != 0 + ? config.windowAnimationStyle.activityPreviewExitAnimation : R.anim.picture_anim_exit); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (adapter != null) { + adapter.clear(); + } + if (PictureSelectionConfig.customVideoPlayCallback != null) { + PictureSelectionConfig.customVideoPlayCallback = null; + } + if (PictureSelectionConfig.onCustomCameraInterfaceListener != null) { + PictureSelectionConfig.onCustomCameraInterfaceListener = null; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE: + // 存储权限 + for (int i = 0; i < grantResults.length; i++) { + if (grantResults[i] == PackageManager.PERMISSION_GRANTED) { + showDownLoadDialog(); + } else { + ToastUtils.s(getContext(), getString(R.string.picture_jurisdiction)); + } + } + break; + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureFileProvider.java b/picture_library/src/main/java/com/luck/picture/lib/PictureFileProvider.java new file mode 100644 index 0000000..54816d4 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureFileProvider.java @@ -0,0 +1,12 @@ +package com.luck.picture.lib; + +import androidx.core.content.FileProvider; + +/** + * @author:luck + * @data:2016/3/28 下午2:00 + * @描述: 自定义FileProvider 解决FileProvider冲突问题 + */ + +public class PictureFileProvider extends FileProvider { +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureMediaScannerConnection.java b/picture_library/src/main/java/com/luck/picture/lib/PictureMediaScannerConnection.java new file mode 100644 index 0000000..75a5f5b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureMediaScannerConnection.java @@ -0,0 +1,49 @@ +package com.luck.picture.lib; + +import android.content.Context; +import android.media.MediaScannerConnection; +import android.net.Uri; +import android.text.TextUtils; + +/** + * @author:luck + * @date:2019-12-03 10:41 + * @describe:刷新相册 + */ +public class PictureMediaScannerConnection implements MediaScannerConnection.MediaScannerConnectionClient { + public interface ScanListener { + void onScanFinish(); + } + + private MediaScannerConnection mMs; + private String mPath; + private ScanListener mListener; + + public PictureMediaScannerConnection(Context context, String path, ScanListener l) { + this.mListener = l; + this.mPath = path; + this.mMs = new MediaScannerConnection(context.getApplicationContext(), this); + this.mMs.connect(); + } + + public PictureMediaScannerConnection(Context context, String path) { + this.mPath = path; + this.mMs = new MediaScannerConnection(context.getApplicationContext(), this); + this.mMs.connect(); + } + + @Override + public void onMediaScannerConnected() { + if (!TextUtils.isEmpty(mPath)) { + mMs.scanFile(mPath, null); + } + } + + @Override + public void onScanCompleted(String path, Uri uri) { + mMs.disconnect(); + if (mListener != null) { + mListener.onScanFinish(); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PicturePlayAudioActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PicturePlayAudioActivity.java new file mode 100644 index 0000000..0940b0e --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PicturePlayAudioActivity.java @@ -0,0 +1,211 @@ +package com.luck.picture.lib; + +import android.media.MediaPlayer; +import android.os.Bundle; +import android.os.Handler; +import android.view.View; +import android.view.WindowManager; +import android.widget.SeekBar; +import android.widget.TextView; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.tools.DateUtils; + +/** + * # No longer maintain audio related functions, + * but can continue to use but there will be phone compatibility issues. + *

+ * 不再维护音频相关功能,但可以继续使用但会有机型兼容性问题 + */ +@Deprecated +public class PicturePlayAudioActivity extends PictureBaseActivity implements View.OnClickListener { + private String audio_path; + private MediaPlayer mediaPlayer; + private SeekBar musicSeekBar; + private boolean isPlayAudio = false; + private TextView tv_PlayPause, tv_Stop, tv_Quit, + tv_musicStatus, tv_musicTotal, tv_musicTime; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN + , WindowManager.LayoutParams.FLAG_FULLSCREEN); + super.onCreate(savedInstanceState); + } + + @Override + public int getResourceId() { + return R.layout.picture_play_audio; + } + + @Override + protected void initWidgets() { + super.initWidgets(); + audio_path = getIntent().getStringExtra(PictureConfig.EXTRA_AUDIO_PATH); + tv_musicStatus = findViewById(R.id.tv_musicStatus); + tv_musicTime = findViewById(R.id.tv_musicTime); + musicSeekBar = findViewById(R.id.musicSeekBar); + tv_musicTotal = findViewById(R.id.tv_musicTotal); + tv_PlayPause = findViewById(R.id.tv_PlayPause); + tv_Stop = findViewById(R.id.tv_Stop); + tv_Quit = findViewById(R.id.tv_Quit); + handler.postDelayed(() -> initPlayer(audio_path), 30); + tv_PlayPause.setOnClickListener(this); + tv_Stop.setOnClickListener(this); + tv_Quit.setOnClickListener(this); + musicSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mediaPlayer.seekTo(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + } + + // 通过 Handler 更新 UI 上的组件状态 + public Handler handler = new Handler(); + public Runnable runnable = new Runnable() { + @Override + public void run() { + try { + if (mediaPlayer != null) { + tv_musicTime.setText(DateUtils.formatDurationTime(mediaPlayer.getCurrentPosition())); + musicSeekBar.setProgress(mediaPlayer.getCurrentPosition()); + musicSeekBar.setMax(mediaPlayer.getDuration()); + tv_musicTotal.setText(DateUtils.formatDurationTime(mediaPlayer.getDuration())); + handler.postDelayed(runnable, 200); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + /** + * 初始化音频播放组件 + * + * @param path + */ + private void initPlayer(String path) { + mediaPlayer = new MediaPlayer(); + try { + mediaPlayer.setDataSource(path); + mediaPlayer.prepare(); + mediaPlayer.setLooping(true); + playAudio(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onClick(View v) { + int i = v.getId(); + if (i == R.id.tv_PlayPause) { + playAudio(); + + } + if (i == R.id.tv_Stop) { + tv_musicStatus.setText(getString(R.string.picture_stop_audio)); + tv_PlayPause.setText(getString(R.string.picture_play_audio)); + stop(audio_path); + + } + if (i == R.id.tv_Quit) { + handler.removeCallbacks(runnable); + new Handler().postDelayed(() -> stop(audio_path), 30); + try { + closeActivity(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + + /** + * 播放音频 + */ + private void playAudio() { + if (mediaPlayer != null) { + musicSeekBar.setProgress(mediaPlayer.getCurrentPosition()); + musicSeekBar.setMax(mediaPlayer.getDuration()); + } + String ppStr = tv_PlayPause.getText().toString(); + if (ppStr.equals(getString(R.string.picture_play_audio))) { + tv_PlayPause.setText(getString(R.string.picture_pause_audio)); + tv_musicStatus.setText(getString(R.string.picture_play_audio)); + playOrPause(); + } else { + tv_PlayPause.setText(getString(R.string.picture_play_audio)); + tv_musicStatus.setText(getString(R.string.picture_pause_audio)); + playOrPause(); + } + if (!isPlayAudio) { + handler.post(runnable); + isPlayAudio = true; + } + } + + /** + * 停止播放 + * + * @param path + */ + public void stop(String path) { + if (mediaPlayer != null) { + try { + mediaPlayer.stop(); + mediaPlayer.reset(); + mediaPlayer.setDataSource(path); + mediaPlayer.prepare(); + mediaPlayer.seekTo(0); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 暂停播放 + */ + public void playOrPause() { + try { + if (mediaPlayer != null) { + if (mediaPlayer.isPlaying()) { + mediaPlayer.pause(); + } else { + mediaPlayer.start(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + closeActivity(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (mediaPlayer != null && handler != null) { + handler.removeCallbacks(runnable); + mediaPlayer.release(); + mediaPlayer = null; + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PicturePreviewActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PicturePreviewActivity.java new file mode 100644 index 0000000..47bef7f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PicturePreviewActivity.java @@ -0,0 +1,1032 @@ +package com.luck.picture.lib; + +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.os.Handler; +import android.os.Parcelable; +import android.text.TextUtils; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.viewpager.widget.ViewPager; + +import com.luck.picture.lib.adapter.PictureSimpleFragmentAdapter; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnQueryDataResultListener; +import com.luck.picture.lib.model.LocalMediaPageLoader; +import com.luck.picture.lib.observable.ImagesObservable; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.ScreenUtils; +import com.luck.picture.lib.tools.StringUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.ValueOf; +import com.luck.picture.lib.tools.VoiceUtils; +import com.luck.picture.lib.widget.PreviewViewPager; +import com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.model.CutInfo; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @data:2016/1/29 下午21:50 + * @描述:图片预览 + */ +public class PicturePreviewActivity extends PictureBaseActivity implements + View.OnClickListener, PictureSimpleFragmentAdapter.OnCallBackActivity { + private static final String TAG = PicturePreviewActivity.class.getSimpleName(); + protected ImageView pictureLeftBack; + protected TextView tvMediaNum, tvTitle, mTvPictureOk; + protected PreviewViewPager viewPager; + protected int position; + protected boolean isBottomPreview; + private int totalNumber; + protected List selectData = new ArrayList<>(); + protected PictureSimpleFragmentAdapter adapter; + protected Animation animation; + protected TextView check; + protected View btnCheck; + protected boolean refresh; + protected int index; + protected int screenWidth; + protected Handler mHandler; + protected RelativeLayout selectBarLayout; + protected CheckBox mCbOriginal; + protected View titleViewBg; + protected boolean isShowCamera; + protected String currentDirectory; + /** + * 是否已完成选择 + */ + protected boolean isCompleteOrSelected; + /** + * 是否改变已选的数据 + */ + protected boolean isChangeSelectedData; + + /** + * 分页码 + */ + private int mPage = 0; + + + @Override + public int getResourceId() { + return R.layout.picture_preview; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + // 防止内存不足时activity被回收,导致图片未选中 + selectData = PictureSelector.obtainSelectorList(savedInstanceState); + isCompleteOrSelected = savedInstanceState.getBoolean(PictureConfig.EXTRA_COMPLETE_SELECTED, false); + isChangeSelectedData = savedInstanceState.getBoolean(PictureConfig.EXTRA_CHANGE_SELECTED_DATA, false); + onImageChecked(position); + onSelectNumChange(false); + } + } + + @Override + protected void initWidgets() { + super.initWidgets(); + mHandler = new Handler(); + titleViewBg = findViewById(R.id.titleViewBg); + screenWidth = ScreenUtils.getScreenWidth(this); + animation = AnimationUtils.loadAnimation(this, R.anim.picture_anim_modal_in); + pictureLeftBack = findViewById(R.id.pictureLeftBack); + viewPager = findViewById(R.id.preview_pager); + btnCheck = findViewById(R.id.btnCheck); + check = findViewById(R.id.check); + pictureLeftBack.setOnClickListener(this); + mTvPictureOk = findViewById(R.id.tv_ok); + mCbOriginal = findViewById(R.id.cb_original); + tvMediaNum = findViewById(R.id.tvMediaNum); + selectBarLayout = findViewById(R.id.select_bar_layout); + mTvPictureOk.setOnClickListener(this); + tvMediaNum.setOnClickListener(this); + tvTitle = findViewById(R.id.picture_title); + position = getIntent().getIntExtra(PictureConfig.EXTRA_POSITION, 0); + if (numComplete) { + initCompleteText(0); + } + tvMediaNum.setSelected(config.checkNumMode); + btnCheck.setOnClickListener(this); + selectData = getIntent(). + getParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST); + isBottomPreview = getIntent(). + getBooleanExtra(PictureConfig.EXTRA_BOTTOM_PREVIEW, false); + isShowCamera = getIntent().getBooleanExtra(PictureConfig.EXTRA_SHOW_CAMERA, config.isCamera); + // 当前目录 + currentDirectory = getIntent().getStringExtra(PictureConfig.EXTRA_IS_CURRENT_DIRECTORY); + List data; + if (isBottomPreview) { + // 底部预览模式 + data = getIntent(). + getParcelableArrayListExtra(PictureConfig.EXTRA_PREVIEW_SELECT_LIST); + initViewPageAdapterData(data); + } else { + data = ImagesObservable.getInstance().readPreviewMediaData(); + boolean isEmpty = data.size() == 0; + totalNumber = getIntent().getIntExtra(PictureConfig.EXTRA_DATA_COUNT, 0); + if (config.isPageStrategy) { + // 分页模式 + if (isEmpty) { + // 这种情况有可能是单例被回收了导致readPreviewMediaData();返回的数据为0,那就从第一页开始加载吧 + setNewTitle(); + } else { + mPage = getIntent().getIntExtra(PictureConfig.EXTRA_PAGE, 0); + } + initViewPageAdapterData(data); + loadData(); + setTitle(); + } else { + // 普通模式 + initViewPageAdapterData(data); + if (isEmpty) { + // 这种情况有可能是单例被回收了导致readPreviewMediaData();返回的数据为0,暂时自动切换成分页模式去获取数据 + config.isPageStrategy = true; + setNewTitle(); + loadData(); + } + } + } + + viewPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() { + @Override + public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) { + isPreviewEggs(config.previewEggs, position, positionOffsetPixels); + } + + @Override + public void onPageSelected(int i) { + position = i; + setTitle(); + LocalMedia media = adapter.getItem(position); + if (media == null) { + return; + } + index = media.getPosition(); + if (!config.previewEggs) { + if (config.checkNumMode) { + check.setText(ValueOf.toString(media.getNum())); + notifyCheckChanged(media); + } + onImageChecked(position); + } + + if (config.isOriginalControl) { + boolean isHasVideo = PictureMimeType.isHasVideo(media.getMimeType()); + mCbOriginal.setVisibility(isHasVideo ? View.GONE : View.VISIBLE); + mCbOriginal.setChecked(config.isCheckOriginalImage); + } + onPageSelectedChange(media); + + if (config.isPageStrategy && !isBottomPreview) { + if (isHasMore) { + // 滑到adapter.getSize() - PictureConfig.MIN_PAGE_SIZE时或最后一条时预加载 + if (position == (adapter.getSize() - 1) - PictureConfig.MIN_PAGE_SIZE || position == adapter.getSize() - 1) { + loadMoreData(); + } + } + } + } + + @Override + public void onPageScrollStateChanged(int state) { + } + }); + // 原图 + if (config.isOriginalControl) { + boolean isCheckOriginal = getIntent() + .getBooleanExtra(PictureConfig.EXTRA_CHANGE_ORIGINAL, config.isCheckOriginalImage); + mCbOriginal.setVisibility(View.VISIBLE); + config.isCheckOriginalImage = isCheckOriginal; + mCbOriginal.setChecked(config.isCheckOriginalImage); + mCbOriginal.setOnCheckedChangeListener((buttonView, isChecked) -> { + config.isCheckOriginalImage = isChecked; + }); + } + } + + /** + * 从本地获取数据 + */ + private void loadData() { + long bucketId = getIntent().getLongExtra(PictureConfig.EXTRA_BUCKET_ID, -1); + mPage++; + LocalMediaPageLoader.getInstance(getContext(), config).loadPageMediaData(bucketId, mPage, config.pageSize, + (OnQueryDataResultListener) (result, currentPage, isHasMore) -> { + if (!isFinishing()) { + this.isHasMore = isHasMore; + if (isHasMore) { + int size = result.size(); + if (size > 0 && adapter != null) { + adapter.getData().addAll(result); + adapter.notifyDataSetChanged(); + } else { + // 这种情况就是开启过滤损坏文件刚好导致某一页全是损坏的虽然result为0,但还要请求下一页数据 + loadMoreData(); + } + } + } + }); + } + + /** + * 加载更多 + */ + private void loadMoreData() { + long bucketId = getIntent().getLongExtra(PictureConfig.EXTRA_BUCKET_ID, -1); + mPage++; + LocalMediaPageLoader.getInstance(getContext(), config).loadPageMediaData(bucketId, mPage, config.pageSize, + (OnQueryDataResultListener) (result, currentPage, isHasMore) -> { + if (!isFinishing()) { + this.isHasMore = isHasMore; + if (isHasMore) { + int size = result.size(); + if (size > 0 && adapter != null) { + adapter.getData().addAll(result); + adapter.notifyDataSetChanged(); + } else { + // 这种情况就是开启过滤损坏文件刚好导致某一页全是损坏的虽然result为0,但还要请求下一页数据 + loadMoreData(); + } + } + } + }); + } + + @Override + protected void initCompleteText(int startCount) { + boolean isNotEmptyStyle = config.style != null; + if (config.selectionMode == PictureConfig.SINGLE) { + if (startCount <= 0) { + // 未选择任何图片 + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_please_select)); + } else { + // 已选择 + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(String.format(config.style.pictureCompleteText, startCount, 1)); + } else { + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_done)); + } + } + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (startCount <= 0) { + // 未选择任何图片 + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_done_front_num, + startCount, config.maxSelectNum)); + } else { + // 已选择 + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(String.format(config.style.pictureCompleteText, startCount, config.maxSelectNum)); + } else { + mTvPictureOk.setText(getString(R.string.picture_done_front_num, + startCount, config.maxSelectNum)); + } + } + } + } + + /** + * ViewPage滑动数据变化回调 + * + * @param media + */ + protected void onPageSelectedChange(LocalMedia media) { + + } + + /** + * 动态设置相册主题 + */ + @Override + public void initPictureSelectorStyle() { + if (config.style != null) { + if (config.style.pictureTitleTextColor != 0) { + tvTitle.setTextColor(config.style.pictureTitleTextColor); + } + if (config.style.pictureTitleTextSize != 0) { + tvTitle.setTextSize(config.style.pictureTitleTextSize); + } + if (config.style.pictureLeftBackIcon != 0) { + pictureLeftBack.setImageResource(config.style.pictureLeftBackIcon); + } + if (config.style.picturePreviewBottomBgColor != 0) { + selectBarLayout.setBackgroundColor(config.style.picturePreviewBottomBgColor); + } + if (config.style.pictureCheckNumBgStyle != 0) { + tvMediaNum.setBackgroundResource(config.style.pictureCheckNumBgStyle); + } + if (config.style.pictureCheckedStyle != 0) { + check.setBackgroundResource(config.style.pictureCheckedStyle); + } + if (config.style.pictureUnCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureUnCompleteTextColor); + } + if (!TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mTvPictureOk.setText(config.style.pictureUnCompleteText); + } + } + titleViewBg.setBackgroundColor(colorPrimary); + + if (config.isOriginalControl) { + if (config.style != null) { + if (config.style.pictureOriginalControlStyle != 0) { + mCbOriginal.setButtonDrawable(config.style.pictureOriginalControlStyle); + } else { + mCbOriginal.setButtonDrawable(ContextCompat.getDrawable(this, R.drawable.picture_original_checkbox)); + } + if (config.style.pictureOriginalFontColor != 0) { + mCbOriginal.setTextColor(config.style.pictureOriginalFontColor); + } else { + mCbOriginal.setTextColor(ContextCompat.getColor(this, R.color.picture_color_53575e)); + } + if (config.style.pictureOriginalTextSize != 0) { + mCbOriginal.setTextSize(config.style.pictureOriginalTextSize); + } + } else { + mCbOriginal.setButtonDrawable(ContextCompat.getDrawable(this, R.drawable.picture_original_checkbox)); + + mCbOriginal.setTextColor(ContextCompat.getColor(this, R.color.picture_color_53575e)); + } + } + + onSelectNumChange(false); + } + + /** + * 这里没实际意义,好处是预览图片时 滑动到屏幕一半以上可看到下一张图片是否选中了 + * + * @param previewEggs 是否显示预览友好体验 + * @param positionOffsetPixels 滑动偏移量 + */ + private void isPreviewEggs(boolean previewEggs, int position, int positionOffsetPixels) { + if (previewEggs) { + if (adapter.getSize() > 0) { + LocalMedia media; + int num; + if (positionOffsetPixels < screenWidth / 2) { + media = adapter.getItem(position); + if (media != null) { + check.setSelected(isSelected(media)); + if (config.isWeChatStyle) { + onUpdateSelectedChange(media); + } else { + if (config.checkNumMode) { + num = media.getNum(); + check.setText(ValueOf.toString(num)); + notifyCheckChanged(media); + onImageChecked(position); + } + } + } + } else { + media = adapter.getItem(position + 1); + if (media != null) { + check.setSelected(isSelected(media)); + if (config.isWeChatStyle) { + onUpdateSelectedChange(media); + } else { + if (config.checkNumMode) { + num = media.getNum(); + check.setText(ValueOf.toString(num)); + notifyCheckChanged(media); + onImageChecked(position + 1); + } + } + } + } + } + } + } + + /** + * 初始化ViewPage数据 + * + * @param list + */ + private void initViewPageAdapterData(List list) { + adapter = new PictureSimpleFragmentAdapter(config, this); + adapter.bindData(list); + viewPager.setAdapter(adapter); + viewPager.setCurrentItem(position); + setTitle(); + onImageChecked(position); + LocalMedia media = adapter.getItem(position); + if (media != null) { + index = media.getPosition(); + if (config.checkNumMode) { + tvMediaNum.setSelected(true); + check.setText(ValueOf.toString(media.getNum())); + notifyCheckChanged(media); + } + } + } + + /** + * 重置标题栏和分页码 + */ + private void setNewTitle() { + mPage = 0; + position = 0; + setTitle(); + } + + /** + * 设置标题 + */ + private void setTitle() { + if (config.isPageStrategy && !isBottomPreview) { + tvTitle.setText(getString(R.string.picture_preview_image_num, + position + 1, totalNumber)); + } else { + tvTitle.setText(getString(R.string.picture_preview_image_num, + position + 1, adapter.getSize())); + } + } + + /** + * 选择按钮更新 + */ + private void notifyCheckChanged(LocalMedia imageBean) { + if (config.checkNumMode) { + check.setText(""); + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (media.getPath().equals(imageBean.getPath()) + || media.getId() == imageBean.getId()) { + imageBean.setNum(media.getNum()); + check.setText(String.valueOf(imageBean.getNum())); + } + } + } + } + + /** + * 更新选择的顺序 + */ + private void subSelectPosition() { + for (int index = 0, len = selectData.size(); index < len; index++) { + LocalMedia media = selectData.get(index); + media.setNum(index + 1); + } + } + + /** + * 判断当前图片是否选中 + * + * @param position + */ + public void onImageChecked(int position) { + if (adapter.getSize() > 0) { + LocalMedia media = adapter.getItem(position); + if (media != null) { + check.setSelected(isSelected(media)); + } + } else { + check.setSelected(false); + } + } + + /** + * 当前图片是否选中 + * + * @param image + * @return + */ + protected boolean isSelected(LocalMedia image) { + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (media.getPath().equals(image.getPath()) || media.getId() == image.getId()) { + return true; + } + } + return false; + } + + /** + * 更新图片选择数量 + */ + + protected void onSelectNumChange(boolean isRefresh) { + this.refresh = isRefresh; + boolean enable = selectData.size() != 0; + if (enable) { + mTvPictureOk.setEnabled(true); + mTvPictureOk.setSelected(true); + if (config.style != null) { + if (config.style.pictureCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureCompleteTextColor); + } else { + mTvPictureOk.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_fa632d)); + } + } + if (numComplete) { + initCompleteText(selectData.size()); + } else { + if (refresh) { + tvMediaNum.startAnimation(animation); + } + tvMediaNum.setVisibility(View.VISIBLE); + tvMediaNum.setText(String.valueOf(selectData.size())); + if (config.style != null && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(config.style.pictureCompleteText); + } else { + mTvPictureOk.setText(getString(R.string.picture_completed)); + } + } + } else { + mTvPictureOk.setEnabled(false); + mTvPictureOk.setSelected(false); + if (config.style != null) { + if (config.style.pictureUnCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureUnCompleteTextColor); + } else { + mTvPictureOk.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_9b)); + } + } + if (numComplete) { + initCompleteText(0); + } else { + tvMediaNum.setVisibility(View.INVISIBLE); + if (config.style != null && !TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mTvPictureOk.setText(config.style.pictureUnCompleteText); + } else { + mTvPictureOk.setText(getString(R.string.picture_please_select)); + } + } + } + } + + @Override + public void onClick(View view) { + int id = view.getId(); + if (id == R.id.pictureLeftBack) { + onBackPressed(); + } else if (id == R.id.tv_ok || id == R.id.tvMediaNum) { + onComplete(); + } else if (id == R.id.btnCheck) { + onCheckedComplete(); + } + } + + protected void onCheckedComplete() { + if (adapter.getSize() > 0) { + LocalMedia image = adapter.getItem(viewPager.getCurrentItem()); + String mimeType = selectData.size() > 0 ? + selectData.get(0).getMimeType() : ""; + int currentSize = selectData.size(); + if (config.isWithVideoImage) { + // 混选模式 + int videoSize = 0; + for (int i = 0; i < currentSize; i++) { + LocalMedia media = selectData.get(i); + if (PictureMimeType.isHasVideo(media.getMimeType())) { + videoSize++; + } + } + if (image != null && PictureMimeType.isHasVideo(image.getMimeType())) { + if (config.maxVideoSelectNum <= 0) { + // 如果视频可选数量是0 + showPromptDialog(getString(R.string.picture_rule)); + return; + } + + if (selectData.size() >= config.maxSelectNum && !check.isSelected()) { + showPromptDialog(getString(R.string.picture_message_max_num, config.maxSelectNum)); + return; + } + + if (videoSize >= config.maxVideoSelectNum && !check.isSelected()) { + // 如果选择的是视频 + showPromptDialog(StringUtils.getMsg(getContext(), image.getMimeType(), config.maxVideoSelectNum)); + return; + } + + if (!check.isSelected() && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + // 视频小于最低指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!check.isSelected() && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + // 视频时长超过了指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } + if (image != null && PictureMimeType.isHasImage(image.getMimeType())) { + if (selectData.size() >= config.maxSelectNum && !check.isSelected()) { + showPromptDialog(getString(R.string.picture_message_max_num, config.maxSelectNum)); + return; + } + } + } else { + // 非混选模式 + if (!TextUtils.isEmpty(mimeType)) { + boolean mimeTypeSame = PictureMimeType.isMimeTypeSame(mimeType, image.getMimeType()); + if (!mimeTypeSame) { + showPromptDialog(getString(R.string.picture_rule)); + return; + } + } + if (PictureMimeType.isHasVideo(mimeType) && config.maxVideoSelectNum > 0) { + if (currentSize >= config.maxVideoSelectNum && !check.isSelected()) { + // 如果先选择的是视频 + showPromptDialog(StringUtils.getMsg(getContext(), mimeType, config.maxVideoSelectNum)); + return; + } + + if (!check.isSelected() && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + // 视频小于最低指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!check.isSelected() && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + // 视频时长超过了指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } else { + if (currentSize >= config.maxSelectNum && !check.isSelected()) { + showPromptDialog(StringUtils.getMsg(getContext(), mimeType, config.maxSelectNum)); + return; + } + if (PictureMimeType.isHasVideo(image.getMimeType())) { + if (!check.isSelected() && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + // 视频小于最低指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!check.isSelected() && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + // 视频时长超过了指定的长度 + showPromptDialog(getContext().getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } + } + } + // 刷新图片列表中图片状态 + boolean isChecked; + if (!check.isSelected()) { + isChecked = true; + check.setSelected(true); + check.startAnimation(animation); + } else { + isChecked = false; + check.setSelected(false); + } + isChangeSelectedData = true; + if (isChecked) { + VoiceUtils.getInstance().play(); + // 如果是单选,则清空已选中的并刷新列表(作单一选择) + if (config.selectionMode == PictureConfig.SINGLE) { + selectData.clear(); + } + + // 如果宽高为0,重新获取宽高 + if (image.getWidth() == 0 || image.getHeight() == 0) { + int width = 0, height = 0; + image.setOrientation(-1); + if (PictureMimeType.isContent(image.getPath())) { + if (PictureMimeType.isHasVideo(image.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUri(getContext(), Uri.parse(image.getPath())); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(image.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUri(getContext(), Uri.parse(image.getPath())); + width = size[0]; + height = size[1]; + } + } else { + if (PictureMimeType.isHasVideo(image.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUrl(image.getPath()); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(image.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUrl(image.getPath()); + width = size[0]; + height = size[1]; + } + } + image.setWidth(width); + image.setHeight(height); + } + + // 如果有旋转信息图片宽高则是相反 + MediaUtils.setOrientationAsynchronous(getContext(), image, config.isAndroidQChangeWH, config.isAndroidQChangeVideoWH, null); + selectData.add(image); + onSelectedChange(true, image); + image.setNum(selectData.size()); + if (config.checkNumMode) { + check.setText(String.valueOf(image.getNum())); + } + } else { + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (media.getPath().equals(image.getPath()) + || media.getId() == image.getId()) { + selectData.remove(media); + onSelectedChange(false, image); + subSelectPosition(); + notifyCheckChanged(media); + break; + } + } + } + onSelectNumChange(true); + } + } + + /** + * 选中或是移除 + * + * @param isAddRemove + * @param media + */ + protected void onSelectedChange(boolean isAddRemove, LocalMedia media) { + + } + + /** + * 更新选中或是移除状态 + * + * @param media + */ + protected void onUpdateSelectedChange(LocalMedia media) { + + } + + protected void onComplete() { + // 如果设置了图片最小选择数量,则判断是否满足条件 + int size = selectData.size(); + LocalMedia image = selectData.size() > 0 ? selectData.get(0) : null; + String mimeType = image != null ? image.getMimeType() : ""; + if (config.isWithVideoImage) { + // 混选模式 + int videoSize = 0; + int imageSize = 0; + int currentSize = selectData.size(); + for (int i = 0; i < currentSize; i++) { + LocalMedia media = selectData.get(i); + if (PictureMimeType.isHasVideo(media.getMimeType())) { + videoSize++; + } else { + imageSize++; + } + } + if (config.selectionMode == PictureConfig.MULTIPLE) { + if (config.minSelectNum > 0) { + if (imageSize < config.minSelectNum) { + showPromptDialog(getString(R.string.picture_min_img_num, config.minSelectNum)); + return; + } + } + if (config.minVideoSelectNum > 0) { + if (videoSize < config.minVideoSelectNum) { + showPromptDialog(getString(R.string.picture_min_video_num, config.minVideoSelectNum)); + return; + } + } + } + } else { + // 单选模式(同类型) + if (config.selectionMode == PictureConfig.MULTIPLE) { + if (PictureMimeType.isHasImage(mimeType) && config.minSelectNum > 0 && size < config.minSelectNum) { + String str = getString(R.string.picture_min_img_num, config.minSelectNum); + showPromptDialog(str); + return; + } + if (PictureMimeType.isHasVideo(mimeType) && config.minVideoSelectNum > 0 && size < config.minVideoSelectNum) { + String str = getString(R.string.picture_min_video_num, config.minVideoSelectNum); + showPromptDialog(str); + return; + } + } + } + isCompleteOrSelected = true; + isChangeSelectedData = true; + if (config.isCheckOriginalImage) { + onBackPressed(); + return; + } + if (config.chooseMode == PictureMimeType.ofAll() && config.isWithVideoImage) { + bothMimeTypeWith(mimeType, image); + } else { + separateMimeTypeWith(mimeType, image); + } + } + + /** + * 两者不同类型的处理方式 + * + * @param mimeType + * @param image + */ + private void bothMimeTypeWith(String mimeType, LocalMedia image) { + if (config.enableCrop) { + isCompleteOrSelected = false; + boolean isHasImage = PictureMimeType.isHasImage(mimeType); + if (config.selectionMode == PictureConfig.SINGLE && isHasImage) { + config.originalPath = image.getPath(); + startCrop(config.originalPath, image.getMimeType()); + } else { + // 是图片和选择压缩并且是多张,调用批量压缩 + ArrayList cuts = new ArrayList<>(); + int count = selectData.size(); + int imageNum = 0; + for (int i = 0; i < count; i++) { + LocalMedia media = selectData.get(i); + if (media == null + || TextUtils.isEmpty(media.getPath())) { + continue; + } + if (PictureMimeType.isHasImage(media.getMimeType())) { + imageNum++; + } + CutInfo cutInfo = new CutInfo(); + cutInfo.setId(media.getId()); + cutInfo.setPath(media.getPath()); + cutInfo.setImageWidth(media.getWidth()); + cutInfo.setImageHeight(media.getHeight()); + cutInfo.setMimeType(media.getMimeType()); + cutInfo.setAndroidQToPath(media.getAndroidQToPath()); + cutInfo.setId(media.getId()); + cutInfo.setDuration(media.getDuration()); + cutInfo.setRealPath(media.getRealPath()); + cuts.add(cutInfo); + } + if (imageNum <= 0) { + // 全是视频 + isCompleteOrSelected = true; + onBackPressed(); + } else { + // 图片和视频共存 + startCrop(cuts); + } + } + } else { + onBackPressed(); + } + } + + /** + * 同一类型的图片或视频处理逻辑 + * + * @param mimeType + * @param image + */ + private void separateMimeTypeWith(String mimeType, LocalMedia image) { + if (config.enableCrop && PictureMimeType.isHasImage(mimeType)) { + isCompleteOrSelected = false; + if (config.selectionMode == PictureConfig.SINGLE) { + config.originalPath = image.getPath(); + startCrop(config.originalPath, image.getMimeType()); + } else { + // 是图片和选择压缩并且是多张,调用批量压缩 + ArrayList cuts = new ArrayList<>(); + int count = selectData.size(); + for (int i = 0; i < count; i++) { + LocalMedia media = selectData.get(i); + if (media == null + || TextUtils.isEmpty(media.getPath())) { + continue; + } + CutInfo cutInfo = new CutInfo(); + cutInfo.setId(media.getId()); + cutInfo.setPath(media.getPath()); + cutInfo.setImageWidth(media.getWidth()); + cutInfo.setImageHeight(media.getHeight()); + cutInfo.setMimeType(media.getMimeType()); + cutInfo.setAndroidQToPath(media.getAndroidQToPath()); + cutInfo.setId(media.getId()); + cutInfo.setDuration(media.getDuration()); + cutInfo.setRealPath(media.getRealPath()); + cuts.add(cutInfo); + } + startCrop(cuts); + } + } else { + onBackPressed(); + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case UCrop.REQUEST_MULTI_CROP: + // 裁剪数据 + List list = UCrop.getMultipleOutput(data); + data.putParcelableArrayListExtra(UCrop.Options.EXTRA_OUTPUT_URI_LIST, + (ArrayList) list); + // 已选数量 + data.putParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST, + (ArrayList) selectData); + setResult(RESULT_OK, data); + finish(); + break; + case UCrop.REQUEST_CROP: + if (data != null) { + data.putParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST, + (ArrayList) selectData); + setResult(RESULT_OK, data); + } + finish(); + break; + } + } else if (resultCode == UCrop.RESULT_ERROR) { + Throwable throwable = (Throwable) data.getSerializableExtra(UCrop.EXTRA_ERROR); + ToastUtils.s(getContext(), throwable.getMessage()); + } + } + + + @Override + public void onBackPressed() { + updateResult(); + if (config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewExitAnimation != 0) { + finish(); + overridePendingTransition(0, config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewExitAnimation != 0 ? + config.windowAnimationStyle.activityPreviewExitAnimation : R.anim.picture_anim_exit); + } else { + closeActivity(); + } + } + + /** + * 更新选中数据 + */ + private void updateResult() { + Intent intent = new Intent(); + if (isChangeSelectedData) { + intent.putExtra(PictureConfig.EXTRA_COMPLETE_SELECTED, isCompleteOrSelected); + intent.putParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST, + (ArrayList) selectData); + } + // 把是否原图标识返回,主要用于开启了开发者选项不保留活动或内存不足时 原图选中状态没有全局同步问题 + if (config.isOriginalControl) { + intent.putExtra(PictureConfig.EXTRA_CHANGE_ORIGINAL, config.isCheckOriginalImage); + } + setResult(RESULT_CANCELED, intent); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(PictureConfig.EXTRA_COMPLETE_SELECTED, isCompleteOrSelected); + outState.putBoolean(PictureConfig.EXTRA_CHANGE_SELECTED_DATA, isChangeSelectedData); + PictureSelector.saveSelectorList(outState, selectData); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (!isOnSaveInstanceState) { + ImagesObservable.getInstance().clearPreviewMediaData(); + } + if (mHandler != null) { + mHandler.removeCallbacksAndMessages(null); + mHandler = null; + } + if (animation != null) { + animation.cancel(); + animation = null; + } + if (adapter != null) { + adapter.clear(); + } + } + + @Override + public void onActivityBackPressed() { + onBackPressed(); + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectionModel.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectionModel.java new file mode 100644 index 0000000..ff5ae91 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectionModel.java @@ -0,0 +1,1389 @@ +package com.luck.picture.lib; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; + +import androidx.annotation.ColorInt; +import androidx.annotation.FloatRange; +import androidx.annotation.IntRange; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; + +import com.luck.picture.lib.animators.AnimationType; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.config.UCropOptions; +import com.luck.picture.lib.engine.CacheResourcesEngine; +import com.luck.picture.lib.engine.ImageEngine; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnCustomCameraInterfaceListener; +import com.luck.picture.lib.listener.OnResultCallbackListener; +import com.luck.picture.lib.listener.OnVideoSelectedPlayCallback; +import com.luck.picture.lib.style.PictureCropParameterStyle; +import com.luck.picture.lib.style.PictureParameterStyle; +import com.luck.picture.lib.style.PictureWindowAnimationStyle; +import com.luck.picture.lib.tools.DoubleUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; + +import java.lang.ref.WeakReference; +import java.util.List; + +import static android.os.Build.VERSION_CODES.KITKAT; + +/** + * @author:luck + * @date:2017-5-24 21:30 + * @describe:PictureSelectionModel + */ + +public class PictureSelectionModel { + private PictureSelectionConfig selectionConfig; + private PictureSelector selector; + + public PictureSelectionModel(PictureSelector selector, int chooseMode) { + this.selector = selector; + selectionConfig = PictureSelectionConfig.getCleanInstance(); + selectionConfig.chooseMode = chooseMode; + } + + public PictureSelectionModel(PictureSelector selector, int chooseMode, boolean camera) { + this.selector = selector; + selectionConfig = PictureSelectionConfig.getCleanInstance(); + selectionConfig.camera = camera; + selectionConfig.chooseMode = chooseMode; + } + + /** + * @param themeStyleId PictureSelector Theme style + * @return PictureSelectionModel + * Use {@link R.style#picture_default_style#picture_Sina_style#picture_white_style#picture_QQ_style#picture_WeChat_style} + */ + public PictureSelectionModel theme(@StyleRes int themeStyleId) { + selectionConfig.themeStyleId = themeStyleId; + return this; + } + + /** + * @param locale Language + * @return PictureSelectionModel + */ + public PictureSelectionModel setLanguage(int language) { + selectionConfig.language = language; + return this; + } + + /** + * Change the desired orientation of this activity. If the activity + * is currently in the foreground or otherwise impacting the screen + * orientation, the screen will immediately be changed (possibly causing + * the activity to be restarted). Otherwise, this will be used the next + * time the activity is visible. + * + * @param requestedOrientation An orientation constant as used in + * {@link ActivityInfo#screenOrientation ActivityInfo.screenOrientation}. + */ + public PictureSelectionModel setRequestedOrientation(int requestedOrientation) { + selectionConfig.requestedOrientation = requestedOrientation; + return this; + } + + /** + * @param engine Image Load the engine + * @return Use {@link .imageEngine()}. + */ + @Deprecated + public PictureSelectionModel loadImageEngine(ImageEngine engine) { + if (PictureSelectionConfig.imageEngine != engine) { + PictureSelectionConfig.imageEngine = engine; + } + return this; + } + + /** + * @param engine Image Load the engine + * @return + */ + public PictureSelectionModel imageEngine(ImageEngine engine) { + if (PictureSelectionConfig.imageEngine != engine) { + PictureSelectionConfig.imageEngine = engine; + } + return this; + } + + /** + * Only for Android version Q + * + * @param cacheResourcesEngine Image Cache + * @return + */ + @Deprecated + public PictureSelectionModel loadCacheResourcesCallback(CacheResourcesEngine cacheResourcesEngine) { + if (SdkVersionUtils.checkedAndroid_Q()) { + if (PictureSelectionConfig.cacheResourcesEngine != cacheResourcesEngine) { + PictureSelectionConfig.cacheResourcesEngine = new WeakReference<>(cacheResourcesEngine).get(); + } + } + return this; + } + + /** + * @param selectionMode PictureSelector Selection model and PictureConfig.MULTIPLE or PictureConfig.SINGLE + * @return + */ + public PictureSelectionModel selectionMode(int selectionMode) { + selectionConfig.selectionMode = selectionMode; + return this; + } + + /** + * @param isWeChatStyle Select style with or without WeChat enabled + * @return + */ + public PictureSelectionModel isWeChatStyle(boolean isWeChatStyle) { + selectionConfig.isWeChatStyle = isWeChatStyle; + return this; + } + + /** + * @param isUseCustomCamera Whether to use a custom camera + * @return + */ + public PictureSelectionModel isUseCustomCamera(boolean isUseCustomCamera) { + selectionConfig.isUseCustomCamera = Build.VERSION.SDK_INT > KITKAT && isUseCustomCamera; + return this; + } + + /** + * @param callback Provide video playback control,Users are free to customize the video display interface + * @return + */ + public PictureSelectionModel bindCustomPlayVideoCallback(OnVideoSelectedPlayCallback callback) { + PictureSelectionConfig.customVideoPlayCallback = new WeakReference<>(callback).get(); + return this; + } + + /** + * # The developer provides an additional callback interface to the user where the user can perform some custom actions + * {link 如果是自定义相机则必须使用.startActivityForResult(this,PictureConfig.REQUEST_CAMERA);方式启动否则PictureSelector处理不了相机后的回调} + * + * @param listener + * @return Use ${bindCustomCameraInterfaceListener} + */ + @Deprecated + public PictureSelectionModel bindPictureSelectorInterfaceListener(OnCustomCameraInterfaceListener listener) { + PictureSelectionConfig.onCustomCameraInterfaceListener = new WeakReference<>(listener).get(); + return this; + } + + /** + * # The developer provides an additional callback interface to the user where the user can perform some custom actions + * {link 如果是自定义相机则必须使用.startActivityForResult(this,PictureConfig.REQUEST_CAMERA);方式启动否则PictureSelector处理不了相机后的回调} + * + * @param listener + * @return + */ + public PictureSelectionModel bindCustomCameraInterfaceListener(OnCustomCameraInterfaceListener listener) { + PictureSelectionConfig.onCustomCameraInterfaceListener = new WeakReference<>(listener).get(); + return this; + } + + /** + * @param buttonFeatures Set the record button function + * # 具体参考 CustomCameraView.BUTTON_STATE_BOTH、BUTTON_STATE_ONLY_CAPTURE、BUTTON_STATE_ONLY_RECORDER + * @return + */ + public PictureSelectionModel setButtonFeatures(int buttonFeatures) { + selectionConfig.buttonFeatures = buttonFeatures; + return this; + } + + /** + * @param enableCrop Do you want to start cutting ? + * @return Use {link .isEnableCrop()} + */ + @Deprecated + public PictureSelectionModel enableCrop(boolean enableCrop) { + selectionConfig.enableCrop = enableCrop; + return this; + } + + /** + * @param enableCrop Do you want to start cutting ? + * @return + */ + public PictureSelectionModel isEnableCrop(boolean enableCrop) { + selectionConfig.enableCrop = enableCrop; + return this; + } + + /** + * @param uCropOptions UCrop parameter configuration is provided + * @return + */ + public PictureSelectionModel basicUCropConfig(UCropOptions uCropOptions) { + selectionConfig.uCropOptions = uCropOptions; + return this; + } + + /** + * @param isMultipleSkipCrop Whether multiple images can be skipped when cropping + * @return + */ + public PictureSelectionModel isMultipleSkipCrop(boolean isMultipleSkipCrop) { + selectionConfig.isMultipleSkipCrop = isMultipleSkipCrop; + return this; + } + + + /** + * @param enablePreviewAudio Do you want to ic_play audio ? + * @return + */ + @Deprecated + public PictureSelectionModel enablePreviewAudio(boolean enablePreviewAudio) { + selectionConfig.enablePreviewAudio = enablePreviewAudio; + return this; + } + + /** + * @param enablePreviewAudio Do you want to ic_play audio ? + * @return + */ + @Deprecated + public PictureSelectionModel isEnablePreviewAudio(boolean enablePreviewAudio) { + selectionConfig.enablePreviewAudio = enablePreviewAudio; + return this; + } + + /** + * @param freeStyleCropEnabled Crop frame is move ? + * @return + */ + public PictureSelectionModel freeStyleCropEnabled(boolean freeStyleCropEnabled) { + selectionConfig.freeStyleCropEnabled = freeStyleCropEnabled; + return this; + } + + /** + * @param scaleEnabled Crop frame is zoom ? + * @return + */ + public PictureSelectionModel scaleEnabled(boolean scaleEnabled) { + selectionConfig.scaleEnabled = scaleEnabled; + return this; + } + + /** + * @param rotateEnabled Crop frame is rotate ? + * @return + */ + public PictureSelectionModel rotateEnabled(boolean rotateEnabled) { + selectionConfig.rotateEnabled = rotateEnabled; + return this; + } + + /** + * @param circleDimmedLayer Circular head cutting + * @return + */ + public PictureSelectionModel circleDimmedLayer(boolean circleDimmedLayer) { + selectionConfig.circleDimmedLayer = circleDimmedLayer; + return this; + } + + /** + * @param circleDimmedColor setCircleDimmedColor + * @return + */ + @Deprecated + public PictureSelectionModel setCircleDimmedColor(int circleDimmedColor) { + selectionConfig.circleDimmedColor = circleDimmedColor; + return this; + } + + /** + * @param dimmedColor + * @return + */ + public PictureSelectionModel setCropDimmedColor(int dimmedColor) { + selectionConfig.circleDimmedColor = dimmedColor; + return this; + } + + /** + * @param circleDimmedBorderColor setCircleDimmedBorderColor + * @return + */ + public PictureSelectionModel setCircleDimmedBorderColor(int circleDimmedBorderColor) { + selectionConfig.circleDimmedBorderColor = circleDimmedBorderColor; + return this; + } + + /** + * @param circleStrokeWidth setCircleStrokeWidth + * @return + */ + public PictureSelectionModel setCircleStrokeWidth(int circleStrokeWidth) { + selectionConfig.circleStrokeWidth = circleStrokeWidth; + return this; + } + + /** + * @param showCropFrame Whether to show crop frame + * @return + */ + public PictureSelectionModel showCropFrame(boolean showCropFrame) { + selectionConfig.showCropFrame = showCropFrame; + return this; + } + + /** + * @param showCropGrid Whether to show CropGrid + * @return + */ + public PictureSelectionModel showCropGrid(boolean showCropGrid) { + selectionConfig.showCropGrid = showCropGrid; + return this; + } + + /** + * @param hideBottomControls Whether is Clipping function bar + * 单选有效 + * @return + */ + public PictureSelectionModel hideBottomControls(boolean hideBottomControls) { + selectionConfig.hideBottomControls = hideBottomControls; + return this; + } + + /** + * @param aspect_ratio_x Crop Proportion x + * @param aspect_ratio_y Crop Proportion y + * @return + */ + public PictureSelectionModel withAspectRatio(int aspect_ratio_x, int aspect_ratio_y) { + selectionConfig.aspect_ratio_x = aspect_ratio_x; + selectionConfig.aspect_ratio_y = aspect_ratio_y; + return this; + } + + /** + * @param isWithVideoImage Whether the pictures and videos can be selected together + * @return + */ + public PictureSelectionModel isWithVideoImage(boolean isWithVideoImage) { + selectionConfig.isWithVideoImage = + selectionConfig.selectionMode != PictureConfig.SINGLE + && selectionConfig.chooseMode == PictureMimeType.ofAll() && isWithVideoImage; + return this; + } + + /** + * When the maximum number of choices is reached, does the list enable the mask effect + * + * @param isMaxSelectEnabledMask + * @return + */ + public PictureSelectionModel isMaxSelectEnabledMask(boolean isMaxSelectEnabledMask) { + selectionConfig.isMaxSelectEnabledMask = isMaxSelectEnabledMask; + return this; + } + + /** + * @param maxSelectNum PictureSelector max selection + * @return + */ + public PictureSelectionModel maxSelectNum(int maxSelectNum) { + selectionConfig.maxSelectNum = maxSelectNum; + return this; + } + + /** + * @param minSelectNum PictureSelector min selection + * @return + */ + public PictureSelectionModel minSelectNum(int minSelectNum) { + selectionConfig.minSelectNum = minSelectNum; + return this; + } + + /** + * @param maxVideoSelectNum PictureSelector video max selection + * @return + */ + public PictureSelectionModel maxVideoSelectNum(int maxVideoSelectNum) { + selectionConfig.maxVideoSelectNum = maxVideoSelectNum; + return this; + } + + /** + * @param minVideoSelectNum PictureSelector video min selection + * @return + */ + public PictureSelectionModel minVideoSelectNum(int minVideoSelectNum) { + selectionConfig.minVideoSelectNum = minVideoSelectNum; + return this; + } + + /** + * Turn off Android Q to solve the problem that the width and height are reversed + * + * @param isChangeWH + * @return + */ + public PictureSelectionModel closeAndroidQChangeWH(boolean isChangeWH) { + selectionConfig.isAndroidQChangeWH = isChangeWH; + return this; + } + + /** + * Turn off Android Q to solve the problem that the width and height are reversed + * + * @param isChangeVideoWH + * @return + */ + public PictureSelectionModel closeAndroidQChangeVideoWH(boolean isChangeVideoWH) { + selectionConfig.isAndroidQChangeVideoWH = isChangeVideoWH; + return this; + } + + /** + * By clicking the title bar consecutively, RecyclerView automatically rolls back to the top + * + * @param isAutomaticTitleRecyclerTop + * @return + */ + public PictureSelectionModel isAutomaticTitleRecyclerTop(boolean isAutomaticTitleRecyclerTop) { + selectionConfig.isAutomaticTitleRecyclerTop = isAutomaticTitleRecyclerTop; + return this; + } + + + /** + * @param Select whether to return directly + * @return + */ + public PictureSelectionModel isSingleDirectReturn(boolean isSingleDirectReturn) { + selectionConfig.isSingleDirectReturn = selectionConfig.selectionMode + == PictureConfig.SINGLE && isSingleDirectReturn; + selectionConfig.isOriginalControl = (selectionConfig.selectionMode != PictureConfig.SINGLE || !isSingleDirectReturn) && selectionConfig.isOriginalControl; + return this; + } + + /** + * Whether to turn on paging mode + * + * @param isPageStrategy + * @param pageSize Maximum number of pages {@link PageSize is preferably no less than 20} + * @return + */ + public PictureSelectionModel isPageStrategy(boolean isPageStrategy, int pageSize) { + selectionConfig.isPageStrategy = isPageStrategy; + selectionConfig.pageSize = pageSize < PictureConfig.MIN_PAGE_SIZE ? PictureConfig.MAX_PAGE_SIZE : pageSize; + return this; + } + + /** + * Whether to turn on paging mode + * + * @param isPageStrategy + * @param pageSize Maximum number of pages {@link PageSize is preferably no less than 20} + * @param isFilterInvalidFile Whether to filter invalid files {@link Some of the query performance is consumed,Especially on the Q version} + * @return + */ + public PictureSelectionModel isPageStrategy(boolean isPageStrategy, int pageSize, boolean isFilterInvalidFile) { + selectionConfig.isPageStrategy = isPageStrategy; + selectionConfig.pageSize = pageSize < PictureConfig.MIN_PAGE_SIZE ? PictureConfig.MAX_PAGE_SIZE : pageSize; + selectionConfig.isFilterInvalidFile = isFilterInvalidFile; + return this; + } + + /** + * Whether to turn on paging mode + * + * @param isPageStrategy + * @return + */ + public PictureSelectionModel isPageStrategy(boolean isPageStrategy) { + selectionConfig.isPageStrategy = isPageStrategy; + return this; + } + + /** + * Whether to turn on paging mode + * + * @param isPageStrategy + * @param isFilterInvalidFile Whether to filter invalid files {@link Some of the query performance is consumed,Especially on the Q version} + * @return + */ + public PictureSelectionModel isPageStrategy(boolean isPageStrategy, boolean isFilterInvalidFile) { + selectionConfig.isPageStrategy = isPageStrategy; + selectionConfig.isFilterInvalidFile = isFilterInvalidFile; + return this; + } + + /** + * @param videoQuality video quality and 0 or 1 + * @return + */ + public PictureSelectionModel videoQuality(int videoQuality) { + selectionConfig.videoQuality = videoQuality; + return this; + } + + /** + * # alternative api cameraFileName(xxx.PNG); + * + * @param suffixType PictureSelector media format + * @return + */ + public PictureSelectionModel imageFormat(String suffixType) { + selectionConfig.suffixType = suffixType; + return this; + } + + + /** + * @param cropWidth crop width + * @param cropHeight crop height + * @return this + * @deprecated Crop image output width and height + * {@link cropImageWideHigh()} + */ + @Deprecated + public PictureSelectionModel cropWH(int cropWidth, int cropHeight) { + selectionConfig.cropWidth = cropWidth; + selectionConfig.cropHeight = cropHeight; + return this; + } + + /** + * @param cropWidth crop width + * @param cropHeight crop height + * @return this + */ + public PictureSelectionModel cropImageWideHigh(int cropWidth, int cropHeight) { + selectionConfig.cropWidth = cropWidth; + selectionConfig.cropHeight = cropHeight; + return this; + } + + /** + * @param videoMaxSecond selection video max second + * @return + */ + public PictureSelectionModel videoMaxSecond(int videoMaxSecond) { + selectionConfig.videoMaxSecond = (videoMaxSecond * 1000); + return this; + } + + /** + * @param videoMinSecond selection video min second + * @return + */ + public PictureSelectionModel videoMinSecond(int videoMinSecond) { + selectionConfig.videoMinSecond = videoMinSecond * 1000; + return this; + } + + + /** + * @param recordVideoSecond video record second + * @return + */ + public PictureSelectionModel recordVideoSecond(int recordVideoSecond) { + selectionConfig.recordVideoSecond = recordVideoSecond; + return this; + } + + /** + * @param width glide width + * @param height glide height + * @return 2.2.9开始 Glide改为外部用户自己定义此方法没有意义了 + */ + @Deprecated + public PictureSelectionModel glideOverride(@IntRange(from = 100) int width, + @IntRange(from = 100) int height) { + selectionConfig.overrideWidth = width; + selectionConfig.overrideHeight = height; + return this; + } + + /** + * @param sizeMultiplier The multiplier to apply to the + * {@link com.bumptech.glide.request.target.Target}'s dimensions when + * loading the resource. + * @return 2.2.9开始Glide改为外部用户自己定义此方法没有意义了 + */ + @Deprecated + public PictureSelectionModel sizeMultiplier(@FloatRange(from = 0.1f) float sizeMultiplier) { + selectionConfig.sizeMultiplier = sizeMultiplier; + return this; + } + + /** + * @param imageSpanCount PictureSelector image span count + * @return + */ + public PictureSelectionModel imageSpanCount(int imageSpanCount) { + selectionConfig.imageSpanCount = imageSpanCount; + return this; + } + + /** + * @param Less than how many KB images are not compressed + * @return + */ + public PictureSelectionModel minimumCompressSize(int size) { + selectionConfig.minimumCompressSize = size; + return this; + } + + /** + * @param compressQuality crop compress quality default 90 + * @return 请使用 cutOutQuality();方法 + */ + @Deprecated + public PictureSelectionModel cropCompressQuality(int compressQuality) { + selectionConfig.cropCompressQuality = compressQuality; + return this; + } + + /** + * @param cutQuality crop compress quality default 90 + * @return + */ + public PictureSelectionModel cutOutQuality(int cutQuality) { + selectionConfig.cropCompressQuality = cutQuality; + return this; + } + + /** + * @param isCompress Whether to open compress + * @return Use {link .isCompress()} + */ + @Deprecated + public PictureSelectionModel compress(boolean isCompress) { + selectionConfig.isCompress = isCompress; + return this; + } + + /** + * @param isCompress Whether to open compress + * @return + */ + public PictureSelectionModel isCompress(boolean isCompress) { + selectionConfig.isCompress = isCompress; + return this; + } + + /** + * @param compressQuality Image compressed output quality + * @return + */ + public PictureSelectionModel compressQuality(int compressQuality) { + selectionConfig.compressQuality = compressQuality; + return this; + } + + /** + * @param returnEmpty No data can be returned + * @return + */ + public PictureSelectionModel isReturnEmpty(boolean returnEmpty) { + selectionConfig.returnEmpty = returnEmpty; + return this; + } + + /** + * @param synOrAsy Synchronous or asynchronous compression + * @return + */ + public PictureSelectionModel synOrAsy(boolean synOrAsy) { + selectionConfig.synOrAsy = synOrAsy; + return this; + } + + /** + * @param focusAlpha After compression, the transparent channel is retained + * @return + */ + public PictureSelectionModel compressFocusAlpha(boolean focusAlpha) { + selectionConfig.focusAlpha = focusAlpha; + return this; + } + + /** + * After recording with the system camera, does it support playing the video immediately using the system player + * + * @param isQuickCapture + * @return + */ + public PictureSelectionModel isQuickCapture(boolean isQuickCapture) { + selectionConfig.isQuickCapture = isQuickCapture; + return this; + } + + /** + * @param isOriginalControl Whether the original image is displayed + * @return + */ + public PictureSelectionModel isOriginalImageControl(boolean isOriginalControl) { + selectionConfig.isOriginalControl = !selectionConfig.camera + && selectionConfig.chooseMode != PictureMimeType.ofVideo() + && selectionConfig.chooseMode != PictureMimeType.ofAudio() && isOriginalControl; + return this; + } + + /** + * @param path save path + * @return + */ + public PictureSelectionModel compressSavePath(String path) { + selectionConfig.compressSavePath = path; + return this; + } + + /** + * Camera custom local file name + * # Such as xxx.png + * + * @param fileName + * @return + */ + public PictureSelectionModel cameraFileName(String fileName) { + selectionConfig.cameraFileName = fileName; + return this; + } + + /** + * crop custom local file name + * # Such as xxx.png + * + * @param renameCropFileName + * @return + */ + public PictureSelectionModel renameCropFileName(String renameCropFileName) { + selectionConfig.renameCropFileName = renameCropFileName; + return this; + } + + /** + * custom compress local file name + * # Such as xxx.png + * + * @param renameFile + * @return + */ + public PictureSelectionModel renameCompressFile(String renameFile) { + selectionConfig.renameCompressFileName = renameFile; + return this; + } + + /** + * @param zoomAnim Picture list zoom anim + * @return + */ + public PictureSelectionModel isZoomAnim(boolean zoomAnim) { + selectionConfig.zoomAnim = zoomAnim; + return this; + } + + /** + * @param previewEggs preview eggs It doesn't make much sense + * @return Use {link .isPreviewEggs()} + */ + @Deprecated + public PictureSelectionModel previewEggs(boolean previewEggs) { + selectionConfig.previewEggs = previewEggs; + return this; + } + + /** + * @param previewEggs preview eggs It doesn't make much sense + * @return + */ + public PictureSelectionModel isPreviewEggs(boolean previewEggs) { + selectionConfig.previewEggs = previewEggs; + return this; + } + + /** + * @param isCamera Whether to open camera button + * @return + */ + public PictureSelectionModel isCamera(boolean isCamera) { + selectionConfig.isCamera = isCamera; + return this; + } + + /** + * Extra used with {@link #Environment.getExternalStorageDirectory() + File.separator + "CustomCamera" + File.separator} to indicate that + * + * @param outPutCameraPath Camera save path 只支持Build.VERSION.SDK_INT < Build.VERSION_CODES.Q + * @return + */ + public PictureSelectionModel setOutputCameraPath(String outPutCameraPath) { + selectionConfig.outPutCameraPath = outPutCameraPath; + return this; + } + + + /** + * # file size The unit is M + * + * @param fileSize Filter file size + * @return + */ + public PictureSelectionModel queryMaxFileSize(float fileSize) { + selectionConfig.filterFileSize = fileSize; + return this; + } + + /** + * @param isGif Whether to open gif + * @return + */ + public PictureSelectionModel isGif(boolean isGif) { + selectionConfig.isGif = isGif; + return this; + } + + /** + * @param enablePreview Do you want to preview the picture? + * @return Use {link .isPreviewImage()} + */ + @Deprecated + public PictureSelectionModel previewImage(boolean enablePreview) { + selectionConfig.enablePreview = enablePreview; + return this; + } + + /** + * @param enablePreview Do you want to preview the picture? + * @return + */ + public PictureSelectionModel isPreviewImage(boolean enablePreview) { + selectionConfig.enablePreview = enablePreview; + return this; + } + + /** + * @param enPreviewVideo Do you want to preview the video? + * @return Use {link .isPreviewVideo()} + */ + @Deprecated + public PictureSelectionModel previewVideo(boolean enPreviewVideo) { + selectionConfig.enPreviewVideo = enPreviewVideo; + return this; + } + + /** + * @param enPreviewVideo Do you want to preview the video? + * @return + */ + public PictureSelectionModel isPreviewVideo(boolean enPreviewVideo) { + selectionConfig.enPreviewVideo = enPreviewVideo; + return this; + } + + /** + * @param isNotPreviewDownload Previews do not show downloads + * @return + */ + public PictureSelectionModel isNotPreviewDownload(boolean isNotPreviewDownload) { + selectionConfig.isNotPreviewDownload = isNotPreviewDownload; + return this; + } + + /** + * @param Specify get image format + * @return + */ + public PictureSelectionModel querySpecifiedFormatSuffix(String specifiedFormat) { + selectionConfig.specifiedFormat = specifiedFormat; + return this; + } + + /** + * @param openClickSound Whether to open click voice + * @return Use {link .isOpenClickSound()} + */ + @Deprecated + public PictureSelectionModel openClickSound(boolean openClickSound) { + selectionConfig.openClickSound = !selectionConfig.camera && openClickSound; + return this; + } + + /** + * @param isOpenClickSound Whether to open click voice + * @return + */ + public PictureSelectionModel isOpenClickSound(boolean openClickSound) { + selectionConfig.openClickSound = !selectionConfig.camera && openClickSound; + return this; + } + + /** + * 是否可拖动裁剪框(setFreeStyleCropEnabled 为true 有效) + */ + public PictureSelectionModel isDragFrame(boolean isDragFrame) { + selectionConfig.isDragFrame = isDragFrame; + return this; + } + + /** + * Whether the multi-graph clipping list is animated or not + * + * @param isAnimation + * @return + */ + public PictureSelectionModel isMultipleRecyclerAnimation(boolean isAnimation) { + selectionConfig.isMultipleRecyclerAnimation = isAnimation; + return this; + } + + + /** + * 设置摄像头方向(前后 默认后置) + */ + public PictureSelectionModel isCameraAroundState(boolean isCameraAroundState) { + selectionConfig.isCameraAroundState = isCameraAroundState; + return this; + } + + /** + * @param selectionMedia Select the selected picture set + * @return Use {link .selectionData()} + */ + @Deprecated + public PictureSelectionModel selectionMedia(List selectionMedia) { + if (selectionConfig.selectionMode == PictureConfig.SINGLE && selectionConfig.isSingleDirectReturn) { + selectionConfig.selectionMedias = null; + } else { + selectionConfig.selectionMedias = selectionMedia; + } + return this; + } + + /** + * @param selectionData Select the selected picture set + * @return + */ + public PictureSelectionModel selectionData(List selectionData) { + if (selectionConfig.selectionMode == PictureConfig.SINGLE && selectionConfig.isSingleDirectReturn) { + selectionConfig.selectionMedias = null; + } else { + selectionConfig.selectionMedias = selectionData; + } + return this; + } + + /** + * 是否改变状态栏字段颜色(黑白字体转换) + * #适合所有style使用 + * + * @param isChangeStatusBarFontColor + * @return + */ + @Deprecated + public PictureSelectionModel isChangeStatusBarFontColor(boolean isChangeStatusBarFontColor) { + selectionConfig.isChangeStatusBarFontColor = isChangeStatusBarFontColor; + return this; + } + + /** + * 选择图片样式0/9 + * #适合所有style使用 + * + * @param isOpenStyleNumComplete + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel isOpenStyleNumComplete(boolean isOpenStyleNumComplete) { + selectionConfig.isOpenStyleNumComplete = isOpenStyleNumComplete; + return this; + } + + /** + * 是否开启数字选择模式 + * #适合qq style 样式使用 + * + * @param isOpenStyleCheckNumMode + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel isOpenStyleCheckNumMode(boolean isOpenStyleCheckNumMode) { + selectionConfig.isOpenStyleCheckNumMode = isOpenStyleCheckNumMode; + return this; + } + + /** + * 设置标题栏背景色 + * + * @param color + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel setTitleBarBackgroundColor(@ColorInt int color) { + selectionConfig.titleBarBackgroundColor = color; + return this; + } + + + /** + * 状态栏背景色 + * + * @param color + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel setStatusBarColorPrimaryDark(@ColorInt int color) { + selectionConfig.pictureStatusBarColor = color; + return this; + } + + + /** + * 裁剪页面标题背景色 + * + * @param color + * @return 使用setPictureCropStyle方法 + */ + @Deprecated + public PictureSelectionModel setCropTitleBarBackgroundColor(@ColorInt int color) { + selectionConfig.cropTitleBarBackgroundColor = color; + return this; + } + + /** + * 裁剪页面状态栏背景色 + * + * @param color + * @return 使用setPictureCropStyle方法 + */ + @Deprecated + public PictureSelectionModel setCropStatusBarColorPrimaryDark(@ColorInt int color) { + selectionConfig.cropStatusBarColorPrimaryDark = color; + return this; + } + + /** + * 裁剪页面标题文字颜色 + * + * @param color + * @return 使用setPictureCropStyle方法 + */ + @Deprecated + public PictureSelectionModel setCropTitleColor(@ColorInt int color) { + selectionConfig.cropTitleColor = color; + return this; + } + + /** + * 设置相册标题右侧向上箭头图标 + * + * @param resId + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel setUpArrowDrawable(int resId) { + selectionConfig.upResId = resId; + return this; + } + + /** + * 设置相册标题右侧向下箭头图标 + * + * @param resId + * @return 使用setPictureStyle方法 + */ + @Deprecated + public PictureSelectionModel setDownArrowDrawable(int resId) { + selectionConfig.downResId = resId; + return this; + } + + /** + * 动态设置裁剪主题样式 + * + * @param style 裁剪页主题 + * @return + */ + public PictureSelectionModel setPictureCropStyle(PictureCropParameterStyle style) { + selectionConfig.cropStyle = style; + return this; + } + + /** + * 动态设置相册主题样式 + * + * @param style 主题 + * @return + */ + public PictureSelectionModel setPictureStyle(PictureParameterStyle style) { + selectionConfig.style = style; + return this; + } + + /** + * Dynamically set the album to start and exit the animation + * + * @param style Activity Launch exit animation theme + * @return + */ + public PictureSelectionModel setPictureWindowAnimationStyle(PictureWindowAnimationStyle windowAnimationStyle) { + selectionConfig.windowAnimationStyle = windowAnimationStyle; + return this; + } + + /** + * Photo album list animation {} + * Use {@link AnimationType#ALPHA_IN_ANIMATION or SLIDE_IN_BOTTOM_ANIMATION} directly. + * + * @param animationMode + * @return + */ + public PictureSelectionModel setRecyclerAnimationMode(int animationMode) { + selectionConfig.animationMode = animationMode; + return this; + } + + /** + * # If you want to handle the Android Q path, if not, just return the uri, + * The getAndroidQToPath(); field will be empty + * + * @param isAndroidQTransform + * @return + */ + public PictureSelectionModel isAndroidQTransform(boolean isAndroidQTransform) { + selectionConfig.isAndroidQTransform = isAndroidQTransform; + return this; + } + + /** + * # 内部方法-要使用此方法时最好先咨询作者!!! + * + * @param isFallbackVersion 仅供特殊情况内部使用 如果某功能出错此开关可以回退至之前版本 + * @return + */ + public PictureSelectionModel isFallbackVersion(boolean isFallbackVersion) { + selectionConfig.isFallbackVersion = isFallbackVersion; + return this; + } + + /** + * # 内部方法-要使用此方法时最好先咨询作者!!! + * + * @param isFallbackVersion 仅供特殊情况内部使用 如果某功能出错此开关可以回退至之前版本 + * @return + */ + public PictureSelectionModel isFallbackVersion2(boolean isFallbackVersion) { + selectionConfig.isFallbackVersion2 = isFallbackVersion; + return this; + } + + /** + * # 内部方法-要使用此方法时最好先咨询作者!!! + * + * @param isFallbackVersion 仅供特殊情况内部使用 如果某功能出错此开关可以回退至之前版本 + * @return + */ + public PictureSelectionModel isFallbackVersion3(boolean isFallbackVersion) { + selectionConfig.isFallbackVersion3 = isFallbackVersion; + return this; + } + + /** + * Start to select media and wait for result. + * + * @param requestCode Identity of the request Activity or Fragment. + */ + public void forResult(int requestCode) { + if (!DoubleUtils.isFastDoubleClick()) { + Activity activity = selector.getActivity(); + if (activity == null || selectionConfig == null) { + return; + } + Intent intent; + if (selectionConfig.camera && selectionConfig.isUseCustomCamera) { + intent = new Intent(activity, PictureCustomCameraActivity.class); + } else { + intent = new Intent(activity, selectionConfig.camera + ? PictureSelectorCameraEmptyActivity.class : + selectionConfig.isWeChatStyle ? PictureSelectorWeChatStyleActivity.class + : PictureSelectorActivity.class); + } + selectionConfig.isCallbackMode = false; + Fragment fragment = selector.getFragment(); + if (fragment != null) { + fragment.startActivityForResult(intent, requestCode); + } else { + activity.startActivityForResult(intent, requestCode); + } + PictureWindowAnimationStyle windowAnimationStyle = selectionConfig.windowAnimationStyle; + activity.overridePendingTransition(windowAnimationStyle != null && + windowAnimationStyle.activityEnterAnimation != 0 ? + windowAnimationStyle.activityEnterAnimation : + R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } + } + + /** + * # replace for setPictureWindowAnimationStyle(); + * Start to select media and wait for result. + *

+ * # Use PictureWindowAnimationStyle to achieve animation effects + * + * @param requestCode Identity of the request Activity or Fragment. + */ + @Deprecated + public void forResult(int requestCode, int enterAnim, int exitAnim) { + if (!DoubleUtils.isFastDoubleClick()) { + Activity activity = selector.getActivity(); + if (activity == null) { + return; + } + Intent intent = new Intent(activity, selectionConfig != null && selectionConfig.camera + ? PictureSelectorCameraEmptyActivity.class : + selectionConfig.isWeChatStyle ? PictureSelectorWeChatStyleActivity.class : + PictureSelectorActivity.class); + selectionConfig.isCallbackMode = false; + Fragment fragment = selector.getFragment(); + if (fragment != null) { + fragment.startActivityForResult(intent, requestCode); + } else { + activity.startActivityForResult(intent, requestCode); + } + activity.overridePendingTransition(enterAnim, exitAnim); + } + } + + + /** + * Start to select media and wait for result. + * + * @param listener The resulting callback listens + */ + public void forResult(OnResultCallbackListener listener) { + if (!DoubleUtils.isFastDoubleClick()) { + Activity activity = selector.getActivity(); + if (activity == null || selectionConfig == null) { + return; + } + // 绑定回调监听 + PictureSelectionConfig.listener = new WeakReference<>(listener).get(); + selectionConfig.isCallbackMode = true; + Intent intent; + if (selectionConfig.camera && selectionConfig.isUseCustomCamera) { + intent = new Intent(activity, PictureCustomCameraActivity.class); + } else { + intent = new Intent(activity, selectionConfig.camera + ? PictureSelectorCameraEmptyActivity.class : + selectionConfig.isWeChatStyle ? PictureSelectorWeChatStyleActivity.class + : PictureSelectorActivity.class); + } + Fragment fragment = selector.getFragment(); + if (fragment != null) { + fragment.startActivity(intent); + } else { + activity.startActivity(intent); + } + PictureWindowAnimationStyle windowAnimationStyle = selectionConfig.windowAnimationStyle; + activity.overridePendingTransition(windowAnimationStyle != null && + windowAnimationStyle.activityEnterAnimation != 0 ? + windowAnimationStyle.activityEnterAnimation : + R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } + } + + /** + * Start to select media and wait for result. + * + * @param requestCode Identity of the request Activity or Fragment. + * @param listener The resulting callback listens + */ + public void forResult(int requestCode, OnResultCallbackListener listener) { + if (!DoubleUtils.isFastDoubleClick()) { + Activity activity = selector.getActivity(); + if (activity == null || selectionConfig == null) { + return; + } + // 绑定回调监听 + PictureSelectionConfig.listener = new WeakReference<>(listener).get(); + selectionConfig.isCallbackMode = true; + Intent intent; + if (selectionConfig.camera && selectionConfig.isUseCustomCamera) { + intent = new Intent(activity, PictureCustomCameraActivity.class); + } else { + intent = new Intent(activity, selectionConfig.camera + ? PictureSelectorCameraEmptyActivity.class : + selectionConfig.isWeChatStyle ? PictureSelectorWeChatStyleActivity.class + : PictureSelectorActivity.class); + } + Fragment fragment = selector.getFragment(); + if (fragment != null) { + fragment.startActivityForResult(intent, requestCode); + } else { + activity.startActivityForResult(intent, requestCode); + } + PictureWindowAnimationStyle windowAnimationStyle = selectionConfig.windowAnimationStyle; + activity.overridePendingTransition(windowAnimationStyle != null && + windowAnimationStyle.activityEnterAnimation != 0 ? + windowAnimationStyle.activityEnterAnimation : + R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } + } + + /** + * 提供外部预览图片方法 + * + * @param position + * @param medias + */ + public void openExternalPreview(int position, List medias) { + if (selector != null) { + selector.externalPicturePreview(position, medias, + selectionConfig.windowAnimationStyle != null && + selectionConfig.windowAnimationStyle.activityPreviewEnterAnimation != 0 + ? selectionConfig.windowAnimationStyle.activityPreviewEnterAnimation : 0); + } else { + throw new NullPointerException("This PictureSelector is Null"); + } + } + + + /** + * 提供外部预览图片方法-带自定义下载保存路径 + * # 废弃 由于Android Q沙盒机制 此方法不在需要了 + * + * @param position + * @param medias + */ + @Deprecated + public void openExternalPreview(int position, String directory_path, List medias) { + if (selector != null) { + selector.externalPicturePreview(position, directory_path, medias, + selectionConfig.windowAnimationStyle != null && + selectionConfig.windowAnimationStyle.activityPreviewEnterAnimation != 0 + ? selectionConfig.windowAnimationStyle.activityPreviewEnterAnimation : 0); + } else { + throw new NullPointerException("This PictureSelector is Null"); + } + } + + /** + * set preview video + * + * @param path + */ + public void externalPictureVideo(String path) { + if (selector != null) { + selector.externalPictureVideo(path); + } else { + throw new NullPointerException("This PictureSelector is Null"); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelector.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelector.java new file mode 100644 index 0000000..81b8a6b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelector.java @@ -0,0 +1,244 @@ +package com.luck.picture.lib; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Parcelable; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.style.PictureParameterStyle; +import com.luck.picture.lib.tools.DoubleUtils; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2017-5-24 22:30 + * @describe:PictureSelector + */ + +public final class PictureSelector { + + private final WeakReference mActivity; + private final WeakReference mFragment; + + private PictureSelector(Activity activity) { + this(activity, null); + } + + private PictureSelector(Fragment fragment) { + this(fragment.getActivity(), fragment); + } + + private PictureSelector(Activity activity, Fragment fragment) { + mActivity = new WeakReference<>(activity); + mFragment = new WeakReference<>(fragment); + } + + /** + * Start PictureSelector for Activity. + * + * @param activity + * @return PictureSelector instance. + */ + public static PictureSelector create(Activity activity) { + return new PictureSelector(activity); + } + + /** + * Start PictureSelector for Fragment. + * + * @param fragment + * @return PictureSelector instance. + */ + public static PictureSelector create(Fragment fragment) { + return new PictureSelector(fragment); + } + + /** + * @param chooseMode Select the type of picture you want,all or Picture or Video . + * @return LocalMedia PictureSelectionModel + * Use {@link PictureMimeType.ofAll(),ofImage(),ofVideo(),ofAudio()}. + */ + public PictureSelectionModel openGallery(int chooseMode) { + return new PictureSelectionModel(this, chooseMode); + } + + /** + * @param chooseMode Select the type of picture you want,Picture or Video. + * @return LocalMedia PictureSelectionModel + * Use {@link PictureMimeType.ofImage(),ofVideo()}. + */ + public PictureSelectionModel openCamera(int chooseMode) { + return new PictureSelectionModel(this, chooseMode, true); + } + + /** + * 外部预览时设置样式 + * + * @param themeStyle + * @return + */ + public PictureSelectionModel themeStyle(int themeStyle) { + return new PictureSelectionModel(this, PictureMimeType.ofImage()) + .theme(themeStyle); + } + + /** + * 外部预览时动态代码设置样式 + * + * @param style + * @return + */ + public PictureSelectionModel setPictureStyle(PictureParameterStyle style) { + return new PictureSelectionModel(this, PictureMimeType.ofImage()) + .setPictureStyle(style); + } + + /** + * @param data + * @return Selector Multiple LocalMedia + */ + public static List obtainMultipleResult(Intent data) { + if (data != null) { + List result = data.getParcelableArrayListExtra(PictureConfig.EXTRA_RESULT_SELECTION); + return result == null ? new ArrayList<>() : result; + } + return new ArrayList<>(); + } + + /** + * @param data + * @return Put image Intent Data + */ + public static Intent putIntentResult(List data) { + return new Intent().putParcelableArrayListExtra(PictureConfig.EXTRA_RESULT_SELECTION, + (ArrayList) data); + } + + /** + * @param bundle + * @return get Selector LocalMedia + */ + public static List obtainSelectorList(Bundle bundle) { + if (bundle != null) { + List selectionMedias = bundle.getParcelableArrayList(PictureConfig.EXTRA_SELECT_LIST); + return selectionMedias == null ? new ArrayList<>() : selectionMedias; + } + return new ArrayList<>(); + } + + /** + * @param selectedImages + * @return put Selector LocalMedia + */ + public static void saveSelectorList(Bundle outState, List selectedImages) { + outState.putParcelableArrayList(PictureConfig.EXTRA_SELECT_LIST, + (ArrayList) selectedImages); + } + + /** + * set preview image + * + * @param position + * @param medias + */ + public void externalPicturePreview(int position, List medias, int enterAnimation) { + if (!DoubleUtils.isFastDoubleClick()) { + if (getActivity() != null) { + Intent intent = new Intent(getActivity(), PictureExternalPreviewActivity.class); + intent.putParcelableArrayListExtra(PictureConfig.EXTRA_PREVIEW_SELECT_LIST, + (ArrayList) medias); + intent.putExtra(PictureConfig.EXTRA_POSITION, position); + getActivity().startActivity(intent); + getActivity().overridePendingTransition(enterAnimation != 0 + ? enterAnimation : R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } else { + throw new NullPointerException("Starting the PictureSelector Activity cannot be empty "); + } + } + } + + /** + * set preview image + * + * @param position + * @param medias + * @param directory_path + */ + public void externalPicturePreview(int position, String directory_path, List medias, int enterAnimation) { + if (!DoubleUtils.isFastDoubleClick()) { + if (getActivity() != null) { + Intent intent = new Intent(getActivity(), PictureExternalPreviewActivity.class); + intent.putParcelableArrayListExtra(PictureConfig.EXTRA_PREVIEW_SELECT_LIST, (ArrayList) medias); + intent.putExtra(PictureConfig.EXTRA_POSITION, position); + intent.putExtra(PictureConfig.EXTRA_DIRECTORY_PATH, directory_path); + getActivity().startActivity(intent); + getActivity().overridePendingTransition(enterAnimation != 0 + ? enterAnimation : R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } else { + throw new NullPointerException("Starting the PictureSelector Activity cannot be empty "); + } + } + } + + /** + * set preview video + * + * @param path + */ + public void externalPictureVideo(String path) { + if (!DoubleUtils.isFastDoubleClick()) { + if (getActivity() != null) { + Intent intent = new Intent(getActivity(), PictureVideoPlayActivity.class); + intent.putExtra(PictureConfig.EXTRA_VIDEO_PATH, path); + intent.putExtra(PictureConfig.EXTRA_PREVIEW_VIDEO, true); + getActivity().startActivity(intent); + } else { + throw new NullPointerException("Starting the PictureSelector Activity cannot be empty "); + } + } + } + + /** + * set preview audio + * + * @param path + */ + public void externalPictureAudio(String path) { + if (!DoubleUtils.isFastDoubleClick()) { + if (getActivity() != null) { + Intent intent = new Intent(getActivity(), PicturePlayAudioActivity.class); + intent.putExtra(PictureConfig.EXTRA_AUDIO_PATH, path); + getActivity().startActivity(intent); + getActivity().overridePendingTransition(R.anim.picture_anim_enter, 0); + } else { + throw new NullPointerException("Starting the PictureSelector Activity cannot be empty "); + } + } + } + + /** + * @return Activity. + */ + @Nullable + Activity getActivity() { + return mActivity.get(); + } + + /** + * @return Fragment. + */ + @Nullable + Fragment getFragment() { + return mFragment != null ? mFragment.get() : null; + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorActivity.java new file mode 100644 index 0000000..5d64b94 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorActivity.java @@ -0,0 +1,2253 @@ +package com.luck.picture.lib; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.drawable.Drawable; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.os.Handler; +import android.os.Parcelable; +import android.os.SystemClock; +import android.text.TextUtils; +import android.view.View; +import android.view.animation.Animation; +import android.view.animation.AnimationUtils; +import android.widget.Button; +import android.widget.CheckBox; +import android.widget.ImageView; +import android.widget.RelativeLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.recyclerview.widget.SimpleItemAnimator; + +import com.luck.picture.lib.adapter.PictureImageGridAdapter; +import com.luck.picture.lib.animators.AlphaInAnimationAdapter; +import com.luck.picture.lib.animators.AnimationType; +import com.luck.picture.lib.animators.SlideInBottomAnimationAdapter; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.decoration.GridSpacingItemDecoration; +import com.luck.picture.lib.dialog.PhotoItemSelectedDialog; +import com.luck.picture.lib.dialog.PictureCustomDialog; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.listener.OnAlbumItemClickListener; +import com.luck.picture.lib.listener.OnItemClickListener; +import com.luck.picture.lib.listener.OnPhotoSelectChangedListener; +import com.luck.picture.lib.listener.OnQueryDataResultListener; +import com.luck.picture.lib.listener.OnRecyclerViewPreloadMoreListener; +import com.luck.picture.lib.model.LocalMediaLoader; +import com.luck.picture.lib.model.LocalMediaPageLoader; +import com.luck.picture.lib.observable.ImagesObservable; +import com.luck.picture.lib.permissions.PermissionChecker; +import com.luck.picture.lib.style.PictureWindowAnimationStyle; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.AttrsUtils; +import com.luck.picture.lib.tools.BitmapUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.DoubleUtils; +import com.luck.picture.lib.tools.JumpUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.ScreenUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.StringUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.ValueOf; +import com.luck.picture.lib.widget.FolderPopWindow; +import com.luck.picture.lib.widget.RecyclerPreloadView; +import com.yalantis.ucrop.UCrop; +import com.yalantis.ucrop.model.CutInfo; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @data:2018/1/27 19:12 + * @describe: PictureSelectorActivity + */ +public class PictureSelectorActivity extends PictureBaseActivity implements View.OnClickListener, + OnAlbumItemClickListener, OnPhotoSelectChangedListener, OnItemClickListener, + OnRecyclerViewPreloadMoreListener { + private static final String TAG = PictureSelectorActivity.class.getSimpleName(); + protected ImageView mIvPictureLeftBack; + protected ImageView mIvArrow; + protected View titleViewBg; + protected TextView mTvPictureTitle, mTvPictureRight, mTvPictureOk, mTvEmpty, + mTvPictureImgNum, mTvPicturePreview, mTvPlayPause, mTvStop, mTvQuit, + mTvMusicStatus, mTvMusicTotal, mTvMusicTime; + protected RecyclerPreloadView mRecyclerView; + protected RelativeLayout mBottomLayout; + protected PictureImageGridAdapter mAdapter; + protected FolderPopWindow folderWindow; + protected Animation animation = null; + protected boolean isStartAnimation = false; + protected MediaPlayer mediaPlayer; + protected SeekBar musicSeekBar; + protected boolean isPlayAudio = false; + protected PictureCustomDialog audioDialog; + protected CheckBox mCbOriginal; + protected int oldCurrentListSize; + protected boolean isEnterSetting; + private long intervalClickTime = 0; + private int allFolderSize; + private int mOpenCameraCount; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (savedInstanceState != null) { + allFolderSize = savedInstanceState.getInt(PictureConfig.EXTRA_ALL_FOLDER_SIZE); + oldCurrentListSize = savedInstanceState.getInt(PictureConfig.EXTRA_OLD_CURRENT_LIST_SIZE, 0); + selectionMedias = PictureSelector.obtainSelectorList(savedInstanceState); + if (mAdapter != null) { + isStartAnimation = true; + mAdapter.bindSelectData(selectionMedias); + } + } + } + + @Override + protected void onResume() { + super.onResume(); + if (isEnterSetting) { + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + if (mAdapter.isDataEmpty()) { + readLocalMedia(); + } + } else { + showPermissionsDialog(false, getString(R.string.picture_jurisdiction)); + } + isEnterSetting = false; + } + + if (config.isOriginalControl) { + if (mCbOriginal != null) { + mCbOriginal.setChecked(config.isCheckOriginalImage); + } + } + } + + @Override + public int getResourceId() { + return R.layout.picture_selector; + } + + @Override + protected void initWidgets() { + super.initWidgets(); + container = findViewById(R.id.container); + titleViewBg = findViewById(R.id.titleViewBg); + mIvPictureLeftBack = findViewById(R.id.pictureLeftBack); + mTvPictureTitle = findViewById(R.id.picture_title); + mTvPictureRight = findViewById(R.id.picture_right); + mTvPictureOk = findViewById(R.id.picture_tv_ok); + mCbOriginal = findViewById(R.id.cb_original); + mIvArrow = findViewById(R.id.ivArrow); + mTvPicturePreview = findViewById(R.id.picture_id_preview); + mTvPictureImgNum = findViewById(R.id.picture_tvMediaNum); + mRecyclerView = findViewById(R.id.picture_recycler); + mBottomLayout = findViewById(R.id.rl_bottom); + mTvEmpty = findViewById(R.id.tv_empty); + isNumComplete(numComplete); + if (!numComplete) { + animation = AnimationUtils.loadAnimation(this, R.anim.picture_anim_modal_in); + } + mTvPicturePreview.setOnClickListener(this); + if (config.isAutomaticTitleRecyclerTop) { + titleViewBg.setOnClickListener(this); + } + mTvPicturePreview.setVisibility(config.chooseMode != PictureMimeType.ofAudio() && config.enablePreview ? View.VISIBLE : View.GONE); + mBottomLayout.setVisibility(config.selectionMode == PictureConfig.SINGLE + && config.isSingleDirectReturn ? View.GONE : View.VISIBLE); + mIvPictureLeftBack.setOnClickListener(this); + mTvPictureRight.setOnClickListener(this); + mTvPictureOk.setOnClickListener(this); + mTvPictureImgNum.setOnClickListener(this); + mTvPictureTitle.setOnClickListener(this); + mIvArrow.setOnClickListener(this); + String title = config.chooseMode == PictureMimeType.ofAudio() ? + getString(R.string.picture_all_audio) : getString(R.string.picture_camera_roll); + mTvPictureTitle.setText(title); + mTvPictureTitle.setTag(R.id.view_tag, -1); + folderWindow = new FolderPopWindow(this, config); + folderWindow.setArrowImageView(mIvArrow); + folderWindow.setOnAlbumItemClickListener(this); + mRecyclerView.addItemDecoration(new GridSpacingItemDecoration(config.imageSpanCount, + ScreenUtils.dip2px(this, 2), false)); + mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), config.imageSpanCount)); + if (!config.isPageStrategy) { + mRecyclerView.setHasFixedSize(true); + } else { + mRecyclerView.setReachBottomRow(RecyclerPreloadView.BOTTOM_PRELOAD); + mRecyclerView.setOnRecyclerViewPreloadListener(PictureSelectorActivity.this); + } + RecyclerView.ItemAnimator itemAnimator = mRecyclerView.getItemAnimator(); + if (itemAnimator != null) { + ((SimpleItemAnimator) itemAnimator).setSupportsChangeAnimations(false); + mRecyclerView.setItemAnimator(null); + } + loadAllMediaData(); + mTvEmpty.setText(config.chooseMode == PictureMimeType.ofAudio() ? + getString(R.string.picture_audio_empty) + : getString(R.string.picture_empty)); + StringUtils.tempTextFont(mTvEmpty, config.chooseMode); + mAdapter = new PictureImageGridAdapter(getContext(), config); + mAdapter.setOnPhotoSelectChangedListener(this); + + switch (config.animationMode) { + case AnimationType + .ALPHA_IN_ANIMATION: + mRecyclerView.setAdapter(new AlphaInAnimationAdapter(mAdapter)); + break; + case AnimationType + .SLIDE_IN_BOTTOM_ANIMATION: + mRecyclerView.setAdapter(new SlideInBottomAnimationAdapter(mAdapter)); + break; + default: + mRecyclerView.setAdapter(mAdapter); + break; + } + if (config.isOriginalControl) { + mCbOriginal.setVisibility(View.VISIBLE); + mCbOriginal.setChecked(config.isCheckOriginalImage); + mCbOriginal.setOnCheckedChangeListener((buttonView, isChecked) -> { + config.isCheckOriginalImage = isChecked; + }); + } + } + + @Override + public void onRecyclerViewPreloadMore() { + loadMoreData(); + } + + /** + * getPageLimit + * # If the user clicks to take a photo and returns, the Limit should be adjusted dynamically + * + * @return + */ + private int getPageLimit() { + int bucketId = ValueOf.toInt(mTvPictureTitle.getTag(R.id.view_tag)); + if (bucketId == -1) { + int limit = mOpenCameraCount > 0 ? config.pageSize - mOpenCameraCount : config.pageSize; + mOpenCameraCount = 0; + return limit; + } + return config.pageSize; + } + + /** + * load more data + */ + private void loadMoreData() { + if (mAdapter != null) { + if (isHasMore) { + mPage++; + long bucketId = ValueOf.toLong(mTvPictureTitle.getTag(R.id.view_tag)); + LocalMediaPageLoader.getInstance(getContext(), config).loadPageMediaData(bucketId, mPage, getPageLimit(), + (OnQueryDataResultListener) (result, currentPage, isHasMore) -> { + if (!isFinishing()) { + this.isHasMore = isHasMore; + if (isHasMore) { + hideDataNull(); + int size = result.size(); + if (size > 0) { + int positionStart = mAdapter.getSize(); + mAdapter.getData().addAll(result); + int itemCount = mAdapter.getItemCount(); + mAdapter.notifyItemRangeChanged(positionStart, itemCount); + } else { + onRecyclerViewPreloadMore(); + } + if (size < PictureConfig.MIN_PAGE_SIZE) { + mRecyclerView.onScrolled(mRecyclerView.getScrollX(), mRecyclerView.getScrollY()); + } + } else { + boolean isEmpty = mAdapter.isDataEmpty(); + if (isEmpty) { + showDataNull(bucketId == -1 ? getString(R.string.picture_empty) : getString(R.string.picture_data_null), R.drawable.picture_icon_no_data); + } + } + } + }); + } + } + } + + /** + * load All Data + */ + private void loadAllMediaData() { + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + readLocalMedia(); + } else { + PermissionChecker.requestPermissions(this, new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE); + } + } + + @Override + public void initPictureSelectorStyle() { + if (config.style != null) { + if (config.style.pictureTitleDownResId != 0) { + Drawable drawable = ContextCompat.getDrawable(this, config.style.pictureTitleDownResId); + mIvArrow.setImageDrawable(drawable); + } + if (config.style.pictureTitleTextColor != 0) { + mTvPictureTitle.setTextColor(config.style.pictureTitleTextColor); + } + if (config.style.pictureTitleTextSize != 0) { + mTvPictureTitle.setTextSize(config.style.pictureTitleTextSize); + } + + if (config.style.pictureRightDefaultTextColor != 0) { + mTvPictureRight.setTextColor(config.style.pictureRightDefaultTextColor); + } else { + if (config.style.pictureCancelTextColor != 0) { + mTvPictureRight.setTextColor(config.style.pictureCancelTextColor); + } + } + + if (config.style.pictureRightTextSize != 0) { + mTvPictureRight.setTextSize(config.style.pictureRightTextSize); + } + + if (config.style.pictureLeftBackIcon != 0) { + mIvPictureLeftBack.setImageResource(config.style.pictureLeftBackIcon); + } + if (config.style.pictureUnPreviewTextColor != 0) { + mTvPicturePreview.setTextColor(config.style.pictureUnPreviewTextColor); + } + if (config.style.picturePreviewTextSize != 0) { + mTvPicturePreview.setTextSize(config.style.picturePreviewTextSize); + } + if (config.style.pictureCheckNumBgStyle != 0) { + mTvPictureImgNum.setBackgroundResource(config.style.pictureCheckNumBgStyle); + } + if (config.style.pictureUnCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureUnCompleteTextColor); + } + if (config.style.pictureCompleteTextSize != 0) { + mTvPictureOk.setTextSize(config.style.pictureCompleteTextSize); + } + if (config.style.pictureBottomBgColor != 0) { + mBottomLayout.setBackgroundColor(config.style.pictureBottomBgColor); + } + if (config.style.pictureContainerBackgroundColor != 0) { + container.setBackgroundColor(config.style.pictureContainerBackgroundColor); + } + if (!TextUtils.isEmpty(config.style.pictureRightDefaultText)) { + mTvPictureRight.setText(config.style.pictureRightDefaultText); + } + if (!TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mTvPictureOk.setText(config.style.pictureUnCompleteText); + } + if (!TextUtils.isEmpty(config.style.pictureUnPreviewText)) { + mTvPicturePreview.setText(config.style.pictureUnPreviewText); + } + } else { + if (config.downResId != 0) { + Drawable drawable = ContextCompat.getDrawable(this, config.downResId); + mIvArrow.setImageDrawable(drawable); + } + int pictureBottomBgColor = AttrsUtils. + getTypeValueColor(getContext(), R.attr.picture_bottom_bg); + if (pictureBottomBgColor != 0) { + mBottomLayout.setBackgroundColor(pictureBottomBgColor); + } + } + titleViewBg.setBackgroundColor(colorPrimary); + + if (config.isOriginalControl) { + if (config.style != null) { + if (config.style.pictureOriginalControlStyle != 0) { + mCbOriginal.setButtonDrawable(config.style.pictureOriginalControlStyle); + } else { + mCbOriginal.setButtonDrawable(ContextCompat.getDrawable(this, R.drawable.picture_original_checkbox)); + } + if (config.style.pictureOriginalFontColor != 0) { + mCbOriginal.setTextColor(config.style.pictureOriginalFontColor); + } else { + mCbOriginal.setTextColor(ContextCompat.getColor(this, R.color.picture_color_53575e)); + } + if (config.style.pictureOriginalTextSize != 0) { + mCbOriginal.setTextSize(config.style.pictureOriginalTextSize); + } + } else { + mCbOriginal.setButtonDrawable(ContextCompat.getDrawable(this, R.drawable.picture_original_checkbox)); + mCbOriginal.setTextColor(ContextCompat.getColor(this, R.color.picture_color_53575e)); + } + } + + mAdapter.bindSelectData(selectionMedias); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + if (mAdapter != null) { + // Save the number of pictures or videos in the current list + outState.putInt(PictureConfig.EXTRA_OLD_CURRENT_LIST_SIZE, mAdapter.getSize()); + // Save the number of Camera film and Camera folder files + int size = folderWindow.getFolderData().size(); + if (size > 0) { + outState.putInt(PictureConfig.EXTRA_ALL_FOLDER_SIZE, folderWindow.getFolder(0).getImageNum()); + } + if (mAdapter.getSelectedData() != null) { + List selectedImages = mAdapter.getSelectedData(); + PictureSelector.saveSelectorList(outState, selectedImages); + } + } + } + + /** + * none number style + */ + private void isNumComplete(boolean numComplete) { + if (numComplete) { + initCompleteText(0); + } + } + + /** + * init Text + */ + @Override + protected void initCompleteText(int startCount) { + boolean isNotEmptyStyle = config.style != null; + if (config.selectionMode == PictureConfig.SINGLE) { + if (startCount <= 0) { + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_please_select)); + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(String.format(config.style.pictureCompleteText, startCount, 1)); + } else { + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_done)); + } + } + + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (startCount <= 0) { + mTvPictureOk.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_done_front_num, + startCount, config.maxSelectNum)); + } else { + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(String.format(config.style.pictureCompleteText, startCount, config.maxSelectNum)); + } else { + mTvPictureOk.setText(getString(R.string.picture_done_front_num, + startCount, config.maxSelectNum)); + } + } + } + } + + + /** + * get LocalMedia s + */ + protected void readLocalMedia() { + showPleaseDialog(); + if (config.isPageStrategy) { + LocalMediaPageLoader.getInstance(getContext(), config).loadAllMedia( + (OnQueryDataResultListener) (data, currentPage, isHasMore) -> { + if (!isFinishing()) { + this.isHasMore = true; + initPageModel(data); + synchronousCover(); + } + }); + } else { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + + @Override + public List doInBackground() { + return new LocalMediaLoader(getContext(), config).loadAllMedia(); + } + + @Override + public void onSuccess(List folders) { + initStandardModel(folders); + } + }); + } + } + + /** + * Page Model + * + * @param folders + */ + private void initPageModel(List folders) { + if (folders != null) { + folderWindow.bindFolder(folders); + mPage = 1; + LocalMediaFolder folder = folderWindow.getFolder(0); + mTvPictureTitle.setTag(R.id.view_count_tag, folder != null ? folder.getImageNum() : 0); + mTvPictureTitle.setTag(R.id.view_index_tag, 0); + long bucketId = folder != null ? folder.getBucketId() : -1; + mRecyclerView.setEnabledLoadMore(true); + LocalMediaPageLoader.getInstance(getContext(), config).loadPageMediaData(bucketId, mPage, + (OnQueryDataResultListener) (data, currentPage, isHasMore) -> { + if (!isFinishing()) { + dismissDialog(); + if (mAdapter != null) { + this.isHasMore = true; + // IsHasMore being true means that there's still data, but data being 0 might be a filter that's turned on and that doesn't happen to fit on the whole page + if (isHasMore && data.size() == 0) { + onRecyclerViewPreloadMore(); + return; + } + int currentSize = mAdapter.getSize(); + int resultSize = data.size(); + oldCurrentListSize = oldCurrentListSize + currentSize; + if (resultSize >= currentSize) { + // This situation is mainly caused by the use of camera memory, the Activity is recycled + if (currentSize > 0 && currentSize < resultSize && oldCurrentListSize != resultSize) { + if (isLocalMediaSame(data.get(0))) { + mAdapter.bindData(data); + } else { + mAdapter.getData().addAll(data); + } + } else { + mAdapter.bindData(data); + } + } + boolean isEmpty = mAdapter.isDataEmpty(); + if (isEmpty) { + showDataNull(getString(R.string.picture_empty), R.drawable.picture_icon_no_data); + } else { + hideDataNull(); + } + + } + } + }); + } else { + showDataNull(getString(R.string.picture_data_exception), R.drawable.picture_icon_data_error); + dismissDialog(); + } + } + + /** + * ofAll Page Model Synchronous cover + */ + private void synchronousCover() { + if (config.chooseMode == PictureMimeType.ofAll()) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public Boolean doInBackground() { + int size = folderWindow.getFolderData().size(); + for (int i = 0; i < size; i++) { + LocalMediaFolder mediaFolder = folderWindow.getFolder(i); + if (mediaFolder == null) { + continue; + } + String firstCover = LocalMediaPageLoader + .getInstance(getContext(), config).getFirstCover(mediaFolder.getBucketId()); + mediaFolder.setFirstImagePath(firstCover); + } + return true; + } + + @Override + public void onSuccess(Boolean result) { + // TODO Synchronous Success + } + }); + } + } + + /** + * Standard Model + * + * @param folders + */ + private void initStandardModel(List folders) { + if (folders != null) { + if (folders.size() > 0) { + folderWindow.bindFolder(folders); + LocalMediaFolder folder = folders.get(0); + folder.setChecked(true); + mTvPictureTitle.setTag(R.id.view_count_tag, folder.getImageNum()); + List result = folder.getData(); + if (mAdapter != null) { + int currentSize = mAdapter.getSize(); + int resultSize = result.size(); + oldCurrentListSize = oldCurrentListSize + currentSize; + if (resultSize >= currentSize) { + // This situation is mainly caused by the use of camera memory, the Activity is recycled + if (currentSize > 0 && currentSize < resultSize && oldCurrentListSize != resultSize) { + mAdapter.getData().addAll(result); + LocalMedia media = mAdapter.getData().get(0); + folder.setFirstImagePath(media.getPath()); + folder.getData().add(0, media); + folder.setCheckedNum(1); + folder.setImageNum(folder.getImageNum() + 1); + updateMediaFolder(folderWindow.getFolderData(), media); + } else { + mAdapter.bindData(result); + } + } + boolean isEmpty = mAdapter.isDataEmpty(); + if (isEmpty) { + showDataNull(getString(R.string.picture_empty), R.drawable.picture_icon_no_data); + } else { + hideDataNull(); + } + } + } else { + showDataNull(getString(R.string.picture_empty), R.drawable.picture_icon_no_data); + } + } else { + showDataNull(getString(R.string.picture_data_exception), R.drawable.picture_icon_data_error); + } + dismissDialog(); + } + + /** + * isSame + * + * @param newMedia + * @return + */ + private boolean isLocalMediaSame(LocalMedia newMedia) { + LocalMedia oldMedia = mAdapter.getItem(0); + if (oldMedia == null || newMedia == null) { + return false; + } + if (oldMedia.getPath().equals(newMedia.getPath())) { + return true; + } + // if Content:// type,determines whether the suffix id is consistent, mainly to solve the following two types of problems + // content://media/external/images/media/5844 + // content://media/external/file/5844 + if (PictureMimeType.isContent(newMedia.getPath()) + && PictureMimeType.isContent(oldMedia.getPath())) { + if (!TextUtils.isEmpty(newMedia.getPath()) && !TextUtils.isEmpty(oldMedia.getPath())) { + String newId = newMedia.getPath().substring(newMedia.getPath().lastIndexOf("/") + 1); + String oldId = oldMedia.getPath().substring(oldMedia.getPath().lastIndexOf("/") + 1); + if (newId.equals(oldId)) { + return true; + } + } + } + return false; + } + + /** + * Open Camera + */ + public void startCamera() { + if (!DoubleUtils.isFastDoubleClick()) { + if (PictureSelectionConfig.onCustomCameraInterfaceListener != null) { + if (config.chooseMode == PictureConfig.TYPE_ALL) { + PhotoItemSelectedDialog selectedDialog = PhotoItemSelectedDialog.newInstance(); + selectedDialog.setOnItemClickListener(this); + selectedDialog.show(getSupportFragmentManager(), "PhotoItemSelectedDialog"); + } else { + PictureSelectionConfig.onCustomCameraInterfaceListener.onCameraClick(getContext(), config, config.chooseMode); + config.cameraMimeType = config.chooseMode; + } + return; + } + if (config.isUseCustomCamera) { + startCustomCamera(); + return; + } + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + PhotoItemSelectedDialog selectedDialog = PhotoItemSelectedDialog.newInstance(); + selectedDialog.setOnItemClickListener(this); + selectedDialog.show(getSupportFragmentManager(), "PhotoItemSelectedDialog"); + break; + case PictureConfig.TYPE_IMAGE: + startOpenCamera(); + break; + case PictureConfig.TYPE_VIDEO: + startOpenCameraVideo(); + break; + case PictureConfig.TYPE_AUDIO: + startOpenCameraAudio(); + break; + default: + break; + } + } + } + + /** + * Open Custom Camera + */ + private void startCustomCamera() { + if (PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO)) { + Intent intent = new Intent(this, PictureCustomCameraActivity.class); + startActivityForResult(intent, PictureConfig.REQUEST_CAMERA); + PictureWindowAnimationStyle windowAnimationStyle = config.windowAnimationStyle; + overridePendingTransition(windowAnimationStyle != null && + windowAnimationStyle.activityEnterAnimation != 0 ? + windowAnimationStyle.activityEnterAnimation : + R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } else { + PermissionChecker + .requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE); + } + } + + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.pictureLeftBack || id == R.id.picture_right) { + if (folderWindow != null && folderWindow.isShowing()) { + folderWindow.dismiss(); + } else { + onBackPressed(); + } + return; + } + if (id == R.id.picture_title || id == R.id.ivArrow) { + if (folderWindow.isShowing()) { + folderWindow.dismiss(); + } else { + if (!folderWindow.isEmpty()) { + folderWindow.showAsDropDown(titleViewBg); + if (!config.isSingleDirectReturn) { + List selectedImages = mAdapter.getSelectedData(); + folderWindow.updateFolderCheckStatus(selectedImages); + } + } + } + return; + } + + if (id == R.id.picture_id_preview) { + onPreview(); + return; + } + + if (id == R.id.picture_tv_ok || id == R.id.picture_tvMediaNum) { + onComplete(); + return; + } + + if (id == R.id.titleViewBg) { + if (config.isAutomaticTitleRecyclerTop) { + int intervalTime = 500; + if (SystemClock.uptimeMillis() - intervalClickTime < intervalTime) { + if (mAdapter.getItemCount() > 0) { + mRecyclerView.scrollToPosition(0); + } + } else { + intervalClickTime = SystemClock.uptimeMillis(); + } + } + } + } + + /** + * Preview + */ + private void onPreview() { + List selectedImages = mAdapter.getSelectedData(); + List medias = new ArrayList<>(); + int size = selectedImages.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectedImages.get(i); + medias.add(media); + } + Bundle bundle = new Bundle(); + bundle.putParcelableArrayList(PictureConfig.EXTRA_PREVIEW_SELECT_LIST, (ArrayList) medias); + bundle.putParcelableArrayList(PictureConfig.EXTRA_SELECT_LIST, (ArrayList) selectedImages); + bundle.putBoolean(PictureConfig.EXTRA_BOTTOM_PREVIEW, true); + bundle.putBoolean(PictureConfig.EXTRA_CHANGE_ORIGINAL, config.isCheckOriginalImage); + bundle.putBoolean(PictureConfig.EXTRA_SHOW_CAMERA, mAdapter.isShowCamera()); + bundle.putString(PictureConfig.EXTRA_IS_CURRENT_DIRECTORY, mTvPictureTitle.getText().toString()); + JumpUtils.startPicturePreviewActivity(getContext(), config.isWeChatStyle, bundle, + config.selectionMode == PictureConfig.SINGLE ? UCrop.REQUEST_CROP : UCrop.REQUEST_MULTI_CROP); + + overridePendingTransition(config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewEnterAnimation != 0 + ? config.windowAnimationStyle.activityPreviewEnterAnimation : R.anim.picture_anim_enter, + R.anim.picture_anim_fade_in); + } + + /** + * Complete + */ + private void onComplete() { + List result = mAdapter.getSelectedData(); + int size = result.size(); + LocalMedia image = result.size() > 0 ? result.get(0) : null; + String mimeType = image != null ? image.getMimeType() : ""; + boolean isHasImage = PictureMimeType.isHasImage(mimeType); + if (config.isWithVideoImage) { + int videoSize = 0; + int imageSize = 0; + for (int i = 0; i < size; i++) { + LocalMedia media = result.get(i); + if (PictureMimeType.isHasVideo(media.getMimeType())) { + videoSize++; + } else { + imageSize++; + } + } + if (config.selectionMode == PictureConfig.MULTIPLE) { + if (config.minSelectNum > 0) { + if (imageSize < config.minSelectNum) { + showPromptDialog(getString(R.string.picture_min_img_num, config.minSelectNum)); + return; + } + } + if (config.minVideoSelectNum > 0) { + if (videoSize < config.minVideoSelectNum) { + showPromptDialog(getString(R.string.picture_min_video_num, config.minVideoSelectNum)); + return; + } + } + } + } else { + if (config.selectionMode == PictureConfig.MULTIPLE) { + if (PictureMimeType.isHasImage(mimeType) && config.minSelectNum > 0 && size < config.minSelectNum) { + String str = getString(R.string.picture_min_img_num, config.minSelectNum); + showPromptDialog(str); + return; + } + if (PictureMimeType.isHasVideo(mimeType) && config.minVideoSelectNum > 0 && size < config.minVideoSelectNum) { + String str = getString(R.string.picture_min_video_num, config.minVideoSelectNum); + showPromptDialog(str); + return; + } + } + } + + if (config.returnEmpty && size == 0) { + if (config.selectionMode == PictureConfig.MULTIPLE) { + if (config.minSelectNum > 0 && size < config.minSelectNum) { + String str = getString(R.string.picture_min_img_num, config.minSelectNum); + showPromptDialog(str); + return; + } + if (config.minVideoSelectNum > 0 && size < config.minVideoSelectNum) { + String str = getString(R.string.picture_min_video_num, config.minVideoSelectNum); + showPromptDialog(str); + return; + } + } + if (PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onResult(result); + } else { + Intent intent = PictureSelector.putIntentResult(result); + setResult(RESULT_OK, intent); + } + closeActivity(); + return; + } + if (config.isCheckOriginalImage) { + onResult(result); + return; + } + if (config.chooseMode == PictureMimeType.ofAll() && config.isWithVideoImage) { + bothMimeTypeWith(isHasImage, result); + } else { + separateMimeTypeWith(isHasImage, result); + } + } + + + /** + * They are different types of processing + * + * @param isHasImage + * @param images + */ + private void bothMimeTypeWith(boolean isHasImage, List images) { + LocalMedia image = images.size() > 0 ? images.get(0) : null; + if (image == null) { + return; + } + if (config.enableCrop) { + if (config.selectionMode == PictureConfig.SINGLE && isHasImage) { + config.originalPath = image.getPath(); + startCrop(config.originalPath, image.getMimeType()); + } else { + ArrayList cuts = new ArrayList<>(); + int count = images.size(); + int imageNum = 0; + for (int i = 0; i < count; i++) { + LocalMedia media = images.get(i); + if (media == null + || TextUtils.isEmpty(media.getPath())) { + continue; + } + if (PictureMimeType.isHasImage(media.getMimeType())) { + imageNum++; + } + CutInfo cutInfo = new CutInfo(); + cutInfo.setId(media.getId()); + cutInfo.setPath(media.getPath()); + cutInfo.setImageWidth(media.getWidth()); + cutInfo.setImageHeight(media.getHeight()); + cutInfo.setMimeType(media.getMimeType()); + cutInfo.setDuration(media.getDuration()); + cutInfo.setRealPath(media.getRealPath()); + cuts.add(cutInfo); + } + if (imageNum <= 0) { + onResult(images); + } else { + startCrop(cuts); + } + } + } else if (config.isCompress) { + int size = images.size(); + int imageNum = 0; + for (int i = 0; i < size; i++) { + LocalMedia media = images.get(i); + if (PictureMimeType.isHasImage(media.getMimeType())) { + imageNum++; + break; + } + } + if (imageNum <= 0) { + onResult(images); + } else { + compressImage(images); + } + } else { + onResult(images); + } + } + + /** + * Same type of image or video processing logic + * + * @param isHasImage + * @param images + */ + private void separateMimeTypeWith(boolean isHasImage, List images) { + LocalMedia image = images.size() > 0 ? images.get(0) : null; + if (image == null) { + return; + } + if (config.enableCrop && isHasImage) { + if (config.selectionMode == PictureConfig.SINGLE) { + config.originalPath = image.getPath(); + startCrop(config.originalPath, image.getMimeType()); + } else { + ArrayList cuts = new ArrayList<>(); + int count = images.size(); + for (int i = 0; i < count; i++) { + LocalMedia media = images.get(i); + if (media == null + || TextUtils.isEmpty(media.getPath())) { + continue; + } + CutInfo cutInfo = new CutInfo(); + cutInfo.setId(media.getId()); + cutInfo.setPath(media.getPath()); + cutInfo.setImageWidth(media.getWidth()); + cutInfo.setImageHeight(media.getHeight()); + cutInfo.setMimeType(media.getMimeType()); + cutInfo.setDuration(media.getDuration()); + cutInfo.setRealPath(media.getRealPath()); + cuts.add(cutInfo); + } + startCrop(cuts); + } + } else if (config.isCompress + && isHasImage) { + compressImage(images); + } else { + onResult(images); + } + } + + /** + * Play Audio + * + * @param path + */ + private void AudioDialog(final String path) { + if (!isFinishing()) { + audioDialog = new PictureCustomDialog(getContext(), R.layout.picture_audio_dialog); + if (audioDialog.getWindow() != null) { + audioDialog.getWindow().setWindowAnimations(R.style.Picture_Theme_Dialog_AudioStyle); + } + mTvMusicStatus = audioDialog.findViewById(R.id.tv_musicStatus); + mTvMusicTime = audioDialog.findViewById(R.id.tv_musicTime); + musicSeekBar = audioDialog.findViewById(R.id.musicSeekBar); + mTvMusicTotal = audioDialog.findViewById(R.id.tv_musicTotal); + mTvPlayPause = audioDialog.findViewById(R.id.tv_PlayPause); + mTvStop = audioDialog.findViewById(R.id.tv_Stop); + mTvQuit = audioDialog.findViewById(R.id.tv_Quit); + if (mHandler != null) { + mHandler.postDelayed(() -> initPlayer(path), 30); + } + mTvPlayPause.setOnClickListener(new AudioOnClick(path)); + mTvStop.setOnClickListener(new AudioOnClick(path)); + mTvQuit.setOnClickListener(new AudioOnClick(path)); + musicSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + if (fromUser) { + mediaPlayer.seekTo(progress); + } + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + } + }); + audioDialog.setOnDismissListener(dialog -> { + if (mHandler != null) { + mHandler.removeCallbacks(mRunnable); + } + new Handler().postDelayed(() -> stop(path), 30); + try { + if (audioDialog != null + && audioDialog.isShowing()) { + audioDialog.dismiss(); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + if (mHandler != null) { + mHandler.post(mRunnable); + } + audioDialog.show(); + } + } + + public Runnable mRunnable = new Runnable() { + @Override + public void run() { + try { + if (mediaPlayer != null) { + mTvMusicTime.setText(DateUtils.formatDurationTime(mediaPlayer.getCurrentPosition())); + musicSeekBar.setProgress(mediaPlayer.getCurrentPosition()); + musicSeekBar.setMax(mediaPlayer.getDuration()); + mTvMusicTotal.setText(DateUtils.formatDurationTime(mediaPlayer.getDuration())); + if (mHandler != null) { + mHandler.postDelayed(mRunnable, 200); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + + /** + * init Player + * + * @param path + */ + private void initPlayer(String path) { + mediaPlayer = new MediaPlayer(); + try { + mediaPlayer.setDataSource(path); + mediaPlayer.prepare(); + mediaPlayer.setLooping(true); + playAudio(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Audio Click + */ + public class AudioOnClick implements View.OnClickListener { + private String path; + + public AudioOnClick(String path) { + super(); + this.path = path; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.tv_PlayPause) { + playAudio(); + } + if (id == R.id.tv_Stop) { + mTvMusicStatus.setText(getString(R.string.picture_stop_audio)); + mTvPlayPause.setText(getString(R.string.picture_play_audio)); + stop(path); + } + if (id == R.id.tv_Quit) { + if (mHandler != null) { + mHandler.postDelayed(() -> stop(path), 30); + try { + if (audioDialog != null + && audioDialog.isShowing()) { + audioDialog.dismiss(); + } + } catch (Exception e) { + e.printStackTrace(); + } + mHandler.removeCallbacks(mRunnable); + } + } + } + } + + /** + * Play Audio + */ + private void playAudio() { + if (mediaPlayer != null) { + musicSeekBar.setProgress(mediaPlayer.getCurrentPosition()); + musicSeekBar.setMax(mediaPlayer.getDuration()); + } + String ppStr = mTvPlayPause.getText().toString(); + if (ppStr.equals(getString(R.string.picture_play_audio))) { + mTvPlayPause.setText(getString(R.string.picture_pause_audio)); + mTvMusicStatus.setText(getString(R.string.picture_play_audio)); + playOrPause(); + } else { + mTvPlayPause.setText(getString(R.string.picture_play_audio)); + mTvMusicStatus.setText(getString(R.string.picture_pause_audio)); + playOrPause(); + } + if (!isPlayAudio) { + if (mHandler != null) { + mHandler.post(mRunnable); + } + isPlayAudio = true; + } + } + + /** + * Audio Stop + * + * @param path + */ + public void stop(String path) { + if (mediaPlayer != null) { + try { + mediaPlayer.stop(); + mediaPlayer.reset(); + mediaPlayer.setDataSource(path); + mediaPlayer.prepare(); + mediaPlayer.seekTo(0); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * Audio Pause + */ + public void playOrPause() { + try { + if (mediaPlayer != null) { + if (mediaPlayer.isPlaying()) { + mediaPlayer.pause(); + } else { + mediaPlayer.start(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } + } + + + @Override + public void onItemClick(int position, boolean isCameraFolder, long bucketId, String + folderName, List data) { + boolean camera = config.isCamera && isCameraFolder; + mAdapter.setShowCamera(camera); + mTvPictureTitle.setText(folderName); + long currentBucketId = ValueOf.toLong(mTvPictureTitle.getTag(R.id.view_tag)); + mTvPictureTitle.setTag(R.id.view_count_tag, folderWindow.getFolder(position) != null + ? folderWindow.getFolder(position).getImageNum() : 0); + if (config.isPageStrategy) { + if (currentBucketId != bucketId) { + setLastCacheFolderData(); + boolean isCurrentCacheFolderData = isCurrentCacheFolderData(position); + if (!isCurrentCacheFolderData) { + mPage = 1; + showPleaseDialog(); + LocalMediaPageLoader.getInstance(getContext(), config).loadPageMediaData(bucketId, mPage, + (OnQueryDataResultListener) (result, currentPage, isHasMore) -> { + this.isHasMore = isHasMore; + if (!isFinishing()) { + if (result.size() == 0) { + mAdapter.clear(); + } + mAdapter.bindData(result); + mRecyclerView.onScrolled(0, 0); + mRecyclerView.smoothScrollToPosition(0); + dismissDialog(); + } + }); + } + } + } else { + mAdapter.bindData(data); + mRecyclerView.smoothScrollToPosition(0); + } + mTvPictureTitle.setTag(R.id.view_tag, bucketId); + folderWindow.dismiss(); + } + + /** + * Before switching directories, set the current directory cache + */ + private void setLastCacheFolderData() { + int oldPosition = ValueOf.toInt(mTvPictureTitle.getTag(R.id.view_index_tag)); + LocalMediaFolder lastFolder = folderWindow.getFolder(oldPosition); + lastFolder.setData(mAdapter.getData()); + lastFolder.setCurrentDataPage(mPage); + lastFolder.setHasMore(isHasMore); + } + + /** + * Does the current album have a cache + * + * @param position + */ + private boolean isCurrentCacheFolderData(int position) { + mTvPictureTitle.setTag(R.id.view_index_tag, position); + LocalMediaFolder currentFolder = folderWindow.getFolder(position); + if (currentFolder != null + && currentFolder.getData() != null + && currentFolder.getData().size() > 0) { + mAdapter.bindData(currentFolder.getData()); + mPage = currentFolder.getCurrentDataPage(); + isHasMore = currentFolder.isHasMore(); + mRecyclerView.smoothScrollToPosition(0); + + return true; + } + return false; + } + + @Override + public void onTakePhoto() { + // Check the permissions + if (PermissionChecker.checkSelfPermission(this, Manifest.permission.CAMERA)) { + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + startCamera(); + } else { + PermissionChecker.requestPermissions(this, new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_CAMERA_STORAGE_PERMISSIONS_CODE); + } + } else { + PermissionChecker + .requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE); + } + } + + @Override + public void onChange(List selectData) { + changeImageNumber(selectData); + } + + @Override + public void onPictureClick(LocalMedia media, int position) { + if (config.selectionMode == PictureConfig.SINGLE && config.isSingleDirectReturn) { + List list = new ArrayList<>(); + list.add(media); + if (config.enableCrop && PictureMimeType.isHasImage(media.getMimeType()) && !config.isCheckOriginalImage) { + mAdapter.bindSelectData(list); + startCrop(media.getPath(), media.getMimeType()); + } else { + handlerResult(list); + } + } else { + List data = mAdapter.getData(); + startPreview(data, position); + } + } + + /** + * preview image and video + * + * @param previewImages + * @param position + */ + public void startPreview(List previewImages, int position) { + LocalMedia media = previewImages.get(position); + String mimeType = media.getMimeType(); + Bundle bundle = new Bundle(); + List result = new ArrayList<>(); + if (PictureMimeType.isHasVideo(mimeType)) { + // video + if (config.selectionMode == PictureConfig.SINGLE && !config.enPreviewVideo) { + result.add(media); + onResult(result); + } else { + if (PictureSelectionConfig.customVideoPlayCallback != null) { + PictureSelectionConfig.customVideoPlayCallback.startPlayVideo(media); + } else { + bundle.putParcelable(PictureConfig.EXTRA_MEDIA_KEY, media); + JumpUtils.startPictureVideoPlayActivity(getContext(), bundle, PictureConfig.PREVIEW_VIDEO_CODE); + } + } + } else if (PictureMimeType.isHasAudio(mimeType)) { + // audio + if (config.selectionMode == PictureConfig.SINGLE) { + result.add(media); + onResult(result); + } else { + AudioDialog(media.getPath()); + } + } else { + // image + List selectedData = mAdapter.getSelectedData(); + ImagesObservable.getInstance().savePreviewMediaData(new ArrayList<>(previewImages)); + bundle.putParcelableArrayList(PictureConfig.EXTRA_SELECT_LIST, (ArrayList) selectedData); + bundle.putInt(PictureConfig.EXTRA_POSITION, position); + bundle.putBoolean(PictureConfig.EXTRA_CHANGE_ORIGINAL, config.isCheckOriginalImage); + bundle.putBoolean(PictureConfig.EXTRA_SHOW_CAMERA, mAdapter.isShowCamera()); + bundle.putLong(PictureConfig.EXTRA_BUCKET_ID, ValueOf.toLong(mTvPictureTitle.getTag(R.id.view_tag))); + bundle.putInt(PictureConfig.EXTRA_PAGE, mPage); + bundle.putParcelable(PictureConfig.EXTRA_CONFIG, config); + bundle.putInt(PictureConfig.EXTRA_DATA_COUNT, ValueOf.toInt(mTvPictureTitle.getTag(R.id.view_count_tag))); + bundle.putString(PictureConfig.EXTRA_IS_CURRENT_DIRECTORY, mTvPictureTitle.getText().toString()); + JumpUtils.startPicturePreviewActivity(getContext(), config.isWeChatStyle, bundle, + config.selectionMode == PictureConfig.SINGLE ? UCrop.REQUEST_CROP : UCrop.REQUEST_MULTI_CROP); + overridePendingTransition(config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewEnterAnimation != 0 + ? config.windowAnimationStyle.activityPreviewEnterAnimation : R.anim.picture_anim_enter, R.anim.picture_anim_fade_in); + } + } + + + /** + * change image selector state + * + * @param selectData + */ + protected void changeImageNumber(List selectData) { + boolean enable = selectData.size() != 0; + if (enable) { + mTvPictureOk.setEnabled(true); + mTvPictureOk.setSelected(true); + mTvPicturePreview.setEnabled(true); + mTvPicturePreview.setSelected(true); + if (config.style != null) { + if (config.style.pictureCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureCompleteTextColor); + } + if (config.style.picturePreviewTextColor != 0) { + mTvPicturePreview.setTextColor(config.style.picturePreviewTextColor); + } + } + if (config.style != null && !TextUtils.isEmpty(config.style.picturePreviewText)) { + mTvPicturePreview.setText(config.style.picturePreviewText); + } else { + mTvPicturePreview.setText(getString(R.string.picture_preview_num, selectData.size())); + } + if (numComplete) { + initCompleteText(selectData.size()); + } else { + if (!isStartAnimation) { + mTvPictureImgNum.startAnimation(animation); + } + mTvPictureImgNum.setVisibility(View.VISIBLE); + mTvPictureImgNum.setText(String.valueOf(selectData.size())); + if (config.style != null && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mTvPictureOk.setText(config.style.pictureCompleteText); + } else { + mTvPictureOk.setText(getString(R.string.picture_completed)); + } + isStartAnimation = false; + } + } else { + mTvPictureOk.setEnabled(config.returnEmpty); + mTvPictureOk.setSelected(false); + mTvPicturePreview.setEnabled(false); + mTvPicturePreview.setSelected(false); + if (config.style != null) { + if (config.style.pictureUnCompleteTextColor != 0) { + mTvPictureOk.setTextColor(config.style.pictureUnCompleteTextColor); + } + if (config.style.pictureUnPreviewTextColor != 0) { + mTvPicturePreview.setTextColor(config.style.pictureUnPreviewTextColor); + } + } + if (config.style != null && !TextUtils.isEmpty(config.style.pictureUnPreviewText)) { + mTvPicturePreview.setText(config.style.pictureUnPreviewText); + } else { + mTvPicturePreview.setText(getString(R.string.picture_preview)); + } + if (numComplete) { + initCompleteText(selectData.size()); + } else { + mTvPictureImgNum.setVisibility(View.INVISIBLE); + if (config.style != null && !TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mTvPictureOk.setText(config.style.pictureUnCompleteText); + } else { + mTvPictureOk.setText(getString(R.string.picture_please_select)); + } + } + } + } + + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case PictureConfig.PREVIEW_VIDEO_CODE: + if (data != null) { + List list = data.getParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST); + if (list != null && list.size() > 0) { + onResult(list); + } + } + break; + case UCrop.REQUEST_CROP: + singleCropHandleResult(data); + break; + case UCrop.REQUEST_MULTI_CROP: + multiCropHandleResult(data); + break; + case PictureConfig.REQUEST_CAMERA: + dispatchHandleCamera(data); + break; + default: + break; + } + } else if (resultCode == RESULT_CANCELED) { + previewCallback(data); + } else if (resultCode == UCrop.RESULT_ERROR) { + if (data != null) { + Throwable throwable = (Throwable) data.getSerializableExtra(UCrop.EXTRA_ERROR); + if (throwable != null) { + ToastUtils.s(getContext(), throwable.getMessage()); + } + } + } + } + + /** + * Preview interface callback processing + * + * @param data + */ + private void previewCallback(Intent data) { + if (data == null) { + return; + } + if (config.isOriginalControl) { + config.isCheckOriginalImage = data.getBooleanExtra(PictureConfig.EXTRA_CHANGE_ORIGINAL, config.isCheckOriginalImage); + mCbOriginal.setChecked(config.isCheckOriginalImage); + } + List list = data.getParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST); + if (mAdapter != null && list != null) { + boolean isCompleteOrSelected = data.getBooleanExtra(PictureConfig.EXTRA_COMPLETE_SELECTED, false); + if (isCompleteOrSelected) { + onChangeData(list); + if (config.isWithVideoImage) { + int size = list.size(); + int imageSize = 0; + for (int i = 0; i < size; i++) { + LocalMedia media = list.get(i); + if (PictureMimeType.isHasImage(media.getMimeType())) { + imageSize++; + break; + } + } + if (imageSize <= 0 || !config.isCompress || config.isCheckOriginalImage) { + onResult(list); + } else { + compressImage(list); + } + } else { + // Determine if the resource is of the same type + String mimeType = list.size() > 0 ? list.get(0).getMimeType() : ""; + if (config.isCompress && PictureMimeType.isHasImage(mimeType) + && !config.isCheckOriginalImage) { + compressImage(list); + } else { + onResult(list); + } + } + } else { + // Resources are selected on the preview page + isStartAnimation = true; + } + mAdapter.bindSelectData(list); + mAdapter.notifyDataSetChanged(); + } + } + + /** + * Preview the callback + * + * @param list + */ + protected void onChangeData(List list) { + + } + + /** + * singleDirectReturn + * + * @param mimeType + */ + private void singleDirectReturnCameraHandleResult(String mimeType) { + boolean isHasImage = PictureMimeType.isHasImage(mimeType); + if (config.enableCrop && isHasImage) { + config.originalPath = config.cameraPath; + startCrop(config.cameraPath, mimeType); + } else if (config.isCompress && isHasImage) { + List selectedImages = mAdapter.getSelectedData(); + compressImage(selectedImages); + } else { + onResult(mAdapter.getSelectedData()); + } + } + + /** + * Camera Handle + * + * @param intent + */ + private void dispatchHandleCamera(Intent intent) { + // If PictureSelectionConfig is not empty, synchronize it + PictureSelectionConfig selectionConfig = intent != null ? intent.getParcelableExtra(PictureConfig.EXTRA_CONFIG) : null; + if (selectionConfig != null) { + config = selectionConfig; + } + boolean isAudio = config.chooseMode == PictureMimeType.ofAudio(); + config.cameraPath = isAudio ? getAudioPath(intent) : config.cameraPath; + if (TextUtils.isEmpty(config.cameraPath)) { + return; + } + showPleaseDialog(); + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public LocalMedia doInBackground() { + LocalMedia media = new LocalMedia(); + String mimeType = isAudio ? PictureMimeType.MIME_TYPE_AUDIO : ""; + int[] newSize = new int[2]; + long duration = 0; + if (!isAudio) { + if (PictureMimeType.isContent(config.cameraPath)) { + // content: Processing rules + String path = PictureFileUtils.getPath(getContext(), Uri.parse(config.cameraPath)); + if (!TextUtils.isEmpty(path)) { + File cameraFile = new File(path); + mimeType = PictureMimeType.getMimeType(config.cameraMimeType); + media.setSize(cameraFile.length()); + } + if (PictureMimeType.isHasImage(mimeType)) { + newSize = MediaUtils.getImageSizeForUrlToAndroidQ(getContext(), config.cameraPath); + } else if (PictureMimeType.isHasVideo(mimeType)) { + newSize = MediaUtils.getVideoSizeForUri(getContext(), Uri.parse(config.cameraPath)); + duration = MediaUtils.extractDuration(getContext(), SdkVersionUtils.checkedAndroid_Q(), config.cameraPath); + } + int lastIndexOf = config.cameraPath.lastIndexOf("/") + 1; + media.setId(lastIndexOf > 0 ? ValueOf.toLong(config.cameraPath.substring(lastIndexOf)) : -1); + media.setRealPath(path); + // Custom photo has been in the application sandbox into the file + String mediaPath = intent != null ? intent.getStringExtra(PictureConfig.EXTRA_MEDIA_PATH) : null; + media.setAndroidQToPath(mediaPath); + } else { + File cameraFile = new File(config.cameraPath); + mimeType = PictureMimeType.getMimeType(config.cameraMimeType); + media.setSize(cameraFile.length()); + if (PictureMimeType.isHasImage(mimeType)) { + int degree = PictureFileUtils.readPictureDegree(getContext(), config.cameraPath); + BitmapUtils.rotateImage(degree, config.cameraPath); + newSize = MediaUtils.getImageSizeForUrl(config.cameraPath); + } else if (PictureMimeType.isHasVideo(mimeType)) { + newSize = MediaUtils.getVideoSizeForUrl(config.cameraPath); + duration = MediaUtils.extractDuration(getContext(), SdkVersionUtils.checkedAndroid_Q(), config.cameraPath); + } + // Taking a photo generates a temporary id + media.setId(System.currentTimeMillis()); + } + media.setPath(config.cameraPath); + media.setDuration(duration); + media.setMimeType(mimeType); + media.setWidth(newSize[0]); + media.setHeight(newSize[1]); + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isHasVideo(media.getMimeType())) { + media.setParentFolderName(Environment.DIRECTORY_MOVIES); + } else { + media.setParentFolderName(PictureMimeType.CAMERA); + } + media.setChooseModel(config.chooseMode); + long bucketId = MediaUtils.getCameraFirstBucketId(getContext()); + media.setBucketId(bucketId); + // The width and height of the image are reversed if there is rotation information + MediaUtils.setOrientationSynchronous(getContext(), media, config.isAndroidQChangeWH,config.isAndroidQChangeVideoWH); + } + return media; + } + + @Override + public void onSuccess(LocalMedia result) { + dismissDialog(); + // Refresh the system library + if (!SdkVersionUtils.checkedAndroid_Q()) { + if (config.isFallbackVersion3) { + new PictureMediaScannerConnection(getContext(), config.cameraPath); + } else { + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(new File(config.cameraPath)))); + } + } + // add data Adapter + notifyAdapterData(result); + // Solve some phone using Camera, DCIM will produce repetitive problems + if (!SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isHasImage(result.getMimeType())) { + int lastImageId = MediaUtils.getDCIMLastImageId(getContext()); + if (lastImageId != -1) { + MediaUtils.removeMedia(getContext(), lastImageId); + } + } + } + }); + + } + + /** + * Update Adapter Data + * + * @param media + */ + private void notifyAdapterData(LocalMedia media) { + if (mAdapter != null) { + boolean isAddSameImp = isAddSameImp(folderWindow.getFolder(0) != null ? folderWindow.getFolder(0).getImageNum() : 0); + if (!isAddSameImp) { + mAdapter.getData().add(0, media); + mOpenCameraCount++; + } + if (checkVideoLegitimacy(media)) { + if (config.selectionMode == PictureConfig.SINGLE) { + dispatchHandleSingle(media); + } else { + dispatchHandleMultiple(media); + } + } + mAdapter.notifyItemInserted(config.isCamera ? 1 : 0); + mAdapter.notifyItemRangeChanged(config.isCamera ? 1 : 0, mAdapter.getSize()); + // Solve the problem that some mobile phones do not refresh the system library timely after using Camera + if (config.isPageStrategy) { + manualSaveFolderForPageModel(media); + } else { + manualSaveFolder(media); + } + mTvEmpty.setVisibility(mAdapter.getSize() > 0 || config.isSingleDirectReturn ? View.GONE : View.VISIBLE); + // update all count + if (folderWindow.getFolder(0) != null) { + mTvPictureTitle.setTag(R.id.view_count_tag, folderWindow.getFolder(0).getImageNum()); + } + allFolderSize = 0; + } + } + + /** + * After using Camera, MultiSelect mode handles the logic + * + * @param media + */ + private void dispatchHandleMultiple(LocalMedia media) { + List selectedData = mAdapter.getSelectedData(); + int count = selectedData.size(); + String oldMimeType = count > 0 ? selectedData.get(0).getMimeType() : ""; + boolean mimeTypeSame = PictureMimeType.isMimeTypeSame(oldMimeType, media.getMimeType()); + if (config.isWithVideoImage) { + int videoSize = 0; + for (int i = 0; i < count; i++) { + LocalMedia item = selectedData.get(i); + if (PictureMimeType.isHasVideo(item.getMimeType())) { + videoSize++; + } + } + if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (config.maxVideoSelectNum <= 0) { + showPromptDialog(getString(R.string.picture_rule)); + } else { + if (selectedData.size() >= config.maxSelectNum) { + showPromptDialog(getString(R.string.picture_message_max_num, config.maxSelectNum)); + } else { + if (videoSize < config.maxVideoSelectNum) { + selectedData.add(0, media); + mAdapter.bindSelectData(selectedData); + } else { + showPromptDialog(StringUtils.getMsg(getContext(), media.getMimeType(), + config.maxVideoSelectNum)); + } + } + } + } else { + if (selectedData.size() < config.maxSelectNum) { + selectedData.add(0, media); + mAdapter.bindSelectData(selectedData); + } else { + showPromptDialog(StringUtils.getMsg(getContext(), media.getMimeType(), + config.maxSelectNum)); + } + } + + } else { + if (PictureMimeType.isHasVideo(oldMimeType) && config.maxVideoSelectNum > 0) { + if (count < config.maxVideoSelectNum) { + if (mimeTypeSame || count == 0) { + if (selectedData.size() < config.maxVideoSelectNum) { + selectedData.add(0, media); + mAdapter.bindSelectData(selectedData); + } + } + } else { + showPromptDialog(StringUtils.getMsg(getContext(), oldMimeType, + config.maxVideoSelectNum)); + } + } else { + if (count < config.maxSelectNum) { + if (mimeTypeSame || count == 0) { + selectedData.add(0, media); + mAdapter.bindSelectData(selectedData); + } + } else { + showPromptDialog(StringUtils.getMsg(getContext(), oldMimeType, + config.maxSelectNum)); + } + } + } + } + + /** + * After using the camera, the radio mode handles the logic + * + * @param media + */ + private void dispatchHandleSingle(LocalMedia media) { + if (config.isSingleDirectReturn) { + List selectedData = mAdapter.getSelectedData(); + selectedData.add(media); + mAdapter.bindSelectData(selectedData); + singleDirectReturnCameraHandleResult(media.getMimeType()); + } else { + List selectedData = mAdapter.getSelectedData(); + String mimeType = selectedData.size() > 0 ? selectedData.get(0).getMimeType() : ""; + boolean mimeTypeSame = PictureMimeType.isMimeTypeSame(mimeType, media.getMimeType()); + if (mimeTypeSame || selectedData.size() == 0) { + singleRadioMediaImage(); + selectedData.add(media); + mAdapter.bindSelectData(selectedData); + } + } + } + + /** + * Verify the validity of the video + * + * @param media + * @return + */ + private boolean checkVideoLegitimacy(LocalMedia media) { + boolean isEnterNext = true; + if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (config.videoMinSecond > 0 && config.videoMaxSecond > 0) { + // The user sets the minimum and maximum video length to determine whether the video is within the interval + if (media.getDuration() < config.videoMinSecond || media.getDuration() > config.videoMaxSecond) { + isEnterNext = false; + showPromptDialog(getString(R.string.picture_choose_limit_seconds, config.videoMinSecond / 1000, config.videoMaxSecond / 1000)); + } + } else if (config.videoMinSecond > 0) { + // The user has only set a minimum video length limit + if (media.getDuration() < config.videoMinSecond) { + isEnterNext = false; + showPromptDialog(getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + } + } else if (config.videoMaxSecond > 0) { + // Only the maximum length of video is set + if (media.getDuration() > config.videoMaxSecond) { + isEnterNext = false; + showPromptDialog(getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + } + } + } + return isEnterNext; + } + + /** + * Single picture clipping callback + * + * @param data + */ + private void singleCropHandleResult(Intent data) { + if (data == null) { + return; + } + Uri resultUri = UCrop.getOutput(data); + if (resultUri == null) { + return; + } + List result = new ArrayList<>(); + String cutPath = resultUri.getPath(); + if (mAdapter != null) { + List list = data.getParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST); + if (list != null) { + mAdapter.bindSelectData(list); + mAdapter.notifyDataSetChanged(); + } + List mediaList = mAdapter.getSelectedData(); + LocalMedia media = mediaList != null && mediaList.size() > 0 ? mediaList.get(0) : null; + if (media != null) { + config.originalPath = media.getPath(); + media.setCutPath(cutPath); + media.setChooseModel(config.chooseMode); + boolean isCutPathEmpty = !TextUtils.isEmpty(cutPath); + if (SdkVersionUtils.checkedAndroid_Q() + && PictureMimeType.isContent(media.getPath())) { + if (isCutPathEmpty) { + media.setSize(new File(cutPath).length()); + } else { + media.setSize(!TextUtils.isEmpty(media.getRealPath()) ? new File(media.getRealPath()).length() : 0); + } + media.setAndroidQToPath(cutPath); + } else { + media.setSize(isCutPathEmpty ? new File(cutPath).length() : 0); + } + media.setCut(isCutPathEmpty); + result.add(media); + handlerResult(result); + } else { + // Preview screen selects the image and crop the callback + media = list != null && list.size() > 0 ? list.get(0) : null; + if (media != null) { + config.originalPath = media.getPath(); + media.setCutPath(cutPath); + media.setChooseModel(config.chooseMode); + boolean isCutPathEmpty = !TextUtils.isEmpty(cutPath); + if (SdkVersionUtils.checkedAndroid_Q() + && PictureMimeType.isContent(media.getPath())) { + if (isCutPathEmpty) { + media.setSize(new File(cutPath).length()); + } else { + media.setSize(!TextUtils.isEmpty(media.getRealPath()) ? new File(media.getRealPath()).length() : 0); + } + media.setAndroidQToPath(cutPath); + } else { + media.setSize(isCutPathEmpty ? new File(cutPath).length() : 0); + } + media.setCut(isCutPathEmpty); + result.add(media); + handlerResult(result); + } + } + } + } + + /** + * Multiple picture crop + * + * @param data + */ + protected void multiCropHandleResult(Intent data) { + if (data == null) { + return; + } + List mCuts = UCrop.getMultipleOutput(data); + if (mCuts == null || mCuts.size() == 0) { + return; + } + int size = mCuts.size(); + boolean isAndroidQ = SdkVersionUtils.checkedAndroid_Q(); + List list = data.getParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST); + if (list != null) { + mAdapter.bindSelectData(list); + mAdapter.notifyDataSetChanged(); + } + int oldSize = mAdapter != null ? mAdapter.getSelectedData().size() : 0; + if (oldSize == size) { + List result = mAdapter.getSelectedData(); + for (int i = 0; i < size; i++) { + CutInfo c = mCuts.get(i); + LocalMedia media = result.get(i); + media.setCut(!TextUtils.isEmpty(c.getCutPath())); + media.setPath(c.getPath()); + media.setMimeType(c.getMimeType()); + media.setCutPath(c.getCutPath()); + media.setWidth(c.getImageWidth()); + media.setHeight(c.getImageHeight()); + media.setAndroidQToPath(isAndroidQ ? c.getCutPath() : media.getAndroidQToPath()); + media.setSize(!TextUtils.isEmpty(c.getCutPath()) ? new File(c.getCutPath()).length() : media.getSize()); + } + handlerResult(result); + } else { + // Fault-tolerant processing + List result = new ArrayList<>(); + for (int i = 0; i < size; i++) { + CutInfo c = mCuts.get(i); + LocalMedia media = new LocalMedia(); + media.setId(c.getId()); + media.setCut(!TextUtils.isEmpty(c.getCutPath())); + media.setPath(c.getPath()); + media.setCutPath(c.getCutPath()); + media.setMimeType(c.getMimeType()); + media.setWidth(c.getImageWidth()); + media.setHeight(c.getImageHeight()); + media.setDuration(c.getDuration()); + media.setChooseModel(config.chooseMode); + media.setAndroidQToPath(isAndroidQ ? c.getCutPath() : c.getAndroidQToPath()); + if (!TextUtils.isEmpty(c.getCutPath())) { + media.setSize(new File(c.getCutPath()).length()); + } else { + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(c.getPath())) { + media.setSize(!TextUtils.isEmpty(c.getRealPath()) ? new File(c.getRealPath()).length() : 0); + } else { + media.setSize(new File(c.getPath()).length()); + } + } + result.add(media); + } + handlerResult(result); + } + } + + /** + * Just make sure you pick one + */ + private void singleRadioMediaImage() { + List selectData = mAdapter.getSelectedData(); + if (selectData != null + && selectData.size() > 0) { + LocalMedia media = selectData.get(0); + int position = media.getPosition(); + selectData.clear(); + mAdapter.notifyItemChanged(position); + } + } + + /** + * Manually add the photo to the list of photos and set it to select-paging mode + * + * @param media + */ + private void manualSaveFolderForPageModel(LocalMedia media) { + if (media == null) { + return; + } + int count = folderWindow.getFolderData().size(); + LocalMediaFolder allFolder = count > 0 ? folderWindow.getFolderData().get(0) : new LocalMediaFolder(); + if (allFolder != null) { + int totalNum = allFolder.getImageNum(); + allFolder.setFirstImagePath(media.getPath()); + allFolder.setImageNum(isAddSameImp(totalNum) ? allFolder.getImageNum() : allFolder.getImageNum() + 1); + // Create All folder + if (count == 0) { + allFolder.setName(config.chooseMode == PictureMimeType.ofAudio() ? + getString(R.string.picture_all_audio) : getString(R.string.picture_camera_roll)); + allFolder.setOfAllType(config.chooseMode); + allFolder.setCameraFolder(true); + allFolder.setChecked(true); + allFolder.setBucketId(-1); + folderWindow.getFolderData().add(0, allFolder); + // Create Camera + LocalMediaFolder cameraFolder = new LocalMediaFolder(); + cameraFolder.setName(media.getParentFolderName()); + cameraFolder.setImageNum(isAddSameImp(totalNum) ? cameraFolder.getImageNum() : cameraFolder.getImageNum() + 1); + cameraFolder.setFirstImagePath(media.getPath()); + cameraFolder.setBucketId(media.getBucketId()); + folderWindow.getFolderData().add(folderWindow.getFolderData().size(), cameraFolder); + } else { + boolean isCamera = false; + String newFolder = SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isHasVideo(media.getMimeType()) + ? Environment.DIRECTORY_MOVIES : PictureMimeType.CAMERA; + for (int i = 0; i < count; i++) { + LocalMediaFolder cameraFolder = folderWindow.getFolderData().get(i); + if (cameraFolder.getName().startsWith(newFolder)) { + media.setBucketId(cameraFolder.getBucketId()); + cameraFolder.setFirstImagePath(config.cameraPath); + cameraFolder.setImageNum(isAddSameImp(totalNum) ? cameraFolder.getImageNum() : cameraFolder.getImageNum() + 1); + if (cameraFolder.getData() != null && cameraFolder.getData().size() > 0) { + cameraFolder.getData().add(0, media); + } + isCamera = true; + break; + } + } + if (!isCamera) { + // There is no Camera folder locally. Create one + LocalMediaFolder cameraFolder = new LocalMediaFolder(); + cameraFolder.setName(media.getParentFolderName()); + cameraFolder.setImageNum(isAddSameImp(totalNum) ? cameraFolder.getImageNum() : cameraFolder.getImageNum() + 1); + cameraFolder.setFirstImagePath(media.getPath()); + cameraFolder.setBucketId(media.getBucketId()); + folderWindow.getFolderData().add(cameraFolder); + sortFolder(folderWindow.getFolderData()); + } + } + folderWindow.bindFolder(folderWindow.getFolderData()); + } + } + + /** + * Manually add the photo to the list of photos and set it to select + * + * @param media + */ + private void manualSaveFolder(LocalMedia media) { + try { + boolean isEmpty = folderWindow.isEmpty(); + int totalNum = folderWindow.getFolder(0) != null ? folderWindow.getFolder(0).getImageNum() : 0; + LocalMediaFolder allFolder; + if (isEmpty) { + // All Folder + createNewFolder(folderWindow.getFolderData()); + allFolder = folderWindow.getFolderData().size() > 0 ? folderWindow.getFolderData().get(0) : null; + if (allFolder == null) { + allFolder = new LocalMediaFolder(); + folderWindow.getFolderData().add(0, allFolder); + } + } else { + // All Folder + allFolder = folderWindow.getFolderData().get(0); + } + allFolder.setFirstImagePath(media.getPath()); + allFolder.setData(mAdapter.getData()); + allFolder.setBucketId(-1); + allFolder.setImageNum(isAddSameImp(totalNum) ? allFolder.getImageNum() : allFolder.getImageNum() + 1); + + // Camera + LocalMediaFolder cameraFolder = getImageFolder(media.getPath(), media.getRealPath(), folderWindow.getFolderData()); + if (cameraFolder != null) { + cameraFolder.setImageNum(isAddSameImp(totalNum) ? cameraFolder.getImageNum() : cameraFolder.getImageNum() + 1); + if (!isAddSameImp(totalNum)) { + cameraFolder.getData().add(0, media); + } + cameraFolder.setBucketId(media.getBucketId()); + cameraFolder.setFirstImagePath(config.cameraPath); + } + folderWindow.bindFolder(folderWindow.getFolderData()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * Is the quantity consistent + * + * @return + */ + private boolean isAddSameImp(int totalNum) { + if (totalNum == 0) { + return false; + } + return allFolderSize > 0 && allFolderSize < totalNum; + } + + /** + * Update Folder + * + * @param imageFolders + */ + private void updateMediaFolder(List imageFolders, LocalMedia media) { + File imageFile = new File(media.getRealPath()); + File folderFile = imageFile.getParentFile(); + if (folderFile == null) { + return; + } + int size = imageFolders.size(); + for (int i = 0; i < size; i++) { + LocalMediaFolder folder = imageFolders.get(i); + String name = folder.getName(); + if (TextUtils.isEmpty(name)) { + continue; + } + if (name.equals(folderFile.getName())) { + folder.setFirstImagePath(config.cameraPath); + folder.setImageNum(folder.getImageNum() + 1); + folder.setCheckedNum(1); + folder.getData().add(0, media); + break; + } + } + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + if (config != null && PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onCancel(); + } + closeActivity(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (animation != null) { + animation.cancel(); + animation = null; + } + if (mediaPlayer != null && mHandler != null) { + mHandler.removeCallbacks(mRunnable); + mediaPlayer.release(); + mediaPlayer = null; + } + } + + @Override + public void onItemClick(View view, int position) { + switch (position) { + case PhotoItemSelectedDialog.IMAGE_CAMERA: + if (PictureSelectionConfig.onCustomCameraInterfaceListener != null) { + PictureSelectionConfig.onCustomCameraInterfaceListener.onCameraClick(getContext(), config, PictureConfig.TYPE_IMAGE); + config.cameraMimeType = PictureMimeType.ofImage(); + } else { + startOpenCamera(); + } + break; + case PhotoItemSelectedDialog.VIDEO_CAMERA: + if (PictureSelectionConfig.onCustomCameraInterfaceListener != null) { + PictureSelectionConfig.onCustomCameraInterfaceListener.onCameraClick(getContext(), config, PictureConfig.TYPE_IMAGE); + config.cameraMimeType = PictureMimeType.ofVideo(); + } else { + startOpenCameraVideo(); + } + break; + default: + break; + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE: + // Store Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + readLocalMedia(); + } else { + showPermissionsDialog(false, getString(R.string.picture_jurisdiction)); + } + break; + case PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE: + // Camera Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onTakePhoto(); + } else { + showPermissionsDialog(true, getString(R.string.picture_camera)); + } + break; + case PictureConfig.APPLY_CAMERA_STORAGE_PERMISSIONS_CODE: + // Using the camera, retrieve the storage permission + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startCamera(); + } else { + showPermissionsDialog(false, getString(R.string.picture_jurisdiction)); + } + break; + case PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE: + // Recording Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + startCustomCamera(); + } else { + showPermissionsDialog(false, getString(R.string.picture_audio)); + } + break; + } + } + + @Override + protected void showPermissionsDialog(boolean isCamera, String errorMsg) { + if (isFinishing()) { + return; + } + final PictureCustomDialog dialog = + new PictureCustomDialog(getContext(), R.layout.picture_wind_base_dialog); + dialog.setCancelable(false); + dialog.setCanceledOnTouchOutside(false); + Button btn_cancel = dialog.findViewById(R.id.btn_cancel); + Button btn_commit = dialog.findViewById(R.id.btn_commit); + btn_commit.setText(getString(R.string.picture_go_setting)); + TextView tvTitle = dialog.findViewById(R.id.tvTitle); + TextView tv_content = dialog.findViewById(R.id.tv_content); + tvTitle.setText(getString(R.string.picture_prompt)); + tv_content.setText(errorMsg); + btn_cancel.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + if (!isCamera) { + closeActivity(); + } + }); + btn_commit.setOnClickListener(v -> { + if (!isFinishing()) { + dialog.dismiss(); + } + PermissionChecker.launchAppDetailsSettings(getContext()); + isEnterSetting = true; + }); + dialog.show(); + } + + + /** + * set Data Null + * + * @param msg + */ + private void showDataNull(String msg, int topErrorResId) { + if (mTvEmpty.getVisibility() == View.GONE || mTvEmpty.getVisibility() == View.INVISIBLE) { + mTvEmpty.setCompoundDrawablesRelativeWithIntrinsicBounds(0, topErrorResId, 0, 0); + mTvEmpty.setText(msg); + mTvEmpty.setVisibility(View.VISIBLE); + } + } + + /** + * hidden + */ + private void hideDataNull() { + if (mTvEmpty.getVisibility() == View.VISIBLE) { + mTvEmpty.setVisibility(View.GONE); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorCameraEmptyActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorCameraEmptyActivity.java new file mode 100644 index 0000000..1d20dad --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorCameraEmptyActivity.java @@ -0,0 +1,395 @@ +package com.luck.picture.lib; + +import android.Manifest; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; +import android.os.Environment; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.immersive.ImmersiveManage; +import com.luck.picture.lib.permissions.PermissionChecker; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.BitmapUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.ValueOf; +import com.yalantis.ucrop.UCrop; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2019-11-15 21:41 + * @describe:PictureSelectorCameraEmptyActivity + */ +public class PictureSelectorCameraEmptyActivity extends PictureBaseActivity { + + @Override + public void immersive() { + ImmersiveManage.immersiveAboveAPI23(this + , ContextCompat.getColor(this, R.color.picture_color_transparent) + , ContextCompat.getColor(this, R.color.picture_color_transparent) + , openWhiteStatusBar); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + if (config == null) { + closeActivity(); + return; + } + if (!config.isUseCustomCamera) { + if (savedInstanceState == null) { + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE) && + PermissionChecker + .checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + + if (PictureSelectionConfig.onCustomCameraInterfaceListener != null) { + if (config.chooseMode == PictureConfig.TYPE_VIDEO) { + PictureSelectionConfig.onCustomCameraInterfaceListener.onCameraClick(getContext(), config, PictureConfig.TYPE_VIDEO); + } else { + PictureSelectionConfig.onCustomCameraInterfaceListener.onCameraClick(getContext(), config, PictureConfig.TYPE_IMAGE); + } + } else { + onTakePhoto(); + } + } else { + PermissionChecker.requestPermissions(this, new String[]{ + Manifest.permission.READ_EXTERNAL_STORAGE, + Manifest.permission.WRITE_EXTERNAL_STORAGE}, PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE); + } + } + setTheme(R.style.Picture_Theme_Translucent); + } + } + + + @Override + public int getResourceId() { + return R.layout.picture_empty; + } + + + /** + * open camera + */ + private void onTakePhoto() { + if (PermissionChecker + .checkSelfPermission(this, Manifest.permission.CAMERA)) { + boolean isPermissionChecker = true; + if (config != null && config.isUseCustomCamera) { + isPermissionChecker = PermissionChecker.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO); + } + if (isPermissionChecker) { + startCamera(); + } else { + PermissionChecker + .requestPermissions(this, + new String[]{Manifest.permission.RECORD_AUDIO}, PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE); + } + } else { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE); + } + } + + /** + * Open the Camera by type + */ + private void startCamera() { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + case PictureConfig.TYPE_IMAGE: + startOpenCamera(); + break; + case PictureConfig.TYPE_VIDEO: + startOpenCameraVideo(); + break; + case PictureConfig.TYPE_AUDIO: + startOpenCameraAudio(); + break; + default: + break; + } + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { + super.onActivityResult(requestCode, resultCode, data); + if (resultCode == RESULT_OK) { + switch (requestCode) { + case UCrop.REQUEST_CROP: + singleCropHandleResult(data); + break; + case PictureConfig.REQUEST_CAMERA: + dispatchHandleCamera(data); + break; + default: + break; + } + } else if (resultCode == RESULT_CANCELED) { + if (config != null && PictureSelectionConfig.listener != null) { + PictureSelectionConfig.listener.onCancel(); + } + closeActivity(); + } else if (resultCode == UCrop.RESULT_ERROR) { + if (data == null) { + return; + } + Throwable throwable = (Throwable) data.getSerializableExtra(UCrop.EXTRA_ERROR); + ToastUtils.s(getContext(), throwable.getMessage()); + } + } + + /** + * Single picture clipping callback + * + * @param data + */ + protected void singleCropHandleResult(Intent data) { + if (data == null) { + return; + } + List medias = new ArrayList<>(); + Uri resultUri = UCrop.getOutput(data); + if (resultUri == null) { + return; + } + String cutPath = resultUri.getPath(); + boolean isCutEmpty = TextUtils.isEmpty(cutPath); + LocalMedia media = new LocalMedia(config.cameraPath, 0, false, + config.isCamera ? 1 : 0, 0, config.chooseMode); + if (SdkVersionUtils.checkedAndroid_Q()) { + int lastIndexOf = config.cameraPath.lastIndexOf("/") + 1; + media.setId(lastIndexOf > 0 ? ValueOf.toLong(config.cameraPath.substring(lastIndexOf)) : -1); + media.setAndroidQToPath(cutPath); + if (isCutEmpty) { + if (PictureMimeType.isContent(config.cameraPath)) { + String path = PictureFileUtils.getPath(this, Uri.parse(config.cameraPath)); + media.setSize(!TextUtils.isEmpty(path) ? new File(path).length() : 0); + } else { + media.setSize(new File(config.cameraPath).length()); + } + } else { + media.setSize(new File(cutPath).length()); + } + } else { + // Taking a photo generates a temporary id + media.setId(System.currentTimeMillis()); + media.setSize(new File(isCutEmpty ? media.getPath() : cutPath).length()); + } + media.setCut(!isCutEmpty); + media.setCutPath(cutPath); + String mimeType = PictureMimeType.getImageMimeType(cutPath); + media.setMimeType(mimeType); + int width = 0, height = 0; + media.setOrientation(-1); + if (PictureMimeType.isContent(media.getPath())) { + if (PictureMimeType.isHasVideo(media.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUri(getContext(), Uri.parse(media.getPath())); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(media.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUri(getContext(), Uri.parse(media.getPath())); + width = size[0]; + height = size[1]; + } + } else { + if (PictureMimeType.isHasVideo(media.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUrl(media.getPath()); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(media.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUrl(media.getPath()); + width = size[0]; + height = size[1]; + } + } + media.setWidth(width); + media.setHeight(height); + // The width and height of the image are reversed if there is rotation information + MediaUtils.setOrientationAsynchronous(getContext(), media, config.isAndroidQChangeWH, config.isAndroidQChangeVideoWH, + item -> { + medias.add(item); + handlerResult(medias); + }); + } + + /** + * dispatchHandleCamera + * + * @param intent + */ + protected void dispatchHandleCamera(Intent intent) { + boolean isAudio = config.chooseMode == PictureMimeType.ofAudio(); + config.cameraPath = isAudio ? getAudioPath(intent) : config.cameraPath; + if (TextUtils.isEmpty(config.cameraPath)) { + return; + } + showPleaseDialog(); + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public LocalMedia doInBackground() { + LocalMedia media = new LocalMedia(); + String mimeType = isAudio ? PictureMimeType.MIME_TYPE_AUDIO : ""; + int[] newSize = new int[2]; + long duration = 0; + if (!isAudio) { + if (PictureMimeType.isContent(config.cameraPath)) { + // content: Processing rules + String path = PictureFileUtils.getPath(getContext(), Uri.parse(config.cameraPath)); + if (!TextUtils.isEmpty(path)) { + File cameraFile = new File(path); + mimeType = PictureMimeType.getMimeType(config.cameraMimeType); + media.setSize(cameraFile.length()); + } + if (PictureMimeType.isHasImage(mimeType)) { + newSize = MediaUtils.getImageSizeForUrlToAndroidQ(getContext(), config.cameraPath); + } else if (PictureMimeType.isHasVideo(mimeType)) { + newSize = MediaUtils.getVideoSizeForUri(getContext(), Uri.parse(config.cameraPath)); + duration = MediaUtils.extractDuration(getContext(), SdkVersionUtils.checkedAndroid_Q(), config.cameraPath); + } + int lastIndexOf = config.cameraPath.lastIndexOf("/") + 1; + media.setId(lastIndexOf > 0 ? ValueOf.toLong(config.cameraPath.substring(lastIndexOf)) : -1); + media.setRealPath(path); + // Custom photo has been in the application sandbox into the file + String mediaPath = intent != null ? intent.getStringExtra(PictureConfig.EXTRA_MEDIA_PATH) : null; + media.setAndroidQToPath(mediaPath); + } else { + File cameraFile = new File(config.cameraPath); + mimeType = PictureMimeType.getMimeType(config.cameraMimeType); + media.setSize(cameraFile.length()); + if (PictureMimeType.isHasImage(mimeType)) { + int degree = PictureFileUtils.readPictureDegree(getContext(), config.cameraPath); + BitmapUtils.rotateImage(degree, config.cameraPath); + newSize = MediaUtils.getImageSizeForUrl(config.cameraPath); + } else if (PictureMimeType.isHasVideo(mimeType)) { + newSize = MediaUtils.getVideoSizeForUrl(config.cameraPath); + duration = MediaUtils.extractDuration(getContext(), SdkVersionUtils.checkedAndroid_Q(), config.cameraPath); + } + // Taking a photo generates a temporary id + media.setId(System.currentTimeMillis()); + } + media.setPath(config.cameraPath); + media.setDuration(duration); + media.setMimeType(mimeType); + media.setWidth(newSize[0]); + media.setHeight(newSize[1]); + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isHasVideo(media.getMimeType())) { + media.setParentFolderName(Environment.DIRECTORY_MOVIES); + } else { + media.setParentFolderName(PictureMimeType.CAMERA); + } + media.setChooseModel(config.chooseMode); + long bucketId = MediaUtils.getCameraFirstBucketId(getContext()); + media.setBucketId(bucketId); + // The width and height of the image are reversed if there is rotation information + MediaUtils.setOrientationSynchronous(getContext(), media, config.isAndroidQChangeWH, config.isAndroidQChangeVideoWH); + } + return media; + } + + @Override + public void onSuccess(LocalMedia result) { + // Refresh the system library + dismissDialog(); + if (!SdkVersionUtils.checkedAndroid_Q()) { + if (config.isFallbackVersion3) { + new PictureMediaScannerConnection(getContext(), config.cameraPath); + } else { + sendBroadcast(new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, Uri.fromFile(new File(config.cameraPath)))); + } + } + dispatchCameraHandleResult(result); + // Solve some phone using Camera, DCIM will produce repetitive problems + if (!SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isHasImage(result.getMimeType())) { + int lastImageId = MediaUtils.getDCIMLastImageId(getContext()); + if (lastImageId != -1) { + MediaUtils.removeMedia(getContext(), lastImageId); + } + } + } + }); + } + + /** + * dispatchCameraHandleResult + * + * @param media + */ + private void dispatchCameraHandleResult(LocalMedia media) { + boolean isHasImage = PictureMimeType.isHasImage(media.getMimeType()); + if (config.enableCrop && isHasImage) { + config.originalPath = config.cameraPath; + startCrop(config.cameraPath, media.getMimeType()); + } else if (config.isCompress && isHasImage && !config.isCheckOriginalImage) { + List result = new ArrayList<>(); + result.add(media); + compressImage(result); + } else { + List result = new ArrayList<>(); + result.add(media); + onResult(result); + } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, + @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + switch (requestCode) { + case PictureConfig.APPLY_STORAGE_PERMISSIONS_CODE: + // Store Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + PermissionChecker.requestPermissions(this, + new String[]{Manifest.permission.CAMERA}, PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE); + } else { + ToastUtils.s(getContext(), getString(R.string.picture_jurisdiction)); + closeActivity(); + } + break; + case PictureConfig.APPLY_CAMERA_PERMISSIONS_CODE: + // Camera Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onTakePhoto(); + } else { + closeActivity(); + ToastUtils.s(getContext(), getString(R.string.picture_camera)); + } + break; + case PictureConfig.APPLY_RECORD_AUDIO_PERMISSIONS_CODE: + // Recording Permissions + if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) { + onTakePhoto(); + } else { + closeActivity(); + ToastUtils.s(getContext(), getString(R.string.picture_audio)); + } + break; + } + } + + + @Override + public void onBackPressed() { + super.onBackPressed(); + closeActivity(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorExternalUtils.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorExternalUtils.java new file mode 100644 index 0000000..1d3e71b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorExternalUtils.java @@ -0,0 +1,46 @@ +package com.luck.picture.lib; + +import android.content.Context; +import android.media.ExifInterface; +import android.net.Uri; + +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; + +import java.io.InputStream; + +/** + * @author:luck + * @date:2020-04-12 13:13 + * @describe:PictureSelector对外提供的一些方法 + */ +public class PictureSelectorExternalUtils { + /** + * 获取ExifInterface + * + * @param context + * @param url + * @return + */ + public static ExifInterface getExifInterface(Context context, String url) { + ExifInterface exifInterface = null; + InputStream inputStream = null; + try { + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(url)) { + inputStream = context.getContentResolver().openInputStream(Uri.parse(url)); + if (inputStream != null) { + exifInterface = new ExifInterface(inputStream); + } + } else { + exifInterface = new ExifInterface(url); + } + return exifInterface; + } catch (Exception e) { + e.printStackTrace(); + } finally { + PictureFileUtils.close(inputStream); + } + return null; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorPreviewWeChatStyleActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorPreviewWeChatStyleActivity.java new file mode 100644 index 0000000..0a76be6 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorPreviewWeChatStyleActivity.java @@ -0,0 +1,387 @@ +package com.luck.picture.lib; + +import android.text.TextUtils; +import android.view.View; +import android.view.animation.AccelerateInterpolator; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.adapter.PictureWeChatPreviewGalleryAdapter; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.decoration.GridSpacingItemDecoration; +import com.luck.picture.lib.decoration.WrapContentLinearLayoutManager; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.tools.ScreenUtils; + + +/** + * @author:luck + * @date:2019-11-30 17:16 + * @describe:PictureSelector WeChatStyle + */ +public class PictureSelectorPreviewWeChatStyleActivity extends PicturePreviewActivity { + /** + * alpha duration + */ + private final static int ALPHA_DURATION = 300; + private TextView mPictureSendView; + private RecyclerView mRvGallery; + private TextView tvSelected; + private View bottomLine; + private PictureWeChatPreviewGalleryAdapter mGalleryAdapter; + + @Override + public int getResourceId() { + return R.layout.picture_wechat_style_preview; + } + + private void goneParent() { + if (tvMediaNum.getVisibility() == View.VISIBLE) { + tvMediaNum.setVisibility(View.GONE); + } + if (mTvPictureOk.getVisibility() == View.VISIBLE) { + mTvPictureOk.setVisibility(View.GONE); + } + if (!TextUtils.isEmpty(check.getText())) { + check.setText(""); + } + } + + @Override + protected void initWidgets() { + super.initWidgets(); + goneParent(); + mRvGallery = findViewById(R.id.rv_gallery); + bottomLine = findViewById(R.id.bottomLine); + tvSelected = findViewById(R.id.tv_selected); + mPictureSendView = findViewById(R.id.picture_send); + mPictureSendView.setOnClickListener(this); + mPictureSendView.setText(getString(R.string.picture_send)); + mCbOriginal.setTextSize(16); + mGalleryAdapter = new PictureWeChatPreviewGalleryAdapter(config); + WrapContentLinearLayoutManager layoutManager = new WrapContentLinearLayoutManager(getContext()); + layoutManager.setOrientation(WrapContentLinearLayoutManager.HORIZONTAL); + mRvGallery.setLayoutManager(layoutManager); + mRvGallery.addItemDecoration(new GridSpacingItemDecoration(Integer.MAX_VALUE, + ScreenUtils.dip2px(this, 8), false)); + mRvGallery.setAdapter(mGalleryAdapter); + mGalleryAdapter.setItemClickListener((position, media, v) -> { + if (viewPager != null && media != null) { + if (isEqualsDirectory(media.getParentFolderName(), currentDirectory)) { + int newPosition = isBottomPreview ? position : isShowCamera ? media.position - 1 : media.position; + viewPager.setCurrentItem(newPosition); + } else { + // TODO The picture is not in the album directory, click invalid + } + } + }); + if (isBottomPreview) { + if (selectData != null && selectData.size() > position) { + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + media.setChecked(false); + } + LocalMedia media = selectData.get(position); + media.setChecked(true); + } + } else { + int size = selectData != null ? selectData.size() : 0; + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (isEqualsDirectory(media.getParentFolderName(), currentDirectory)) { + media.setChecked(isShowCamera ? media.position - 1 == position : media.position == position); + } + } + } + } + + /** + * Is it the same directory + * + * @param parentFolderName + * @param currentDirectory + * @return + */ + private boolean isEqualsDirectory(String parentFolderName, String currentDirectory) { + return isBottomPreview + || TextUtils.isEmpty(parentFolderName) + || TextUtils.isEmpty(currentDirectory) + || currentDirectory.equals(getString(R.string.picture_camera_roll)) + || parentFolderName.equals(currentDirectory); + } + + @Override + public void initPictureSelectorStyle() { + super.initPictureSelectorStyle(); + if (config.style != null) { + if (config.style.pictureCompleteBackgroundStyle != 0) { + mPictureSendView.setBackgroundResource(config.style.pictureCompleteBackgroundStyle); + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_bg); + } + if (config.style.pictureRightTextSize != 0) { + mPictureSendView.setTextSize(config.style.pictureRightTextSize); + } + if (!TextUtils.isEmpty(config.style.pictureWeChatPreviewSelectedText)) { + tvSelected.setText(config.style.pictureWeChatPreviewSelectedText); + } + if (config.style.pictureWeChatPreviewSelectedTextSize != 0) { + tvSelected.setTextSize(config.style.pictureWeChatPreviewSelectedTextSize); + } + if (config.style.picturePreviewBottomBgColor != 0) { + selectBarLayout.setBackgroundColor(config.style.picturePreviewBottomBgColor); + } else { + selectBarLayout.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.picture_color_half_grey)); + } + if (config.style.pictureCompleteTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureCompleteTextColor); + } else { + if (config.style.pictureCancelTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureCancelTextColor); + } else { + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + } + } + if (config.style.pictureOriginalFontColor == 0) { + mCbOriginal.setTextColor(ContextCompat + .getColor(this, R.color.picture_color_white)); + } + if (config.style.pictureWeChatChooseStyle != 0) { + check.setBackgroundResource(config.style.pictureWeChatChooseStyle); + } else { + check.setBackgroundResource(R.drawable.picture_wechat_select_cb); + } + if (config.isOriginalControl) { + if (config.style.pictureOriginalControlStyle == 0) { + mCbOriginal.setButtonDrawable(ContextCompat + .getDrawable(this, R.drawable.picture_original_wechat_checkbox)); + } + } + if (config.style.pictureWeChatLeftBackStyle != 0) { + pictureLeftBack.setImageResource(config.style.pictureWeChatLeftBackStyle); + } else { + pictureLeftBack.setImageResource(R.drawable.picture_icon_back); + } + if (!TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mPictureSendView.setText(config.style.pictureUnCompleteText); + } + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_bg); + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + selectBarLayout.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.picture_color_half_grey)); + check.setBackgroundResource(R.drawable.picture_wechat_select_cb); + pictureLeftBack.setImageResource(R.drawable.picture_icon_back); + mCbOriginal.setTextColor(ContextCompat + .getColor(this, R.color.picture_color_white)); + if (config.isOriginalControl) { + mCbOriginal.setButtonDrawable(ContextCompat + .getDrawable(this, R.drawable.picture_original_wechat_checkbox)); + } + } + + onSelectNumChange(false); + } + + @Override + public void onClick(View v) { + super.onClick(v); + int id = v.getId(); + if (id == R.id.picture_send) { + boolean enable = selectData.size() != 0; + if (enable) { + mTvPictureOk.performClick(); + } else { + btnCheck.performClick(); + boolean isNewEnableStatus = selectData.size() != 0; + if (isNewEnableStatus) { + mTvPictureOk.performClick(); + } + } + } + } + + @Override + protected void onUpdateSelectedChange(LocalMedia media) { + onChangeMediaStatus(media); + } + + @Override + protected void onSelectedChange(boolean isAddRemove, LocalMedia media) { + if (isAddRemove) { + media.setChecked(true); + if (config.selectionMode == PictureConfig.SINGLE) { + mGalleryAdapter.addSingleMediaToData(media); + } + } else { + media.setChecked(false); + mGalleryAdapter.removeMediaToData(media); + if (isBottomPreview) { + if (selectData != null && selectData.size() > position) { + selectData.get(position).setChecked(true); + } + if (mGalleryAdapter.isDataEmpty()) { + onActivityBackPressed(); + } else { + int currentItem = viewPager.getCurrentItem(); + adapter.remove(currentItem); + adapter.removeCacheView(currentItem); + position = currentItem; + tvTitle.setText(getString(R.string.picture_preview_image_num, + position + 1, adapter.getSize())); + check.setSelected(true); + adapter.notifyDataSetChanged(); + } + } + } + int itemCount = mGalleryAdapter.getItemCount(); + if (itemCount > 5) { + mRvGallery.smoothScrollToPosition(itemCount - 1); + } + } + + @Override + protected void onPageSelectedChange(LocalMedia media) { + super.onPageSelectedChange(media); + goneParent(); + if (!config.previewEggs) { + onChangeMediaStatus(media); + } + } + + /** + * onChangeMediaStatus + * + * @param media + */ + private void onChangeMediaStatus(LocalMedia media) { + if (mGalleryAdapter != null) { + int itemCount = mGalleryAdapter.getItemCount(); + if (itemCount > 0) { + boolean isChangeData = false; + for (int i = 0; i < itemCount; i++) { + LocalMedia item = mGalleryAdapter.getItem(i); + if (item == null || TextUtils.isEmpty(item.getPath())) { + continue; + } + boolean isOldChecked = item.isChecked(); + boolean isNewChecked = item.getPath().equals(media.getPath()) || item.getId() == media.getId(); + if (!isChangeData) { + isChangeData = (isOldChecked && !isNewChecked) || (!isOldChecked && isNewChecked); + } + item.setChecked(isNewChecked); + } + if (isChangeData) { + mGalleryAdapter.notifyDataSetChanged(); + } + } + } + } + + @Override + protected void onSelectNumChange(boolean isRefresh) { + if (mPictureSendView == null) { + return; + } + goneParent(); + boolean enable = selectData.size() != 0; + if (enable) { + initCompleteText(selectData.size()); + if (mRvGallery.getVisibility() == View.GONE) { + mRvGallery.animate().alpha(1).setDuration(ALPHA_DURATION).setInterpolator(new AccelerateInterpolator()); + mRvGallery.setVisibility(View.VISIBLE); + bottomLine.animate().alpha(1).setDuration(ALPHA_DURATION).setInterpolator(new AccelerateInterpolator()); + bottomLine.setVisibility(View.VISIBLE); + // 重置一片内存区域 不然在其他地方添加也影响这里的数量 + mGalleryAdapter.setNewData(selectData); + } + if (config.style != null) { + if (config.style.pictureCompleteTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureCompleteTextColor); + } + if (config.style.pictureCompleteBackgroundStyle != 0) { + mPictureSendView.setBackgroundResource(config.style.pictureCompleteBackgroundStyle); + } + } else { + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_bg); + } + } else { + if (config.style != null && !TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mPictureSendView.setText(config.style.pictureUnCompleteText); + } else { + mPictureSendView.setText(getString(R.string.picture_send)); + } + mRvGallery.animate().alpha(0).setDuration(ALPHA_DURATION).setInterpolator(new AccelerateInterpolator()); + mRvGallery.setVisibility(View.GONE); + bottomLine.animate().alpha(0).setDuration(ALPHA_DURATION).setInterpolator(new AccelerateInterpolator()); + bottomLine.setVisibility(View.GONE); + } + } + + + /** + * initCompleteText + */ + @Override + protected void initCompleteText(int startCount) { + boolean isNotEmptyStyle = config.style != null; + if (config.isWithVideoImage) { + // 混选模式 + if (config.selectionMode == PictureConfig.SINGLE) { + if (startCount <= 0) { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send)); + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, selectData.size(), 1)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_send)); + } + } + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, + selectData.size(), config.maxSelectNum)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send_num, selectData.size(), + config.maxSelectNum)); + } + } + } else { + String mimeType = selectData.get(0).getMimeType(); + int maxSize = PictureMimeType.isHasVideo(mimeType) && config.maxVideoSelectNum > 0 ? config.maxVideoSelectNum : config.maxSelectNum; + if (config.selectionMode == PictureConfig.SINGLE) { + if (startCount <= 0) { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send)); + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, selectData.size(), + 1)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_send)); + } + } + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, selectData.size(), maxSize)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText + : getString(R.string.picture_send_num, selectData.size(), maxSize)); + } + } + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorWeChatStyleActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorWeChatStyleActivity.java new file mode 100644 index 0000000..2ab1e2a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureSelectorWeChatStyleActivity.java @@ -0,0 +1,286 @@ +package com.luck.picture.lib; + + +import android.graphics.drawable.Drawable; +import android.text.TextUtils; +import android.view.View; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.tools.AttrsUtils; + +import java.util.List; + +/** + * @author:luck + * @date:2019-11-30 13:27 + * @describe:PictureSelector WeChatStyle + */ +public class PictureSelectorWeChatStyleActivity extends PictureSelectorActivity { + private TextView mPictureSendView; + private RelativeLayout rlAlbum; + + + @Override + public int getResourceId() { + return R.layout.picture_wechat_style_selector; + } + + @Override + protected void initWidgets() { + super.initWidgets(); + rlAlbum = findViewById(R.id.rlAlbum); + mPictureSendView = findViewById(R.id.picture_send); + mPictureSendView.setOnClickListener(this); + mPictureSendView.setText(getString(R.string.picture_send)); + mTvPicturePreview.setTextSize(16); + mCbOriginal.setTextSize(16); + boolean isChooseMode = config.selectionMode == + PictureConfig.SINGLE && config.isSingleDirectReturn; + mPictureSendView.setVisibility(isChooseMode ? View.GONE : View.VISIBLE); + if (rlAlbum.getLayoutParams() != null + && rlAlbum.getLayoutParams() instanceof RelativeLayout.LayoutParams) { + RelativeLayout.LayoutParams lp = (RelativeLayout.LayoutParams) rlAlbum.getLayoutParams(); + if (isChooseMode) { + lp.addRule(RelativeLayout.CENTER_HORIZONTAL); + } else { + lp.addRule(RelativeLayout.RIGHT_OF, R.id.pictureLeftBack); + } + } + } + + @Override + public void initPictureSelectorStyle() { + if (config.style != null) { + if (config.style.pictureUnCompleteBackgroundStyle != 0) { + mPictureSendView.setBackgroundResource(config.style.pictureUnCompleteBackgroundStyle); + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_default_bg); + } + if (config.style.pictureBottomBgColor != 0) { + mBottomLayout.setBackgroundColor(config.style.pictureBottomBgColor); + } else { + mBottomLayout.setBackgroundColor(ContextCompat.getColor(getContext(), R.color.picture_color_grey)); + } + if (config.style.pictureUnCompleteTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureUnCompleteTextColor); + } else { + if (config.style.pictureCancelTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureCancelTextColor); + } else { + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_53575e)); + } + } + if (config.style.pictureRightTextSize != 0) { + mPictureSendView.setTextSize(config.style.pictureRightTextSize); + } + if (config.style.pictureOriginalFontColor == 0) { + mCbOriginal.setTextColor(ContextCompat + .getColor(this, R.color.picture_color_white)); + } + if (config.isOriginalControl) { + if (config.style.pictureOriginalControlStyle == 0) { + mCbOriginal.setButtonDrawable(ContextCompat + .getDrawable(this, R.drawable.picture_original_wechat_checkbox)); + } + } + if (config.style.pictureContainerBackgroundColor != 0) { + container.setBackgroundColor(config.style.pictureContainerBackgroundColor); + } + + if (config.style.pictureWeChatTitleBackgroundStyle != 0) { + rlAlbum.setBackgroundResource(config.style.pictureWeChatTitleBackgroundStyle); + } else { + rlAlbum.setBackgroundResource(R.drawable.picture_album_bg); + } + + if (!TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mPictureSendView.setText(config.style.pictureUnCompleteText); + } + + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_default_bg); + rlAlbum.setBackgroundResource(R.drawable.picture_album_bg); + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_53575e)); + int pictureBottomBgColor = AttrsUtils.getTypeValueColor(getContext(), R.attr.picture_bottom_bg); + mBottomLayout.setBackgroundColor(pictureBottomBgColor != 0 + ? pictureBottomBgColor : ContextCompat.getColor(getContext(), R.color.picture_color_grey)); + + mCbOriginal.setTextColor(ContextCompat + .getColor(this, R.color.picture_color_white)); + Drawable drawable = ContextCompat.getDrawable(this, R.drawable.picture_icon_wechat_down); + mIvArrow.setImageDrawable(drawable); + if (config.isOriginalControl) { + mCbOriginal.setButtonDrawable(ContextCompat + .getDrawable(this, R.drawable.picture_original_wechat_checkbox)); + } + } + super.initPictureSelectorStyle(); + goneParentView(); + } + + /** + * Hide views that are not needed by the parent container + */ + private void goneParentView() { + mTvPictureRight.setVisibility(View.GONE); + mTvPictureImgNum.setVisibility(View.GONE); + mTvPictureOk.setVisibility(View.GONE); + } + + @Override + protected void changeImageNumber(List selectData) { + if (mPictureSendView == null) { + return; + } + int size = selectData.size(); + boolean enable = size != 0; + if (enable) { + mPictureSendView.setEnabled(true); + mPictureSendView.setSelected(true); + mTvPicturePreview.setEnabled(true); + mTvPicturePreview.setSelected(true); + initCompleteText(selectData); + if (config.style != null) { + if (config.style.pictureCompleteBackgroundStyle != 0) { + mPictureSendView.setBackgroundResource(config.style.pictureCompleteBackgroundStyle); + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_bg); + } + if (config.style.pictureCompleteTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureCompleteTextColor); + } else { + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + } + if (config.style.picturePreviewTextColor != 0) { + mTvPicturePreview.setTextColor(config.style.picturePreviewTextColor); + } else { + mTvPicturePreview.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + } + if (!TextUtils.isEmpty(config.style.picturePreviewText)) { + mTvPicturePreview.setText(config.style.picturePreviewText); + } else { + mTvPicturePreview.setText(getString(R.string.picture_preview_num, size)); + } + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_bg); + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + mTvPicturePreview.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_white)); + mTvPicturePreview.setText(getString(R.string.picture_preview_num, size)); + } + } else { + mPictureSendView.setEnabled(false); + mPictureSendView.setSelected(false); + mTvPicturePreview.setEnabled(false); + mTvPicturePreview.setSelected(false); + if (config.style != null) { + if (config.style.pictureUnCompleteBackgroundStyle != 0) { + mPictureSendView.setBackgroundResource(config.style.pictureUnCompleteBackgroundStyle); + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_default_bg); + } + if (config.style.pictureUnCompleteTextColor != 0) { + mPictureSendView.setTextColor(config.style.pictureUnCompleteTextColor); + } else { + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_53575e)); + } + if (config.style.pictureUnPreviewTextColor != 0) { + mTvPicturePreview.setTextColor(config.style.pictureUnPreviewTextColor); + } else { + mTvPicturePreview.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_9b)); + } + if (!TextUtils.isEmpty(config.style.pictureUnCompleteText)) { + mPictureSendView.setText(config.style.pictureUnCompleteText); + } else { + mPictureSendView.setText(getString(R.string.picture_send)); + } + if (!TextUtils.isEmpty(config.style.pictureUnPreviewText)) { + mTvPicturePreview.setText(config.style.pictureUnPreviewText); + } else { + mTvPicturePreview.setText(getString(R.string.picture_preview)); + } + } else { + mPictureSendView.setBackgroundResource(R.drawable.picture_send_button_default_bg); + mPictureSendView.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_53575e)); + mTvPicturePreview.setTextColor(ContextCompat.getColor(getContext(), R.color.picture_color_9b)); + mTvPicturePreview.setText(getString(R.string.picture_preview)); + mPictureSendView.setText(getString(R.string.picture_send)); + } + } + } + + @Override + public void onClick(View v) { + super.onClick(v); + int id = v.getId(); + if (id == R.id.picture_send) { + if (folderWindow != null + && folderWindow.isShowing()) { + folderWindow.dismiss(); + } else { + mTvPictureOk.performClick(); + } + } + } + + @Override + protected void onChangeData(List list) { + super.onChangeData(list); + initCompleteText(list); + } + + @Override + protected void initCompleteText(List list) { + int size = list.size(); + boolean isNotEmptyStyle = config.style != null; + if (config.isWithVideoImage) { + if (config.selectionMode == PictureConfig.SINGLE) { + if (size <= 0) { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send)); + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, size, 1)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_send)); + } + } + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, size, config.maxSelectNum)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send_num, size, config.maxSelectNum)); + } + } + } else { + String mimeType = list.get(0).getMimeType(); + int maxSize = PictureMimeType.isHasVideo(mimeType) && config.maxVideoSelectNum > 0 ? config.maxVideoSelectNum : config.maxSelectNum; + if (config.selectionMode == PictureConfig.SINGLE) { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, size, 1)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureCompleteText) + ? config.style.pictureCompleteText : getString(R.string.picture_send)); + } + } else { + boolean isCompleteReplaceNum = isNotEmptyStyle && config.style.isCompleteReplaceNum; + if (isCompleteReplaceNum && !TextUtils.isEmpty(config.style.pictureCompleteText)) { + mPictureSendView.setText(String.format(config.style.pictureCompleteText, size, maxSize)); + } else { + mPictureSendView.setText(isNotEmptyStyle && !TextUtils.isEmpty(config.style.pictureUnCompleteText) + ? config.style.pictureUnCompleteText : getString(R.string.picture_send_num, size, maxSize)); + } + } + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/PictureVideoPlayActivity.java b/picture_library/src/main/java/com/luck/picture/lib/PictureVideoPlayActivity.java new file mode 100644 index 0000000..01a752d --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/PictureVideoPlayActivity.java @@ -0,0 +1,218 @@ +package com.luck.picture.lib; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; +import android.graphics.Color; +import android.media.MediaPlayer; +import android.net.Uri; +import android.os.Bundle; +import android.os.Parcelable; +import android.text.TextUtils; +import android.view.View; +import android.view.WindowManager; +import android.widget.ImageButton; +import android.widget.ImageView; +import android.widget.MediaController; +import android.widget.TextView; +import android.widget.VideoView; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.tools.SdkVersionUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @data:2017/8/28 下午11:00 + * @描述: 视频播放类 + */ +public class PictureVideoPlayActivity extends PictureBaseActivity implements + MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener, + MediaPlayer.OnCompletionListener, View.OnClickListener { + private String videoPath; + private ImageButton ibLeftBack; + private MediaController mMediaController; + private VideoView mVideoView; + private TextView tvConfirm; + private ImageView iv_play; + private int mPositionWhenPaused = -1; + + @Override + public boolean isImmersive() { + return false; + } + + @Override + public boolean isRequestedOrientation() { + return false; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + super.onCreate(savedInstanceState); + } + + @Override + public int getResourceId() { + return R.layout.picture_activity_video_play; + } + + @Override + protected void initPictureSelectorStyle() { + if (config.style != null) { + if (config.style.pictureLeftBackIcon != 0) { + ibLeftBack.setImageResource(config.style.pictureLeftBackIcon); + } + } + } + + @Override + protected void initWidgets() { + super.initWidgets(); + videoPath = getIntent().getStringExtra(PictureConfig.EXTRA_VIDEO_PATH); + boolean isExternalPreview = getIntent().getBooleanExtra + (PictureConfig.EXTRA_PREVIEW_VIDEO, false); + if (TextUtils.isEmpty(videoPath)) { + LocalMedia media = getIntent().getParcelableExtra(PictureConfig.EXTRA_MEDIA_KEY); + if (media == null || TextUtils.isEmpty(media.getPath())) { + finish(); + return; + } + videoPath = media.getPath(); + } + if (TextUtils.isEmpty(videoPath)) { + closeActivity(); + return; + } + ibLeftBack = findViewById(R.id.pictureLeftBack); + mVideoView = findViewById(R.id.video_view); + tvConfirm = findViewById(R.id.tv_confirm); + mVideoView.setBackgroundColor(Color.BLACK); + iv_play = findViewById(R.id.iv_play); + mMediaController = new MediaController(this); + mVideoView.setOnCompletionListener(this); + mVideoView.setOnPreparedListener(this); + mVideoView.setMediaController(mMediaController); + ibLeftBack.setOnClickListener(this); + iv_play.setOnClickListener(this); + tvConfirm.setOnClickListener(this); + + tvConfirm.setVisibility(config.selectionMode + == PictureConfig.SINGLE + && config.enPreviewVideo && !isExternalPreview ? View.VISIBLE : View.GONE); + } + + @Override + public void onStart() { + // Play Video + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(videoPath)) { + mVideoView.setVideoURI(Uri.parse(videoPath)); + } else { + mVideoView.setVideoPath(videoPath); + } + mVideoView.start(); + super.onStart(); + } + + @Override + public void onPause() { + // Stop video when the activity is pause. + mPositionWhenPaused = mVideoView.getCurrentPosition(); + mVideoView.stopPlayback(); + + super.onPause(); + } + + @Override + protected void onDestroy() { + mMediaController = null; + mVideoView = null; + iv_play = null; + super.onDestroy(); + } + + @Override + public void onResume() { + // Resume video player + if (mPositionWhenPaused >= 0) { + mVideoView.seekTo(mPositionWhenPaused); + mPositionWhenPaused = -1; + } + + super.onResume(); + } + + @Override + public boolean onError(MediaPlayer player, int arg1, int arg2) { + return false; + } + + @Override + public void onCompletion(MediaPlayer mp) { + if (null != iv_play) { + iv_play.setVisibility(View.VISIBLE); + } + + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (id == R.id.pictureLeftBack) { + onBackPressed(); + } else if (id == R.id.iv_play) { + mVideoView.start(); + iv_play.setVisibility(View.INVISIBLE); + } else if (id == R.id.tv_confirm) { + List result = new ArrayList<>(); + result.add(getIntent().getParcelableExtra(PictureConfig.EXTRA_MEDIA_KEY)); + setResult(RESULT_OK, new Intent() + .putParcelableArrayListExtra(PictureConfig.EXTRA_SELECT_LIST, + (ArrayList) result)); + onBackPressed(); + } + } + + @Override + public void onBackPressed() { + if (config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewExitAnimation != 0) { + finish(); + overridePendingTransition(0, config.windowAnimationStyle != null + && config.windowAnimationStyle.activityPreviewExitAnimation != 0 ? + config.windowAnimationStyle.activityPreviewExitAnimation : R.anim.picture_anim_exit); + } else { + closeActivity(); + } + } + + @Override + protected void attachBaseContext(Context newBase) { + super.attachBaseContext(new ContextWrapper(newBase) { + @Override + public Object getSystemService(String name) { + if (Context.AUDIO_SERVICE.equals(name)) { + return getApplicationContext().getSystemService(name); + } + return super.getSystemService(name); + } + }); + } + + @Override + public void onPrepared(MediaPlayer mp) { + mp.setOnInfoListener((mp1, what, extra) -> { + if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) { + // video started + mVideoView.setBackgroundColor(Color.TRANSPARENT); + return true; + } + return false; + }); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureAlbumDirectoryAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureAlbumDirectoryAdapter.java new file mode 100644 index 0000000..c9b16aa --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureAlbumDirectoryAdapter.java @@ -0,0 +1,122 @@ +package com.luck.picture.lib.adapter; + +import android.content.Context; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.listener.OnAlbumItemClickListener; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2016-12-11 17:02 + * @describe:PictureAlbumDirectoryAdapter + */ +public class PictureAlbumDirectoryAdapter extends RecyclerView.Adapter { + private List folders = new ArrayList<>(); + private int chooseMode; + private PictureSelectionConfig config; + + public PictureAlbumDirectoryAdapter(PictureSelectionConfig config) { + super(); + this.config = config; + this.chooseMode = config.chooseMode; + } + + public void bindFolderData(List folders) { + this.folders = folders == null ? new ArrayList<>() : folders; + notifyDataSetChanged(); + } + + public void setChooseMode(int chooseMode) { + this.chooseMode = chooseMode; + } + + public List getFolderData() { + return folders == null ? new ArrayList<>() : folders; + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.picture_album_folder_item, parent, false); + return new ViewHolder(itemView); + } + + @Override + public void onBindViewHolder(final ViewHolder holder, int position) { + final LocalMediaFolder folder = folders.get(position); + String name = folder.getName(); + int imageNum = folder.getImageNum(); + String imagePath = folder.getFirstImagePath(); + boolean isChecked = folder.isChecked(); + int checkedNum = folder.getCheckedNum(); + holder.tvSign.setVisibility(checkedNum > 0 ? View.VISIBLE : View.INVISIBLE); + holder.itemView.setSelected(isChecked); + if (config.style != null && config.style.pictureAlbumStyle != 0) { + holder.itemView.setBackgroundResource(config.style.pictureAlbumStyle); + } + if (chooseMode == PictureMimeType.ofAudio()) { + holder.ivFirstImage.setImageResource(R.drawable.picture_audio_placeholder); + } else { + if (PictureSelectionConfig.imageEngine != null) { + PictureSelectionConfig.imageEngine.loadFolderImage(holder.itemView.getContext(), + imagePath, holder.ivFirstImage); + } + } + Context context = holder.itemView.getContext(); + String firstTitle = folder.getOfAllType() != -1 ? folder.getOfAllType() == PictureMimeType.ofAudio() ? + context.getString(R.string.picture_all_audio) + : context.getString(R.string.picture_camera_roll) : name; + holder.tvFolderName.setText(context.getString(R.string.picture_camera_roll_num, firstTitle, imageNum)); + holder.itemView.setOnClickListener(view -> { + if (onAlbumItemClickListener != null) { + int size = folders.size(); + for (int i = 0; i < size; i++) { + LocalMediaFolder mediaFolder = folders.get(i); + mediaFolder.setChecked(false); + } + folder.setChecked(true); + notifyDataSetChanged(); + onAlbumItemClickListener.onItemClick(position, folder.isCameraFolder(), folder.getBucketId(), folder.getName(), folder.getData()); + } + }); + } + + @Override + public int getItemCount() { + return folders.size(); + } + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivFirstImage; + TextView tvFolderName, tvSign; + + public ViewHolder(View itemView) { + super(itemView); + ivFirstImage = itemView.findViewById(R.id.first_image); + tvFolderName = itemView.findViewById(R.id.tv_folder_name); + tvSign = itemView.findViewById(R.id.tv_sign); + if (config.style != null && config.style.pictureFolderCheckedDotStyle != 0) { + tvSign.setBackgroundResource(config.style.pictureFolderCheckedDotStyle); + } + } + } + + private OnAlbumItemClickListener onAlbumItemClickListener; + + public void setOnAlbumItemClickListener(OnAlbumItemClickListener listener) { + this.onAlbumItemClickListener = listener; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureImageGridAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureImageGridAdapter.java new file mode 100644 index 0000000..0801be1 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureImageGridAdapter.java @@ -0,0 +1,682 @@ +package com.luck.picture.lib.adapter; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.graphics.PorterDuff; +import android.net.Uri; +import android.text.TextUtils; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.AnimationUtils; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.dialog.PictureCustomDialog; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnPhotoSelectChangedListener; +import com.luck.picture.lib.tools.AnimUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.StringUtils; +import com.luck.picture.lib.tools.ToastUtils; +import com.luck.picture.lib.tools.VoiceUtils; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + + +/** + * @author:luck + * @date:2016-12-30 12:02 + * @describe:PictureImageGridAdapter + */ +public class PictureImageGridAdapter extends RecyclerView.Adapter { + + private Context context; + private boolean showCamera; + private OnPhotoSelectChangedListener imageSelectChangedListener; + private List data = new ArrayList<>(); + private List selectData = new ArrayList<>(); + private PictureSelectionConfig config; + + public PictureImageGridAdapter(Context context, PictureSelectionConfig config) { + this.context = context; + this.config = config; + this.showCamera = config.isCamera; + } + + public void setShowCamera(boolean showCamera) { + this.showCamera = showCamera; + } + + public boolean isShowCamera() { + return showCamera; + } + + /** + * 全量刷新 + * + * @param data + */ + public void bindData(List data) { + this.data = data == null ? new ArrayList<>() : data; + this.notifyDataSetChanged(); + } + + + public void bindSelectData(List images) { + // 这里重新构构造一个新集合,不然会产生已选集合一变,结果集合也会添加的问题 + List selection = new ArrayList<>(); + int size = images.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = images.get(i); + selection.add(media); + } + this.selectData = selection; + if (!config.isSingleDirectReturn) { + subSelectPosition(); + if (imageSelectChangedListener != null) { + imageSelectChangedListener.onChange(selectData); + } + } + } + + public List getSelectedData() { + return selectData == null ? new ArrayList<>() : selectData; + } + + public int getSelectedSize() { + return selectData == null ? 0 : selectData.size(); + } + + public List getData() { + return data == null ? new ArrayList<>() : data; + } + + public boolean isDataEmpty() { + return data == null || data.size() == 0; + } + + public void clear() { + if (getSize() > 0) { + data.clear(); + } + } + + public int getSize() { + return data == null ? 0 : data.size(); + } + + public LocalMedia getItem(int position) { + return getSize() > 0 ? data.get(position) : null; + } + + @Override + public int getItemViewType(int position) { + if (showCamera && position == 0) { + return PictureConfig.TYPE_CAMERA; + } else { + return PictureConfig.TYPE_PICTURE; + } + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + if (viewType == PictureConfig.TYPE_CAMERA) { + View view = LayoutInflater.from(context).inflate(R.layout.picture_item_camera, parent, false); + return new CameraViewHolder(view); + } else { + View view = LayoutInflater.from(context).inflate(R.layout.picture_image_grid_item, parent, false); + return new ViewHolder(view); + } + } + + @Override + public void onBindViewHolder(@NotNull final RecyclerView.ViewHolder holder, final int position) { + if (getItemViewType(position) == PictureConfig.TYPE_CAMERA) { + CameraViewHolder headerHolder = (CameraViewHolder) holder; + headerHolder.headerView.setOnClickListener(v -> { + if (imageSelectChangedListener != null) { + imageSelectChangedListener.onTakePhoto(); + } + }); + } else { + final ViewHolder contentHolder = (ViewHolder) holder; + final LocalMedia image = data.get(showCamera ? position - 1 : position); + image.position = contentHolder.getAdapterPosition(); + final String path = image.getPath(); + final String mimeType = image.getMimeType(); + if (config.checkNumMode) { + notifyCheckChanged(contentHolder, image); + } + if (config.isSingleDirectReturn) { + contentHolder.tvCheck.setVisibility(View.GONE); + contentHolder.btnCheck.setVisibility(View.GONE); + } else { + selectImage(contentHolder, isSelected(image)); + contentHolder.tvCheck.setVisibility(View.VISIBLE); + contentHolder.btnCheck.setVisibility(View.VISIBLE); + // 启用了蒙层效果 + if (config.isMaxSelectEnabledMask) { + dispatchHandleMask(contentHolder, image); + } + } + contentHolder.tvIsGif.setVisibility(PictureMimeType.isGif(mimeType) ? View.VISIBLE : View.GONE); + if (PictureMimeType.isHasImage(image.getMimeType())) { + if (image.loadLongImageStatus == PictureConfig.NORMAL) { + image.isLongImage = MediaUtils.isLongImg(image); + image.loadLongImageStatus = PictureConfig.LOADED; + } + contentHolder.tvLongChart.setVisibility(image.isLongImage ? View.VISIBLE : View.GONE); + } else { + image.loadLongImageStatus = PictureConfig.NORMAL; + contentHolder.tvLongChart.setVisibility(View.GONE); + } + boolean isHasVideo = PictureMimeType.isHasVideo(mimeType); + if (isHasVideo || PictureMimeType.isHasAudio(mimeType)) { + contentHolder.tvDuration.setVisibility(View.VISIBLE); + contentHolder.tvDuration.setText(DateUtils.formatDurationTime(image.getDuration())); + contentHolder.tvDuration.setCompoundDrawablesRelativeWithIntrinsicBounds + (isHasVideo ? R.drawable.picture_icon_video : R.drawable.picture_icon_audio, + 0, 0, 0); + } else { + contentHolder.tvDuration.setVisibility(View.GONE); + } + if (config.chooseMode == PictureMimeType.ofAudio()) { + contentHolder.ivPicture.setImageResource(R.drawable.picture_audio_placeholder); + } else { + if (PictureSelectionConfig.imageEngine != null) { + PictureSelectionConfig.imageEngine.loadGridImage(context, path, contentHolder.ivPicture); + } + } + + if (config.enablePreview || config.enPreviewVideo || config.enablePreviewAudio) { + contentHolder.btnCheck.setOnClickListener(v -> { + if (config.isMaxSelectEnabledMask) { + if (!contentHolder.tvCheck.isSelected() && getSelectedSize() >= config.maxSelectNum) { + String msg = StringUtils.getMsg(context, config.chooseMode == PictureMimeType.ofAll() ? null : image.getMimeType(), config.maxSelectNum); + showPromptDialog(msg); + return; + } + } + // If the original path does not exist or the path does exist but the file does not exist + String newPath = image.getRealPath(); + if (!TextUtils.isEmpty(newPath) && !new File(newPath).exists()) { + ToastUtils.s(context, PictureMimeType.s(context, mimeType)); + return; + } + // The width and height of the image are reversed if there is rotation information + MediaUtils.setOrientationAsynchronous(context, image, config.isAndroidQChangeWH, config.isAndroidQChangeVideoWH, null); + changeCheckboxState(contentHolder, image); + }); + } + contentHolder.contentView.setOnClickListener(v -> { + if (config.isMaxSelectEnabledMask) { + if (image.isMaxSelectEnabledMask()) { + return; + } + } + // If the original path does not exist or the path does exist but the file does not exist + String newPath = image.getRealPath(); + if (!TextUtils.isEmpty(newPath) && !new File(newPath).exists()) { + ToastUtils.s(context, PictureMimeType.s(context, mimeType)); + return; + } + int index = showCamera ? position - 1 : position; + if (index == -1) { + return; + } + // The width and height of the image are reversed if there is rotation information + MediaUtils.setOrientationAsynchronous(context, image, config.isAndroidQChangeWH, config.isAndroidQChangeVideoWH, null); + boolean eqResult = + PictureMimeType.isHasImage(mimeType) && config.enablePreview + || PictureMimeType.isHasVideo(mimeType) && (config.enPreviewVideo + || config.selectionMode == PictureConfig.SINGLE) + || PictureMimeType.isHasAudio(mimeType) && (config.enablePreviewAudio + || config.selectionMode == PictureConfig.SINGLE); + if (eqResult) { + if (PictureMimeType.isHasVideo(image.getMimeType())) { + if (config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + // The video is less than the minimum specified length + showPromptDialog(context.getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + if (config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + // The length of the video exceeds the specified length + showPromptDialog(context.getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } + imageSelectChangedListener.onPictureClick(image, index); + } else { + changeCheckboxState(contentHolder, image); + } + }); + } + } + + /** + * Handle mask effects + * + * @param contentHolder + * @param item + */ + private void dispatchHandleMask(ViewHolder contentHolder, LocalMedia item) { + if (config.isWithVideoImage && config.maxVideoSelectNum > 0) { + if (getSelectedSize() >= config.maxSelectNum) { + boolean isSelected = contentHolder.tvCheck.isSelected(); + contentHolder.ivPicture.setColorFilter(ContextCompat.getColor + (context, isSelected ? R.color.picture_color_80 : R.color.picture_color_half_white), PorterDuff.Mode.SRC_ATOP); + item.setMaxSelectEnabledMask(!isSelected); + } else { + item.setMaxSelectEnabledMask(false); + } + } else { + LocalMedia media = selectData.size() > 0 ? selectData.get(0) : null; + if (media != null) { + boolean isSelected = contentHolder.tvCheck.isSelected(); + if (config.chooseMode == PictureMimeType.ofAll()) { + if (PictureMimeType.isHasImage(media.getMimeType())) { + // All videos are not optional + if (!isSelected && !PictureMimeType.isHasImage(item.getMimeType())) { + contentHolder.ivPicture.setColorFilter(ContextCompat.getColor + (context, PictureMimeType.isHasVideo(item.getMimeType()) ? R.color.picture_color_half_white : R.color.picture_color_20), PorterDuff.Mode.SRC_ATOP); + } + item.setMaxSelectEnabledMask(PictureMimeType.isHasVideo(item.getMimeType())); + } else if (PictureMimeType.isHasVideo(media.getMimeType())) { + // All images are not optional + if (!isSelected && !PictureMimeType.isHasVideo(item.getMimeType())) { + contentHolder.ivPicture.setColorFilter(ContextCompat.getColor + (context, PictureMimeType.isHasImage(item.getMimeType()) ? R.color.picture_color_half_white : R.color.picture_color_20), PorterDuff.Mode.SRC_ATOP); + } + item.setMaxSelectEnabledMask(PictureMimeType.isHasImage(item.getMimeType())); + } + } else { + if (config.chooseMode == PictureMimeType.ofVideo() && config.maxVideoSelectNum > 0) { + if (!isSelected && getSelectedSize() == config.maxVideoSelectNum) { + contentHolder.ivPicture.setColorFilter(ContextCompat.getColor + (context, R.color.picture_color_half_white), PorterDuff.Mode.SRC_ATOP); + } + item.setMaxSelectEnabledMask(!isSelected && getSelectedSize() == config.maxVideoSelectNum); + } else { + if (!isSelected && getSelectedSize() == config.maxSelectNum) { + contentHolder.ivPicture.setColorFilter(ContextCompat.getColor + (context, R.color.picture_color_half_white), PorterDuff.Mode.SRC_ATOP); + } + item.setMaxSelectEnabledMask(!isSelected && getSelectedSize() == config.maxSelectNum); + } + } + } + } + } + + + @Override + public int getItemCount() { + return showCamera ? data.size() + 1 : data.size(); + } + + public class CameraViewHolder extends RecyclerView.ViewHolder { + View headerView; + TextView tvCamera; + + public CameraViewHolder(View itemView) { + super(itemView); + headerView = itemView; + tvCamera = itemView.findViewById(R.id.tvCamera); + String title = config.chooseMode == PictureMimeType.ofAudio() ? + context.getString(R.string.picture_tape) + : context.getString(R.string.picture_take_picture); + tvCamera.setText(title); + } + } + + public class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivPicture; + TextView tvCheck; + TextView tvDuration, tvIsGif, tvLongChart; + View contentView; + View btnCheck; + + public ViewHolder(View itemView) { + super(itemView); + contentView = itemView; + ivPicture = itemView.findViewById(R.id.ivPicture); + tvCheck = itemView.findViewById(R.id.tvCheck); + btnCheck = itemView.findViewById(R.id.btnCheck); + tvDuration = itemView.findViewById(R.id.tv_duration); + tvIsGif = itemView.findViewById(R.id.tv_isGif); + tvLongChart = itemView.findViewById(R.id.tv_long_chart); + if (config.style != null) { + if (config.style.pictureCheckedStyle != 0) { + tvCheck.setBackgroundResource(config.style.pictureCheckedStyle); + } + } + } + } + + public boolean isSelected(LocalMedia image) { + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (media == null || TextUtils.isEmpty(media.getPath())) { + continue; + } + if (media.getPath() + .equals(image.getPath()) + || media.getId() == image.getId()) { + return true; + } + } + return false; + } + + /** + * Update button status + */ + private void notifyCheckChanged(ViewHolder viewHolder, LocalMedia imageBean) { + viewHolder.tvCheck.setText(""); + int size = selectData.size(); + for (int i = 0; i < size; i++) { + LocalMedia media = selectData.get(i); + if (media.getPath().equals(imageBean.getPath()) + || media.getId() == imageBean.getId()) { + imageBean.setNum(media.getNum()); + media.setPosition(imageBean.getPosition()); + viewHolder.tvCheck.setText(String.valueOf(imageBean.getNum())); + } + } + } + + /** + * Update the selected status of the image + * + * @param contentHolder + * @param image + */ + + @SuppressLint("StringFormatMatches") + private void changeCheckboxState(ViewHolder contentHolder, LocalMedia image) { + boolean isChecked = contentHolder.tvCheck.isSelected(); + int count = selectData.size(); + String mimeType = count > 0 ? selectData.get(0).getMimeType() : ""; + if (config.isWithVideoImage) { + // isWithVideoImage mode + int videoSize = 0; + for (int i = 0; i < count; i++) { + LocalMedia media = selectData.get(i); + if (PictureMimeType.isHasVideo(media.getMimeType())) { + videoSize++; + } + } + + if (PictureMimeType.isHasVideo(image.getMimeType())) { + if (config.maxVideoSelectNum <= 0) { + showPromptDialog(context.getString(R.string.picture_rule)); + return; + } + + if (getSelectedSize() >= config.maxSelectNum && !isChecked) { + showPromptDialog(context.getString(R.string.picture_message_max_num, config.maxSelectNum)); + return; + } + + if (videoSize >= config.maxVideoSelectNum && !isChecked) { + showPromptDialog(StringUtils.getMsg(context, image.getMimeType(), config.maxVideoSelectNum)); + return; + } + + if (!isChecked && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + showPromptDialog(context.getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!isChecked && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + showPromptDialog(context.getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } + + if (PictureMimeType.isHasImage(image.getMimeType())) { + if (getSelectedSize() >= config.maxSelectNum && !isChecked) { + showPromptDialog(context.getString(R.string.picture_message_max_num, config.maxSelectNum)); + return; + } + } + + } else { + if (!TextUtils.isEmpty(mimeType)) { + boolean mimeTypeSame = PictureMimeType.isMimeTypeSame(mimeType, image.getMimeType()); + if (!mimeTypeSame) { + showPromptDialog(context.getString(R.string.picture_rule)); + return; + } + } + if (PictureMimeType.isHasVideo(mimeType) && config.maxVideoSelectNum > 0) { + if (count >= config.maxVideoSelectNum && !isChecked) { + showPromptDialog(StringUtils.getMsg(context, mimeType, config.maxVideoSelectNum)); + return; + } + if (!isChecked && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + showPromptDialog(context.getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!isChecked && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + showPromptDialog(context.getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } else { + if (count >= config.maxSelectNum && !isChecked) { + showPromptDialog(StringUtils.getMsg(context, mimeType, config.maxSelectNum)); + return; + } + if (PictureMimeType.isHasVideo(image.getMimeType())) { + if (!isChecked && config.videoMinSecond > 0 && image.getDuration() < config.videoMinSecond) { + showPromptDialog(context.getString(R.string.picture_choose_min_seconds, config.videoMinSecond / 1000)); + return; + } + + if (!isChecked && config.videoMaxSecond > 0 && image.getDuration() > config.videoMaxSecond) { + showPromptDialog(context.getString(R.string.picture_choose_max_seconds, config.videoMaxSecond / 1000)); + return; + } + } + } + } + + if (isChecked) { + for (int i = 0; i < count; i++) { + LocalMedia media = selectData.get(i); + if (media == null || TextUtils.isEmpty(media.getPath())) { + continue; + } + if (media.getPath().equals(image.getPath()) + || media.getId() == image.getId()) { + selectData.remove(media); + subSelectPosition(); + AnimUtils.disZoom(contentHolder.ivPicture, config.zoomAnim); + break; + } + } + } else { + // The radio + if (config.selectionMode == PictureConfig.SINGLE) { + singleRadioMediaImage(); + } + + // If the width and height are 0, regain the width and height + if (image.getWidth() == 0 || image.getHeight() == 0) { + int width = 0, height = 0; + image.setOrientation(-1); + if (PictureMimeType.isContent(image.getPath())) { + if (PictureMimeType.isHasVideo(image.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUri(context, Uri.parse(image.getPath())); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(image.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUri(context, Uri.parse(image.getPath())); + width = size[0]; + height = size[1]; + } + } else { + if (PictureMimeType.isHasVideo(image.getMimeType())) { + int[] size = MediaUtils.getVideoSizeForUrl(image.getPath()); + width = size[0]; + height = size[1]; + } else if (PictureMimeType.isHasImage(image.getMimeType())) { + int[] size = MediaUtils.getImageSizeForUrl(image.getPath()); + width = size[0]; + height = size[1]; + } + } + image.setWidth(width); + image.setHeight(height); + } + + selectData.add(image); + image.setNum(selectData.size()); + VoiceUtils.getInstance().play(); + AnimUtils.zoom(contentHolder.ivPicture, config.zoomAnim); + contentHolder.tvCheck.startAnimation(AnimationUtils.loadAnimation(context, R.anim.picture_anim_modal_in)); + } + + boolean isRefreshAll = false; + if (config.isMaxSelectEnabledMask) { + if (config.chooseMode == PictureMimeType.ofAll()) { + // ofAll + if (config.isWithVideoImage && config.maxVideoSelectNum > 0) { + if (getSelectedSize() >= config.maxSelectNum) { + isRefreshAll = true; + } + if (isChecked) { + // delete + if (getSelectedSize() == config.maxSelectNum - 1) { + isRefreshAll = true; + } + } + } else { + if (!isChecked && getSelectedSize() == 1) { + // add + isRefreshAll = true; + } + if (isChecked && getSelectedSize() == 0) { + // delete + isRefreshAll = true; + } + } + } else { + // ofImage or ofVideo or ofAudio + if (config.chooseMode == PictureMimeType.ofVideo() && config.maxVideoSelectNum > 0) { + if (!isChecked && getSelectedSize() == config.maxVideoSelectNum) { + // add + isRefreshAll = true; + } + if (isChecked && getSelectedSize() == config.maxVideoSelectNum - 1) { + // delete + isRefreshAll = true; + } + } else { + if (!isChecked && getSelectedSize() == config.maxSelectNum) { + // add + isRefreshAll = true; + } + if (isChecked && getSelectedSize() == config.maxSelectNum - 1) { + // delete + isRefreshAll = true; + } + } + } + } + + if (isRefreshAll) { + notifyDataSetChanged(); + } else { + notifyItemChanged(contentHolder.getAdapterPosition()); + } + + selectImage(contentHolder, !isChecked); + if (imageSelectChangedListener != null) { + imageSelectChangedListener.onChange(selectData); + } + } + + /** + * Radio mode + */ + private void singleRadioMediaImage() { + if (selectData != null + && selectData.size() > 0) { + LocalMedia media = selectData.get(0); + notifyItemChanged(media.position); + selectData.clear(); + } + } + + /** + * Update the selection order + */ + private void subSelectPosition() { + if (config.checkNumMode) { + int size = selectData.size(); + for (int index = 0; index < size; index++) { + LocalMedia media = selectData.get(index); + media.setNum(index + 1); + notifyItemChanged(media.position); + } + } + } + + /** + * Select the image and animate it + * + * @param holder + * @param isChecked + */ + public void selectImage(ViewHolder holder, boolean isChecked) { + holder.tvCheck.setSelected(isChecked); + if (isChecked) { + holder.ivPicture.setColorFilter(ContextCompat.getColor + (context, R.color.picture_color_80), PorterDuff.Mode.SRC_ATOP); + } else { + holder.ivPicture.setColorFilter(ContextCompat.getColor + (context, R.color.picture_color_20), PorterDuff.Mode.SRC_ATOP); + } + } + + /** + * Tips + */ + private void showPromptDialog(String content) { + PictureCustomDialog dialog = new PictureCustomDialog(context, R.layout.picture_prompt_dialog); + TextView btnOk = dialog.findViewById(R.id.btnOk); + TextView tvContent = dialog.findViewById(R.id.tv_content); + tvContent.setText(content); + btnOk.setOnClickListener(v -> dialog.dismiss()); + dialog.show(); + } + + + /** + * Binding listener + * + * @param imageSelectChangedListener + */ + public void setOnPhotoSelectChangedListener(OnPhotoSelectChangedListener + imageSelectChangedListener) { + this.imageSelectChangedListener = imageSelectChangedListener; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureSimpleFragmentAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureSimpleFragmentAdapter.java new file mode 100644 index 0000000..beda694 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureSimpleFragmentAdapter.java @@ -0,0 +1,225 @@ +package com.luck.picture.lib.adapter; + +import android.content.Intent; +import android.graphics.PointF; +import android.net.Uri; +import android.os.Bundle; +import android.util.SparseArray; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.viewpager.widget.PagerAdapter; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.photoview.PhotoView; +import com.luck.picture.lib.tools.JumpUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.widget.longimage.ImageSource; +import com.luck.picture.lib.widget.longimage.ImageViewState; +import com.luck.picture.lib.widget.longimage.SubsamplingScaleImageView; + +import org.jetbrains.annotations.NotNull; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @data:2018/1/27 下午7:50 + * @describe:PictureSimpleFragmentAdapter + */ + +public class PictureSimpleFragmentAdapter extends PagerAdapter { + private List data; + private OnCallBackActivity onBackPressed; + private PictureSelectionConfig config; + /** + * Maximum number of cached images + */ + private static final int MAX_CACHE_SIZE = 20; + /** + * To cache the view + */ + private SparseArray mCacheView; + + public void clear() { + if (null != mCacheView) { + mCacheView.clear(); + mCacheView = null; + } + } + + public void removeCacheView(int position) { + if (mCacheView != null && position < mCacheView.size()) { + mCacheView.removeAt(position); + } + } + + public interface OnCallBackActivity { + /** + * Close Activity + */ + void onActivityBackPressed(); + } + + public PictureSimpleFragmentAdapter(PictureSelectionConfig config, + OnCallBackActivity onBackPressed) { + super(); + this.config = config; + this.onBackPressed = onBackPressed; + this.mCacheView = new SparseArray<>(); + } + + /** + * bind data + * + * @param data + */ + public void bindData(List data) { + this.data = data; + } + + /** + * get data + * + * @return + */ + public List getData() { + return data == null ? new ArrayList<>() : data; + } + + public int getSize() { + return data == null ? 0 : data.size(); + } + + public void remove(int currentItem) { + if (getSize() > currentItem) { + data.remove(currentItem); + } + } + + public LocalMedia getItem(int position) { + return getSize() > 0 && position < getSize() ? data.get(position) : null; + } + + @Override + public int getCount() { + return data != null ? data.size() : 0; + } + + @Override + public void destroyItem(ViewGroup container, int position, Object object) { + (container).removeView((View) object); + if (mCacheView.size() > MAX_CACHE_SIZE) { + mCacheView.remove(position); + } + } + + @Override + public int getItemPosition(@NonNull Object object) { + return POSITION_NONE; + } + + @Override + public boolean isViewFromObject(@NotNull View view, @NotNull Object object) { + return view == object; + } + + @NotNull + @Override + public Object instantiateItem(@NotNull ViewGroup container, int position) { + View contentView = mCacheView.get(position); + if (contentView == null) { + contentView = LayoutInflater.from(container.getContext()) + .inflate(R.layout.picture_image_preview, container, false); + mCacheView.put(position, contentView); + } + PhotoView imageView = contentView.findViewById(R.id.preview_image); + SubsamplingScaleImageView longImg = contentView.findViewById(R.id.longImg); + ImageView ivPlay = contentView.findViewById(R.id.iv_play); + LocalMedia media = getItem(position); + if (media != null) { + final String mimeType = media.getMimeType(); + final String path; + if (media.isCut() && !media.isCompressed()) { + path = media.getCutPath(); + } else if (media.isCompressed() || (media.isCut() && media.isCompressed())) { + path = media.getCompressPath(); + } else { + path = media.getPath(); + } + boolean isGif = PictureMimeType.isGif(mimeType); + boolean isHasVideo = PictureMimeType.isHasVideo(mimeType); + ivPlay.setVisibility(isHasVideo ? View.VISIBLE : View.GONE); + ivPlay.setOnClickListener(v -> { + if (PictureSelectionConfig.customVideoPlayCallback != null) { + PictureSelectionConfig.customVideoPlayCallback.startPlayVideo(media); + } else { + Intent intent = new Intent(); + Bundle bundle = new Bundle(); + bundle.putBoolean(PictureConfig.EXTRA_PREVIEW_VIDEO, true); + bundle.putString(PictureConfig.EXTRA_VIDEO_PATH, path); + intent.putExtras(bundle); + JumpUtils.startPictureVideoPlayActivity(container.getContext(), bundle, PictureConfig.PREVIEW_VIDEO_CODE); + } + }); + boolean eqLongImg = MediaUtils.isLongImg(media); + imageView.setVisibility(eqLongImg && !isGif ? View.GONE : View.VISIBLE); + imageView.setOnViewTapListener((view, x, y) -> { + if (onBackPressed != null) { + onBackPressed.onActivityBackPressed(); + } + }); + longImg.setVisibility(eqLongImg && !isGif ? View.VISIBLE : View.GONE); + longImg.setOnClickListener(v -> { + if (onBackPressed != null) { + onBackPressed.onActivityBackPressed(); + } + }); + + if (isGif && !media.isCompressed()) { + if (config != null && PictureSelectionConfig.imageEngine != null) { + PictureSelectionConfig.imageEngine.loadAsGifImage + (contentView.getContext(), path, imageView); + } + } else { + if (config != null && PictureSelectionConfig.imageEngine != null) { + if (eqLongImg) { + displayLongPic(PictureMimeType.isContent(path) + ? Uri.parse(path) : Uri.fromFile(new File(path)), longImg); + } else { + PictureSelectionConfig.imageEngine.loadImage + (contentView.getContext(), path, imageView); + } + } + } + } + + (container).addView(contentView, 0); + return contentView; + } + + /** + * load long image + * + * @param uri + * @param longImg + */ + private void displayLongPic(Uri uri, SubsamplingScaleImageView longImg) { + longImg.setQuickScaleEnabled(true); + longImg.setZoomEnabled(true); + longImg.setPanEnabled(true); + longImg.setDoubleTapZoomDuration(100); + longImg.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_CROP); + longImg.setDoubleTapZoomDpi(SubsamplingScaleImageView.ZOOM_FOCUS_CENTER); + longImg.setImage(ImageSource.uri(uri), new ImageViewState(0, new PointF(0, 0), 0)); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureWeChatPreviewGalleryAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureWeChatPreviewGalleryAdapter.java new file mode 100644 index 0000000..99b9ec1 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/adapter/PictureWeChatPreviewGalleryAdapter.java @@ -0,0 +1,114 @@ +package com.luck.picture.lib.adapter; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2019-11-30 20:50 + * @describe:WeChat style selected after image preview + */ +public class PictureWeChatPreviewGalleryAdapter + extends RecyclerView.Adapter { + private List list; + private PictureSelectionConfig config; + + public PictureWeChatPreviewGalleryAdapter(PictureSelectionConfig config) { + super(); + this.config = config; + } + + public void setNewData(List data) { + this.list = data == null ? new ArrayList<>() : data; + notifyDataSetChanged(); + } + + public void addSingleMediaToData(LocalMedia media) { + if (this.list != null) { + list.clear(); + list.add(media); + notifyDataSetChanged(); + } + } + + public void removeMediaToData(LocalMedia media) { + if (this.list != null && this.list.size() > 0) { + this.list.remove(media); + notifyDataSetChanged(); + } + } + + public boolean isDataEmpty() { + return list == null || list.size() == 0; + } + + @NonNull + @Override + public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View itemView = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.picture_wechat_preview_gallery, parent, false); + return new ViewHolder(itemView); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + LocalMedia item = getItem(position); + if (item != null) { + holder.viewBorder.setVisibility(item.isChecked() ? View.VISIBLE : View.GONE); + if (config != null && PictureSelectionConfig.imageEngine != null) { + PictureSelectionConfig.imageEngine.loadImage(holder.itemView.getContext(), item.getPath(), holder.ivImage); + } + holder.ivPlay.setVisibility(PictureMimeType.isHasVideo(item.getMimeType()) ? View.VISIBLE : View.GONE); + holder.itemView.setOnClickListener(v -> { + if (listener != null && holder.getAdapterPosition() >= 0) { + listener.onItemClick(holder.getAdapterPosition(), getItem(position), v); + } + }); + } + } + + public LocalMedia getItem(int position) { + return list != null && list.size() > 0 ? list.get(position) : null; + } + + class ViewHolder extends RecyclerView.ViewHolder { + ImageView ivImage; + ImageView ivPlay; + View viewBorder; + + public ViewHolder(View itemView) { + super(itemView); + ivImage = itemView.findViewById(R.id.ivImage); + ivPlay = itemView.findViewById(R.id.ivPlay); + viewBorder = itemView.findViewById(R.id.viewBorder); + } + } + + private OnItemClickListener listener; + + public void setItemClickListener(OnItemClickListener listener) { + this.listener = listener; + } + + public interface OnItemClickListener { + void onItemClick(int position, LocalMedia media, View v); + } + + @Override + public int getItemCount() { + return list != null ? list.size() : 0; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/animators/AlphaInAnimationAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/animators/AlphaInAnimationAdapter.java new file mode 100644 index 0000000..51aff95 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/animators/AlphaInAnimationAdapter.java @@ -0,0 +1,31 @@ +package com.luck.picture.lib.animators; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * @author:luck + * @date:2020-04-18 14:11 + * @describe:AlphaInAnimationAdapter + */ +public class AlphaInAnimationAdapter extends BaseAnimationAdapter { + private static final float DEFAULT_ALPHA_FROM = 0f; + private final float mFrom; + + public AlphaInAnimationAdapter(RecyclerView.Adapter adapter) { + this(adapter, DEFAULT_ALPHA_FROM); + } + + public AlphaInAnimationAdapter(RecyclerView.Adapter adapter, float from) { + super(adapter); + mFrom = from; + } + + @Override + protected Animator[] getAnimators(View view) { + return new Animator[]{ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)}; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/animators/AnimationType.java b/picture_library/src/main/java/com/luck/picture/lib/animators/AnimationType.java new file mode 100644 index 0000000..9489bde --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/animators/AnimationType.java @@ -0,0 +1,23 @@ +package com.luck.picture.lib.animators; + +/** + * @author:luck + * @date:2020-04-18 15:21 + * @describe:AnimationType + */ +public class AnimationType { + + /** + * default animation + */ + public final static int DEFAULT_ANIMATION = -1; + + /** + * alpha animation + */ + public final static int ALPHA_IN_ANIMATION = 1; + /** + * slide in bottom animation + */ + public final static int SLIDE_IN_BOTTOM_ANIMATION = 2; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/animators/BaseAnimationAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/animators/BaseAnimationAdapter.java new file mode 100644 index 0000000..72413d8 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/animators/BaseAnimationAdapter.java @@ -0,0 +1,127 @@ +package com.luck.picture.lib.animators; + +import android.animation.Animator; +import android.view.View; +import android.view.ViewGroup; +import android.view.animation.Interpolator; +import android.view.animation.LinearInterpolator; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * @author:luck + * @date:2020-04-18 14:12 + * @describe:BaseAnimationAdapter + */ +public abstract class BaseAnimationAdapter extends RecyclerView.Adapter { + private RecyclerView.Adapter mAdapter; + private int mDuration = 250; + private Interpolator mInterpolator = new LinearInterpolator(); + private int mLastPosition = -1; + + private boolean isFirstOnly = true; + + public BaseAnimationAdapter(RecyclerView.Adapter adapter) { + mAdapter = adapter; + } + + @Override + public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return mAdapter.onCreateViewHolder(parent, viewType); + } + + @Override + public void registerAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.registerAdapterDataObserver(observer); + mAdapter.registerAdapterDataObserver(observer); + } + + @Override + public void unregisterAdapterDataObserver(RecyclerView.AdapterDataObserver observer) { + super.unregisterAdapterDataObserver(observer); + mAdapter.unregisterAdapterDataObserver(observer); + } + + @Override + public void onAttachedToRecyclerView(RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + mAdapter.onAttachedToRecyclerView(recyclerView); + } + + @Override + public void onDetachedFromRecyclerView(RecyclerView recyclerView) { + super.onDetachedFromRecyclerView(recyclerView); + mAdapter.onDetachedFromRecyclerView(recyclerView); + } + + @Override + public void onViewAttachedToWindow(RecyclerView.ViewHolder holder) { + super.onViewAttachedToWindow(holder); + mAdapter.onViewAttachedToWindow(holder); + } + + @Override + public void onViewDetachedFromWindow(RecyclerView.ViewHolder holder) { + super.onViewDetachedFromWindow(holder); + mAdapter.onViewDetachedFromWindow(holder); + } + + @Override + public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { + mAdapter.onBindViewHolder(holder, position); + + int adapterPosition = holder.getAdapterPosition(); + if (!isFirstOnly || adapterPosition > mLastPosition) { + for (Animator anim : getAnimators(holder.itemView)) { + anim.setDuration(mDuration).start(); + anim.setInterpolator(mInterpolator); + } + mLastPosition = adapterPosition; + } else { + ViewHelper.clear(holder.itemView); + } + } + + @Override + public void onViewRecycled(RecyclerView.ViewHolder holder) { + mAdapter.onViewRecycled(holder); + super.onViewRecycled(holder); + } + + @Override + public int getItemCount() { + return mAdapter.getItemCount(); + } + + public void setDuration(int duration) { + mDuration = duration; + } + + public void setInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setStartPosition(int start) { + mLastPosition = start; + } + + protected abstract Animator[] getAnimators(View view); + + public void setFirstOnly(boolean firstOnly) { + isFirstOnly = firstOnly; + } + + @Override + public int getItemViewType(int position) { + return mAdapter.getItemViewType(position); + } + + public RecyclerView.Adapter getWrappedAdapter() { + return mAdapter; + } + + @Override + public long getItemId(int position) { + return mAdapter.getItemId(position); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/animators/SlideInBottomAnimationAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/animators/SlideInBottomAnimationAdapter.java new file mode 100644 index 0000000..525be63 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/animators/SlideInBottomAnimationAdapter.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.animators; + +import android.animation.Animator; +import android.animation.ObjectAnimator; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * @author:luck + * @date:2020-04-18 14:19 + * @describe:SlideInBottomAnimationAdapter + */ +public class SlideInBottomAnimationAdapter extends BaseAnimationAdapter { + + public SlideInBottomAnimationAdapter(RecyclerView.Adapter adapter) { + super(adapter); + } + + @Override + protected Animator[] getAnimators(View view) { + return new Animator[]{ + ObjectAnimator.ofFloat(view, "translationY", view.getMeasuredHeight(), 0) + }; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/animators/ViewHelper.java b/picture_library/src/main/java/com/luck/picture/lib/animators/ViewHelper.java new file mode 100644 index 0000000..02c65de --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/animators/ViewHelper.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.animators; + +import android.view.View; + +import androidx.core.view.ViewCompat; + +/** + * @author:luck + * @date:2020-04-18 14:13 + * @describe:ViewHelper + */ +public final class ViewHelper { + public static void clear(View v) { + v.setAlpha(1); + v.setScaleY(1); + v.setScaleX(1); + v.setTranslationY(0); + v.setTranslationX(0); + v.setRotation(0); + v.setRotationY(0); + v.setRotationX(0); + v.setPivotY(v.getMeasuredHeight() / 2); + v.setPivotX(v.getMeasuredWidth() / 2); + ViewCompat.animate(v).setInterpolator(null).setStartDelay(0); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/app/IApp.java b/picture_library/src/main/java/com/luck/picture/lib/app/IApp.java new file mode 100644 index 0000000..56ec605 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/app/IApp.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.app; + +import android.content.Context; + +import com.luck.picture.lib.engine.PictureSelectorEngine; + +/** + * @author:luck + * @date:2019-12-03 15:14 + * @describe:IApp + */ +public interface IApp { + /** + * Application + * + * @return + */ + Context getAppContext(); + + /** + * PictureSelectorEngine + * + * @return + */ + PictureSelectorEngine getPictureSelectorEngine(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/app/PictureAppMaster.java b/picture_library/src/main/java/com/luck/picture/lib/app/PictureAppMaster.java new file mode 100644 index 0000000..0109a82 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/app/PictureAppMaster.java @@ -0,0 +1,56 @@ +package com.luck.picture.lib.app; + +import android.content.Context; + +import com.luck.picture.lib.engine.PictureSelectorEngine; + +/** + * @author:luck + * @date:2019-12-03 15:12 + * @describe:PictureAppMaster + */ +public class PictureAppMaster implements IApp { + + + @Override + public Context getAppContext() { + if (app == null) { + return null; + } + return app.getAppContext(); + } + + @Override + public PictureSelectorEngine getPictureSelectorEngine() { + if (app == null) { + return null; + } + return app.getPictureSelectorEngine(); + } + + private PictureAppMaster() { + } + + private static PictureAppMaster mInstance; + + public static PictureAppMaster getInstance() { + if (mInstance == null) { + synchronized (PictureAppMaster.class) { + if (mInstance == null) { + mInstance = new PictureAppMaster(); + } + } + } + return mInstance; + } + + private IApp app; + + public void setApp(IApp app) { + this.app = app; + } + + public IApp getApp() { + return app; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastAction.java b/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastAction.java new file mode 100644 index 0000000..57f1d52 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastAction.java @@ -0,0 +1,14 @@ +package com.luck.picture.lib.broadcast; + +/** + * @author:luck + * @date:2019-11-20 14:01 + * @describe:广播Action + */ +public class BroadcastAction { + + /** + * 外部预览界面删除图片Action + */ + public static final String ACTION_DELETE_PREVIEW_POSITION = "com.luck.picture.lib.action.delete_preview_position"; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastManager.java b/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastManager.java new file mode 100644 index 0000000..6e34eb8 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/broadcast/BroadcastManager.java @@ -0,0 +1,250 @@ +package com.luck.picture.lib.broadcast; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.Bundle; +import android.os.Parcelable; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.localbroadcastmanager.content.LocalBroadcastManager; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * @author:luck + * @date:2019-11-20 13:45 + * @describe:本地广播 + */ +public class BroadcastManager { + private static final String TAG = BroadcastManager.class.getSimpleName(); + private LocalBroadcastManager localBroadcastManager; + + private Intent intent; + private String action; + + public static BroadcastManager getInstance(Context ctx) { + BroadcastManager broadcastManager = new BroadcastManager(); + broadcastManager.localBroadcastManager = LocalBroadcastManager.getInstance(ctx.getApplicationContext()); + return broadcastManager; + } + + + public BroadcastManager intent(Intent intent) { + this.intent = intent; + return this; + } + + + public BroadcastManager action(String action) { + this.action = action; + return this; + } + + public BroadcastManager extras(Bundle bundle) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + intent.putExtras(bundle); + return this; + } + + + public BroadcastManager put(String key, ArrayList value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, Parcelable[] value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + + public BroadcastManager put(String key, Parcelable value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, float value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, double value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, long value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, boolean value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + public BroadcastManager put(String key, int value) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, value); + return this; + } + + + public BroadcastManager put(String key, String str) { + createIntent(); + + if (intent == null) { + Log.e(TAG, "intent create failed"); + return this; + } + + intent.putExtra(key, str); + return this; + } + + private void createIntent() { + if (intent == null) { + Log.d(TAG, "intent is not created"); + } + + if (intent == null) { + if (!TextUtils.isEmpty(action)) { + intent = new Intent(action); + } + Log.d(TAG, "intent created with action"); + } + } + + + public void broadcast() { + + createIntent(); + + if (intent == null) { + return; + } + + if (action == null) { + return; + } + + intent.setAction(action); + + if (null != localBroadcastManager) { + localBroadcastManager.sendBroadcast(intent); + } + } + + public void registerReceiver(BroadcastReceiver br, List actions) { + if (null == br || null == actions) { + return; + } + IntentFilter iFilter = new IntentFilter(); + if (actions != null) { + for (String action : actions) { + iFilter.addAction(action); + } + } + if (null != localBroadcastManager) { + localBroadcastManager.registerReceiver(br, iFilter); + } + } + + + public void registerReceiver(BroadcastReceiver br, String... actions) { + if (actions == null || actions.length <= 0) { + return; + } + registerReceiver(br, Arrays.asList(actions)); + } + + + /** + * @param br + */ + public void unregisterReceiver(BroadcastReceiver br) { + if (null == br) { + return; + } + + try { + localBroadcastManager.unregisterReceiver(br); + } catch (Exception e) { + + } + } + + /** + * @param br + * @param actions 至少传入一个 + */ + public void unregisterReceiver(BroadcastReceiver br, @NonNull String... actions) { + unregisterReceiver(br); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/CheckPermission.java b/picture_library/src/main/java/com/luck/picture/lib/camera/CheckPermission.java new file mode 100644 index 0000000..705028f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/CheckPermission.java @@ -0,0 +1,79 @@ +package com.luck.picture.lib.camera; + +import android.media.AudioFormat; +import android.media.AudioRecord; +import android.media.MediaRecorder; + +/** + * ===================================== + * 作 者: 陈嘉桐 + * 版 本:1.1.4 + * 创建日期:2017/6/8 + * 描 述: + * ===================================== + */ +public class CheckPermission { + public static final int STATE_RECORDING = -1; + public static final int STATE_NO_PERMISSION = -2; + public static final int STATE_SUCCESS = 1; + + /** + * 用于检测是否具有录音权限 + * + * @return + */ + public static int getRecordState() { + int minBuffer = AudioRecord.getMinBufferSize(44100, AudioFormat.CHANNEL_IN_MONO, AudioFormat + .ENCODING_PCM_16BIT); + AudioRecord audioRecord = new AudioRecord(MediaRecorder.AudioSource.DEFAULT, 44100, AudioFormat + .CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, (minBuffer * 100)); + short[] point = new short[minBuffer]; + int readSize = 0; + try { + + audioRecord.startRecording();//检测是否可以进入初始化状态 + } catch (Exception e) { + if (audioRecord != null) { + audioRecord.release(); + audioRecord = null; + } + return STATE_NO_PERMISSION; + } + if (audioRecord.getRecordingState() != AudioRecord.RECORDSTATE_RECORDING) { + //6.0以下机型都会返回此状态,故使用时需要判断bulid版本 + //检测是否在录音中 + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + } + return STATE_RECORDING; + } else { + //检测是否可以获取录音结果 + + readSize = audioRecord.read(point, 0, point.length); + + + if (readSize <= 0) { + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + + } + return STATE_NO_PERMISSION; + + } else { + if (audioRecord != null) { + audioRecord.stop(); + audioRecord.release(); + audioRecord = null; + + } + + return STATE_SUCCESS; + } + } + } + +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/CustomCameraView.java b/picture_library/src/main/java/com/luck/picture/lib/camera/CustomCameraView.java new file mode 100644 index 0000000..933583f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/CustomCameraView.java @@ -0,0 +1,547 @@ +package com.luck.picture.lib.camera; + +import android.content.Context; +import android.graphics.SurfaceTexture; +import android.media.MediaPlayer; +import android.net.Uri; +import android.text.TextUtils; +import android.util.AttributeSet; +import android.view.LayoutInflater; +import android.view.Surface; +import android.view.TextureView; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.RelativeLayout; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.camera.core.ImageCapture; +import androidx.camera.core.ImageCaptureException; +import androidx.camera.core.VideoCapture; +import androidx.camera.view.CameraView; +import androidx.core.content.ContextCompat; +import androidx.lifecycle.LifecycleEventObserver; +import androidx.lifecycle.LifecycleOwner; + +import com.luck.picture.lib.PictureMediaScannerConnection; +import com.luck.picture.lib.R; +import com.luck.picture.lib.camera.listener.CameraListener; +import com.luck.picture.lib.camera.listener.CaptureListener; +import com.luck.picture.lib.camera.listener.ClickListener; +import com.luck.picture.lib.camera.listener.ImageCallbackListener; +import com.luck.picture.lib.camera.listener.TypeListener; +import com.luck.picture.lib.camera.view.CaptureLayout; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.AndroidQTransformUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.MediaUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.StringUtils; + +import java.io.File; +import java.io.IOException; +import java.lang.ref.WeakReference; + +/** + * @author:luck + * @date:2020-01-04 13:41 + * @describe:自定义相机View + */ +public class CustomCameraView extends RelativeLayout { + /** + * 只能拍照 + */ + public static final int BUTTON_STATE_ONLY_CAPTURE = 0x101; + /** + * 只能录像 + */ + public static final int BUTTON_STATE_ONLY_RECORDER = 0x102; + /** + * 两者都可以 + */ + public static final int BUTTON_STATE_BOTH = 0x103; + /** + * 闪关灯状态 + */ + private static final int TYPE_FLASH_AUTO = 0x021; + private static final int TYPE_FLASH_ON = 0x022; + private static final int TYPE_FLASH_OFF = 0x023; + private int type_flash = TYPE_FLASH_OFF; + private PictureSelectionConfig mConfig; + /** + * 回调监听 + */ + private CameraListener mCameraListener; + private ClickListener mOnClickListener; + private ImageCallbackListener mImageCallbackListener; + private androidx.camera.view.CameraView mCameraView; + private ImageView mImagePreview; + private ImageView mSwitchCamera; + private ImageView mFlashLamp; + private CaptureLayout mCaptureLayout; + private MediaPlayer mMediaPlayer; + private TextureView mTextureView; + private long recordTime = 0; + private File mVideoFile; + private File mPhotoFile; + + public CustomCameraView(Context context) { + this(context, null); + } + + public CustomCameraView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CustomCameraView(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + initView(); + } + + public void initView() { + setWillNotDraw(false); + setBackgroundColor(ContextCompat.getColor(getContext(), R.color.picture_color_black)); + View view = LayoutInflater.from(getContext()).inflate(R.layout.picture_camera_view, this); + mCameraView = view.findViewById(R.id.cameraView); + mCameraView.enableTorch(true); + mTextureView = view.findViewById(R.id.video_play_preview); + mImagePreview = view.findViewById(R.id.image_preview); + mSwitchCamera = view.findViewById(R.id.image_switch); + mSwitchCamera.setImageResource(R.drawable.picture_ic_camera); + mFlashLamp = view.findViewById(R.id.image_flash); + setFlashRes(); + mFlashLamp.setOnClickListener(v -> { + type_flash++; + if (type_flash > 0x023) + type_flash = TYPE_FLASH_AUTO; + setFlashRes(); + }); + mCaptureLayout = view.findViewById(R.id.capture_layout); + mCaptureLayout.setDuration(15 * 1000); + //切换摄像头 + mSwitchCamera.setOnClickListener(v -> mCameraView.toggleCamera()); + //拍照 录像 + mCaptureLayout.setCaptureListener(new CaptureListener() { + @Override + public void takePictures() { + mSwitchCamera.setVisibility(INVISIBLE); + mFlashLamp.setVisibility(INVISIBLE); + mCameraView.setCaptureMode(androidx.camera.view.CameraView.CaptureMode.IMAGE); + File imageOutFile = createImageFile(); + if (imageOutFile == null) { + return; + } + mPhotoFile = imageOutFile; + mCameraView.takePicture(imageOutFile, ContextCompat.getMainExecutor(getContext()), + new MyImageResultCallback(getContext(), mConfig, imageOutFile, + mImagePreview, mCaptureLayout, mImageCallbackListener, mCameraListener)); + } + + @Override + public void recordStart() { + mSwitchCamera.setVisibility(INVISIBLE); + mFlashLamp.setVisibility(INVISIBLE); + mCameraView.setCaptureMode(androidx.camera.view.CameraView.CaptureMode.VIDEO); + mCameraView.startRecording(createVideoFile(), ContextCompat.getMainExecutor(getContext()), + new VideoCapture.OnVideoSavedCallback() { + @Override + public void onVideoSaved(@NonNull File file) { + mVideoFile = file; + if (recordTime < 1500 && mVideoFile.exists() && mVideoFile.delete()) { + return; + } + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(mConfig.cameraPath)) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public Boolean doInBackground() { + return AndroidQTransformUtils.copyPathToDCIM(getContext(), + file, Uri.parse(mConfig.cameraPath)); + } + + @Override + public void onSuccess(Boolean result) { + PictureThreadUtils.cancel(PictureThreadUtils.getIoPool()); + } + }); + } + mTextureView.setVisibility(View.VISIBLE); + mCameraView.setVisibility(View.INVISIBLE); + if (mTextureView.isAvailable()) { + startVideoPlay(mVideoFile); + } else { + mTextureView.setSurfaceTextureListener(surfaceTextureListener); + } + } + + @Override + public void onError(int videoCaptureError, @NonNull String message, @Nullable Throwable cause) { + if (mCameraListener != null) { + mCameraListener.onError(videoCaptureError, message, cause); + } + } + }); + } + + @Override + public void recordShort(final long time) { + recordTime = time; + mSwitchCamera.setVisibility(VISIBLE); + mFlashLamp.setVisibility(VISIBLE); + mCaptureLayout.resetCaptureLayout(); + mCaptureLayout.setTextWithAnimation(getContext().getString(R.string.picture_recording_time_is_short)); + mCameraView.stopRecording(); + } + + @Override + public void recordEnd(long time) { + recordTime = time; + mCameraView.stopRecording(); + } + + @Override + public void recordZoom(float zoom) { + + } + + @Override + public void recordError() { + if (mCameraListener != null) { + mCameraListener.onError(0, "An unknown error", null); + } + } + }); + //确认 取消 + mCaptureLayout.setTypeListener(new TypeListener() { + @Override + public void cancel() { + stopVideoPlay(); + resetState(); + } + + @Override + public void confirm() { + if (mCameraView.getCaptureMode() == androidx.camera.view.CameraView.CaptureMode.VIDEO) { + if (mVideoFile == null) { + return; + } + stopVideoPlay(); + if (mCameraListener != null || !mVideoFile.exists()) { + mCameraListener.onRecordSuccess(mVideoFile); + } + } else { + if (mPhotoFile == null || !mPhotoFile.exists()) { + return; + } + mImagePreview.setVisibility(INVISIBLE); + if (mCameraListener != null) { + mCameraListener.onPictureSuccess(mPhotoFile); + } + } + } + }); + mCaptureLayout.setLeftClickListener(() -> { + if (mOnClickListener != null) { + mOnClickListener.onClick(); + } + }); + } + + /** + * 拍照回调 + */ + private static class MyImageResultCallback implements ImageCapture.OnImageSavedCallback { + private WeakReference mContextReference; + private WeakReference mConfigReference; + private WeakReference mFileReference; + private WeakReference mImagePreviewReference; + private WeakReference mCaptureLayoutReference; + private WeakReference mImageCallbackListenerReference; + private WeakReference mCameraListenerReference; + + public MyImageResultCallback(Context context, PictureSelectionConfig config, + File imageOutFile, ImageView imagePreview, + CaptureLayout captureLayout, ImageCallbackListener imageCallbackListener, + CameraListener cameraListener) { + super(); + this.mContextReference = new WeakReference<>(context); + this.mConfigReference = new WeakReference<>(config); + this.mFileReference = new WeakReference<>(imageOutFile); + this.mImagePreviewReference = new WeakReference<>(imagePreview); + this.mCaptureLayoutReference = new WeakReference<>(captureLayout); + this.mImageCallbackListenerReference = new WeakReference<>(imageCallbackListener); + this.mCameraListenerReference = new WeakReference<>(cameraListener); + } + + @Override + public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) { + if (mConfigReference.get() != null) { + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(mConfigReference.get().cameraPath)) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public Boolean doInBackground() { + return AndroidQTransformUtils.copyPathToDCIM(mContextReference.get(), + mFileReference.get(), Uri.parse(mConfigReference.get().cameraPath)); + } + + @Override + public void onSuccess(Boolean result) { + PictureThreadUtils.cancel(PictureThreadUtils.getIoPool()); + } + }); + } + } + if (mImageCallbackListenerReference.get() != null + && mFileReference.get() != null + && mImagePreviewReference.get() != null) { + mImageCallbackListenerReference.get().onLoadImage(mFileReference.get(), mImagePreviewReference.get()); + } + if (mImagePreviewReference.get() != null) { + mImagePreviewReference.get().setVisibility(View.VISIBLE); + } + if (mCaptureLayoutReference.get() != null) { + mCaptureLayoutReference.get().startTypeBtnAnimator(); + } + } + + @Override + public void onError(@NonNull ImageCaptureException exception) { + if (mCameraListenerReference.get() != null) { + mCameraListenerReference.get().onError(exception.getImageCaptureError(), exception.getMessage(), exception.getCause()); + } + } + } + + private TextureView.SurfaceTextureListener surfaceTextureListener = new TextureView.SurfaceTextureListener() { + @Override + public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) { + startVideoPlay(mVideoFile); + } + + @Override + public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { + + } + + @Override + public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { + return false; + } + + @Override + public void onSurfaceTextureUpdated(SurfaceTexture surface) { + + } + }; + + public File createImageFile() { + if (SdkVersionUtils.checkedAndroid_Q()) { + String diskCacheDir = PictureFileUtils.getDiskCacheDir(getContext()); + File rootDir = new File(diskCacheDir); + if (!rootDir.exists() && rootDir.mkdirs()) { + } + boolean isOutFileNameEmpty = TextUtils.isEmpty(mConfig.cameraFileName); + String suffix = TextUtils.isEmpty(mConfig.suffixType) ? PictureFileUtils.POSTFIX : mConfig.suffixType; + String newFileImageName = isOutFileNameEmpty ? DateUtils.getCreateFileName("IMG_") + suffix : mConfig.cameraFileName; + File cameraFile = new File(rootDir, newFileImageName); + Uri outUri = getOutUri(PictureMimeType.ofImage()); + if (outUri != null) { + mConfig.cameraPath = outUri.toString(); + } + return cameraFile; + } else { + String cameraFileName = ""; + if (!TextUtils.isEmpty(mConfig.cameraFileName)) { + boolean isSuffixOfImage = PictureMimeType.isSuffixOfImage(mConfig.cameraFileName); + mConfig.cameraFileName = !isSuffixOfImage ? StringUtils.renameSuffix(mConfig.cameraFileName, PictureMimeType.JPEG) : mConfig.cameraFileName; + cameraFileName = mConfig.camera ? mConfig.cameraFileName : StringUtils.rename(mConfig.cameraFileName); + } + File cameraFile = PictureFileUtils.createCameraFile(getContext(), + PictureMimeType.ofImage(), cameraFileName, mConfig.suffixType, mConfig.outPutCameraPath); + if (cameraFile != null) { + mConfig.cameraPath = cameraFile.getAbsolutePath(); + } + return cameraFile; + } + } + + public File createVideoFile() { + if (SdkVersionUtils.checkedAndroid_Q()) { + String diskCacheDir = PictureFileUtils.getVideoDiskCacheDir(getContext()); + File rootDir = new File(diskCacheDir); + if (!rootDir.exists() && rootDir.mkdirs()) { + } + boolean isOutFileNameEmpty = TextUtils.isEmpty(mConfig.cameraFileName); + String suffix = TextUtils.isEmpty(mConfig.suffixType) ? PictureMimeType.MP4 : mConfig.suffixType; + String newFileImageName = isOutFileNameEmpty ? DateUtils.getCreateFileName("VID_") + suffix : mConfig.cameraFileName; + File cameraFile = new File(rootDir, newFileImageName); + Uri outUri = getOutUri(PictureMimeType.ofVideo()); + if (outUri != null) { + mConfig.cameraPath = outUri.toString(); + } + return cameraFile; + } else { + String cameraFileName = ""; + if (!TextUtils.isEmpty(mConfig.cameraFileName)) { + boolean isSuffixOfImage = PictureMimeType.isSuffixOfImage(mConfig.cameraFileName); + mConfig.cameraFileName = !isSuffixOfImage ? StringUtils + .renameSuffix(mConfig.cameraFileName, PictureMimeType.MP4) : mConfig.cameraFileName; + cameraFileName = mConfig.camera ? mConfig.cameraFileName : StringUtils.rename(mConfig.cameraFileName); + } + File cameraFile = PictureFileUtils.createCameraFile(getContext(), + PictureMimeType.ofVideo(), cameraFileName, mConfig.suffixType, mConfig.outPutCameraPath); + mConfig.cameraPath = cameraFile.getAbsolutePath(); + return cameraFile; + } + } + + private Uri getOutUri(int type) { + return type == PictureMimeType.ofVideo() + ? MediaUtils.createVideoUri(getContext(), mConfig.suffixType) : MediaUtils.createImageUri(getContext(), mConfig.suffixType); + } + + public void setCameraListener(CameraListener cameraListener) { + this.mCameraListener = cameraListener; + } + + public void setPictureSelectionConfig(PictureSelectionConfig config) { + this.mConfig = config; + } + + public void setBindToLifecycle(LifecycleOwner lifecycleOwner) { + mCameraView.bindToLifecycle(lifecycleOwner); + lifecycleOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> { + + }); + } + + /** + * 设置录制视频最大时长 秒 + */ + public void setRecordVideoMaxTime(int maxDurationTime) { + mCaptureLayout.setDuration(maxDurationTime * 1000); + } + + /** + * 设置录制视频最小时长 秒 + */ + public void setRecordVideoMinTime(int minDurationTime) { + mCaptureLayout.setMinDuration(minDurationTime * 1000); + } + + /** + * 关闭相机界面按钮 + * + * @param clickListener + */ + public void setOnClickListener(ClickListener clickListener) { + this.mOnClickListener = clickListener; + } + + public void setImageCallbackListener(ImageCallbackListener mImageCallbackListener) { + this.mImageCallbackListener = mImageCallbackListener; + } + + private void setFlashRes() { + switch (type_flash) { + case TYPE_FLASH_AUTO: + mFlashLamp.setImageResource(R.drawable.picture_ic_flash_auto); + mCameraView.setFlash(ImageCapture.FLASH_MODE_AUTO); + break; + case TYPE_FLASH_ON: + mFlashLamp.setImageResource(R.drawable.picture_ic_flash_on); + mCameraView.setFlash(ImageCapture.FLASH_MODE_ON); + break; + case TYPE_FLASH_OFF: + mFlashLamp.setImageResource(R.drawable.picture_ic_flash_off); + mCameraView.setFlash(ImageCapture.FLASH_MODE_OFF); + break; + } + } + + public CameraView getCameraView() { + return mCameraView; + } + + public CaptureLayout getCaptureLayout() { + return mCaptureLayout; + } + + /** + * 重置状态 + */ + private void resetState() { + if (mCameraView.getCaptureMode() == androidx.camera.view.CameraView.CaptureMode.VIDEO) { + if (mCameraView.isRecording()) { + mCameraView.stopRecording(); + } + if (mVideoFile != null && mVideoFile.exists()) { + mVideoFile.delete(); + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(mConfig.cameraPath)) { + getContext().getContentResolver().delete(Uri.parse(mConfig.cameraPath), null, null); + } else { + new PictureMediaScannerConnection(getContext(), mVideoFile.getAbsolutePath()); + } + } + } else { + mImagePreview.setVisibility(INVISIBLE); + if (mPhotoFile != null && mPhotoFile.exists()) { + mPhotoFile.delete(); + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(mConfig.cameraPath)) { + getContext().getContentResolver().delete(Uri.parse(mConfig.cameraPath), null, null); + } else { + new PictureMediaScannerConnection(getContext(), mPhotoFile.getAbsolutePath()); + } + } + } + mSwitchCamera.setVisibility(VISIBLE); + mFlashLamp.setVisibility(VISIBLE); + mCameraView.setVisibility(View.VISIBLE); + mCaptureLayout.resetCaptureLayout(); + } + + /** + * 开始循环播放视频 + * + * @param videoFile + */ + private void startVideoPlay(File videoFile) { + try { + if (mMediaPlayer == null) { + mMediaPlayer = new MediaPlayer(); + } + mMediaPlayer.setDataSource(videoFile.getAbsolutePath()); + mMediaPlayer.setSurface(new Surface(mTextureView.getSurfaceTexture())); + mMediaPlayer.setLooping(true); + mMediaPlayer.setOnPreparedListener(mp -> { + mp.start(); + + float ratio = mp.getVideoWidth() * 1f / mp.getVideoHeight(); + int width1 = mTextureView.getWidth(); + ViewGroup.LayoutParams layoutParams = mTextureView.getLayoutParams(); + layoutParams.height = (int) (width1 / ratio); + mTextureView.setLayoutParams(layoutParams); + }); + mMediaPlayer.prepareAsync(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 停止视频播放 + */ + private void stopVideoPlay() { + if (mMediaPlayer != null) { + mMediaPlayer.stop(); + mMediaPlayer.release(); + mMediaPlayer = null; + } + mTextureView.setVisibility(View.GONE); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CameraListener.java b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CameraListener.java new file mode 100644 index 0000000..3056be6 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CameraListener.java @@ -0,0 +1,33 @@ +package com.luck.picture.lib.camera.listener; + +import androidx.annotation.NonNull; + +import java.io.File; + +/** + * @author:luck + * @date:2020-01-04 13:38 + * @describe:相机回调监听 + */ +public interface CameraListener { + /** + * 拍照成功返回 + * + * @param file + */ + void onPictureSuccess(@NonNull File file); + + /** + * 录像成功返回 + * + * @param file + */ + void onRecordSuccess(@NonNull File file); + + /** + * 使用相机出错 + * + * @param file + */ + void onError(int videoCaptureError, String message, Throwable cause); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CaptureListener.java b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CaptureListener.java new file mode 100644 index 0000000..e47fbe6 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/CaptureListener.java @@ -0,0 +1,19 @@ +package com.luck.picture.lib.camera.listener; + +/** + * @author:luck + * @date:2020-01-04 13:56 + */ +public interface CaptureListener { + void takePictures(); + + void recordShort(long time); + + void recordStart(); + + void recordEnd(long time); + + void recordZoom(float zoom); + + void recordError(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ClickListener.java b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ClickListener.java new file mode 100644 index 0000000..3d61c17 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ClickListener.java @@ -0,0 +1,10 @@ +package com.luck.picture.lib.camera.listener; + +/** + * @author:luck + * @date:2020-01-04 13:45 + * @describe:点击事件监听 + */ +public interface ClickListener { + void onClick(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ImageCallbackListener.java b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ImageCallbackListener.java new file mode 100644 index 0000000..0b0f3f5 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/ImageCallbackListener.java @@ -0,0 +1,20 @@ +package com.luck.picture.lib.camera.listener; + +import android.widget.ImageView; + +import java.io.File; + +/** + * @author:luck + * @date:2020-01-04 15:55 + * @describe:图片加载 + */ +public interface ImageCallbackListener { + /** + * 加载图片回调 + * + * @param file + * @param imageView + */ + void onLoadImage(File file, ImageView imageView); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/listener/TypeListener.java b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/TypeListener.java new file mode 100644 index 0000000..59e8a2d --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/listener/TypeListener.java @@ -0,0 +1,15 @@ +package com.luck.picture.lib.camera.listener; + +/** + * ===================================== + * 作 者: 陈嘉桐 + * 版 本:1.1.4 + * 创建日期:2017/4/25 + * 描 述: + * ===================================== + */ +public interface TypeListener { + void cancel(); + + void confirm(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureButton.java b/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureButton.java new file mode 100644 index 0000000..8839b3a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureButton.java @@ -0,0 +1,379 @@ +package com.luck.picture.lib.camera.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ValueAnimator; +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.os.CountDownTimer; +import android.view.MotionEvent; +import android.view.View; + +import com.luck.picture.lib.camera.CustomCameraView; +import com.luck.picture.lib.camera.CheckPermission; +import com.luck.picture.lib.camera.listener.CaptureListener; +import com.luck.picture.lib.tools.DoubleUtils; + + +/** + * ===================================== + * 作 者: 陈嘉桐 445263848@qq.com + * 版 本:1.1.4 + * 创建日期:2017/4/25 + * 描 述:拍照按钮 + * ===================================== + */ +public class CaptureButton extends View { + + private int state; //当前按钮状态 + private int button_state; //按钮可执行的功能状态(拍照,录制,两者) + + public static final int STATE_IDLE = 0x001; //空闲状态 + public static final int STATE_PRESS = 0x002; //按下状态 + public static final int STATE_LONG_PRESS = 0x003; //长按状态 + public static final int STATE_RECORDERING = 0x004; //录制状态 + public static final int STATE_BAN = 0x005; //禁止状态 + + private int progress_color = 0xEE16AE16; //进度条颜色 + private int outside_color = 0xEEDCDCDC; //外圆背景色 + private int inside_color = 0xFFFFFFFF; //内圆背景色 + + + private float event_Y; //Touch_Event_Down时候记录的Y值 + + + private Paint mPaint; + + private float strokeWidth; //进度条宽度 + private int outside_add_size; //长按外圆半径变大的Size + private int inside_reduce_size; //长安内圆缩小的Size + + //中心坐标 + private float center_X; + private float center_Y; + + private float button_radius; //按钮半径 + private float button_outside_radius; //外圆半径 + private float button_inside_radius; //内圆半径 + private int button_size; //按钮大小 + + private float progress; //录制视频的进度 + private int duration; //录制视频最大时间长度 + private int min_duration; //最短录制时间限制 + private int recorded_time; //记录当前录制的时间 + + private RectF rectF; + + private LongPressRunnable longPressRunnable; //长按后处理的逻辑Runnable + private CaptureListener captureLisenter; //按钮回调接口 + private RecordCountDownTimer timer; //计时器 + + public CaptureButton(Context context) { + super(context); + } + + public CaptureButton(Context context, int size) { + super(context); + this.button_size = size; + button_radius = size / 2.0f; + + button_outside_radius = button_radius; + button_inside_radius = button_radius * 0.75f; + + strokeWidth = size / 15; + outside_add_size = size / 8; + inside_reduce_size = size / 8; + + mPaint = new Paint(); + mPaint.setAntiAlias(true); + + progress = 0; + longPressRunnable = new LongPressRunnable(); + + state = STATE_IDLE; //初始化为空闲状态 + button_state = CustomCameraView.BUTTON_STATE_BOTH; //初始化按钮为可录制可拍照 + duration = 10 * 1000; //默认最长录制时间为10s + min_duration = 1500; //默认最短录制时间为1.5s + + center_X = (button_size + outside_add_size * 2) / 2; + center_Y = (button_size + outside_add_size * 2) / 2; + + rectF = new RectF( + center_X - (button_radius + outside_add_size - strokeWidth / 2), + center_Y - (button_radius + outside_add_size - strokeWidth / 2), + center_X + (button_radius + outside_add_size - strokeWidth / 2), + center_Y + (button_radius + outside_add_size - strokeWidth / 2)); + + timer = new RecordCountDownTimer(duration, duration / 360); //录制定时器 + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(button_size + outside_add_size * 2, button_size + outside_add_size * 2); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + mPaint.setStyle(Paint.Style.FILL); + + mPaint.setColor(outside_color); //外圆(半透明灰色) + canvas.drawCircle(center_X, center_Y, button_outside_radius, mPaint); + + mPaint.setColor(inside_color); //内圆(白色) + canvas.drawCircle(center_X, center_Y, button_inside_radius, mPaint); + + //如果状态为录制状态,则绘制录制进度条 + if (state == STATE_RECORDERING) { + mPaint.setColor(progress_color); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(strokeWidth); + canvas.drawArc(rectF, -90, progress, false, mPaint); + } + } + + + @Override + public boolean onTouchEvent(MotionEvent event) { + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + if (event.getPointerCount() > 1 || state != STATE_IDLE) + break; + event_Y = event.getY(); //记录Y值 + state = STATE_PRESS; //修改当前状态为点击按下 + + //判断按钮状态是否为可录制状态 + if ((button_state == CustomCameraView.BUTTON_STATE_ONLY_RECORDER || button_state == CustomCameraView.BUTTON_STATE_BOTH)) + postDelayed(longPressRunnable, 500); //同时延长500启动长按后处理的逻辑Runnable + break; + case MotionEvent.ACTION_MOVE: + if (captureLisenter != null + && state == STATE_RECORDERING + && (button_state == CustomCameraView.BUTTON_STATE_ONLY_RECORDER || button_state == CustomCameraView.BUTTON_STATE_BOTH)) { + //记录当前Y值与按下时候Y值的差值,调用缩放回调接口 + captureLisenter.recordZoom(event_Y - event.getY()); + } + break; + case MotionEvent.ACTION_UP: + //根据当前按钮的状态进行相应的处理 ----CodeReview---抬起瞬间应该重置状态 当前状态可能为按下和正在录制 + //state = STATE_BAN; + handlerPressByState(); + break; + } + return true; + } + + //当手指松开按钮时候处理的逻辑 + private void handlerPressByState() { + removeCallbacks(longPressRunnable); //移除长按逻辑的Runnable + //根据当前状态处理 + switch (state) { + //当前是点击按下 + case STATE_PRESS: + if (captureLisenter != null && (button_state == CustomCameraView.BUTTON_STATE_ONLY_CAPTURE || button_state == + CustomCameraView.BUTTON_STATE_BOTH)) { + startCaptureAnimation(button_inside_radius); + } else { + state = STATE_IDLE; + } + break; + // ---CodeReview---当内外圆动画未结束时已经是长按状态 但还没有置为STATE_RECORDERING时 应该也要结束录制 此处是一个bug + case STATE_LONG_PRESS: + //当前是长按状态 + case STATE_RECORDERING: + timer.cancel(); //停止计时器 + recordEnd(); //录制结束 + break; + } + state = STATE_IDLE; + } + + //录制结束 + public void recordEnd() { + if (captureLisenter != null) { + if (recorded_time < min_duration) + captureLisenter.recordShort(recorded_time);//回调录制时间过短 + else + captureLisenter.recordEnd(recorded_time); //回调录制结束 + } + resetRecordAnim(); //重制按钮状态 + } + + //重制状态 + private void resetRecordAnim() { + state = STATE_BAN; + progress = 0; //重制进度 + invalidate(); + //还原按钮初始状态动画 + startRecordAnimation( + button_outside_radius, + button_radius, + button_inside_radius, + button_radius * 0.75f + ); + } + + //内圆动画 + private void startCaptureAnimation(float inside_start) { + ValueAnimator inside_anim = ValueAnimator.ofFloat(inside_start, inside_start * 0.75f, inside_start); + inside_anim.addUpdateListener(animation -> { + button_inside_radius = (float) animation.getAnimatedValue(); + invalidate(); + }); + inside_anim.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + //回调拍照接口 +// if (captureLisenter != null) { +// captureLisenter.takePictures(); +// } + // 为何拍照完成要将状态掷为禁止????此处貌似bug!!!!!!---CodeReview + //state = STATE_BAN; + //state = STATE_IDLE; + } + + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (captureLisenter != null) { + captureLisenter.takePictures(); + } + // 防止重复点击 状态重置 + state = STATE_BAN; + } + }); + inside_anim.setDuration(50); + inside_anim.start(); + } + + //内外圆动画 + private void startRecordAnimation(float outside_start, float outside_end, float inside_start, float inside_end) { + ValueAnimator outside_anim = ValueAnimator.ofFloat(outside_start, outside_end); + ValueAnimator inside_anim = ValueAnimator.ofFloat(inside_start, inside_end); + //外圆动画监听 + outside_anim.addUpdateListener(animation -> { + button_outside_radius = (float) animation.getAnimatedValue(); + invalidate(); + }); + //内圆动画监听 + inside_anim.addUpdateListener(animation -> { + button_inside_radius = (float) animation.getAnimatedValue(); + invalidate(); + }); + AnimatorSet set = new AnimatorSet(); + //当动画结束后启动录像Runnable并且回调录像开始接口 + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (DoubleUtils.isFastDoubleClick()) { + return; + } + //设置为录制状态 + if (state == STATE_LONG_PRESS) { + if (captureLisenter != null) + captureLisenter.recordStart(); + state = STATE_RECORDERING; + timer.start(); + } else { + // 此处动画包括长按起始动画和还原动画 若不是长按状态应该还原状态为空闲????---CodeReview + state = STATE_IDLE; + } + } + }); + set.playTogether(outside_anim, inside_anim); + set.setDuration(100); + set.start(); + } + + + //更新进度条 + private void updateProgress(long millisUntilFinished) { + recorded_time = (int) (duration - millisUntilFinished); + progress = 360f - millisUntilFinished / (float) duration * 360f; + invalidate(); + } + + //录制视频计时器 + private class RecordCountDownTimer extends CountDownTimer { + RecordCountDownTimer(long millisInFuture, long countDownInterval) { + super(millisInFuture, countDownInterval); + } + + @Override + public void onTick(long millisUntilFinished) { + updateProgress(millisUntilFinished); + } + + @Override + public void onFinish() { + //updateProgress(duration); + recordEnd(); + } + } + + //长按线程 + private class LongPressRunnable implements Runnable { + @Override + public void run() { + state = STATE_LONG_PRESS; //如果按下后经过500毫秒则会修改当前状态为长按状态 + //没有录制权限 + if (CheckPermission.getRecordState() != CheckPermission.STATE_SUCCESS) { + state = STATE_IDLE; + if (captureLisenter != null) { + captureLisenter.recordError(); + return; + } + } + //启动按钮动画,外圆变大,内圆缩小 + startRecordAnimation( + button_outside_radius, + button_outside_radius + outside_add_size, + button_inside_radius, + button_inside_radius - inside_reduce_size + ); + } + } + + + //设置最长录制时间 + public void setDuration(int duration) { + this.duration = duration; + timer = new RecordCountDownTimer(duration, duration / 360); //录制定时器 + } + + //设置最短录制时间 + public void setMinDuration(int duration) { + this.min_duration = duration; + } + + //设置回调接口 + public void setCaptureListener(CaptureListener captureListener) { + this.captureLisenter = captureListener; + } + + //设置按钮功能(拍照和录像) + public void setButtonFeatures(int state) { + this.button_state = state; + } + + //设置按钮功能(拍照和录像) + public int getButtonFeatures() { + return button_state; + } + + //是否空闲状态 + public boolean isIdle() { + return state == STATE_IDLE ? true : false; + } + + //设置状态 + public void resetState() { + state = STATE_IDLE; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureLayout.java b/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureLayout.java new file mode 100644 index 0000000..73a0086 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/view/CaptureLayout.java @@ -0,0 +1,365 @@ +package com.luck.picture.lib.camera.view; + +import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.content.Context; +import android.content.res.Configuration; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.view.Gravity; +import android.view.View; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.TextView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.camera.listener.CaptureListener; +import com.luck.picture.lib.camera.listener.ClickListener; +import com.luck.picture.lib.camera.listener.TypeListener; +import com.luck.picture.lib.tools.DoubleUtils; + +import static com.luck.picture.lib.camera.CustomCameraView.BUTTON_STATE_ONLY_CAPTURE; +import static com.luck.picture.lib.camera.CustomCameraView.BUTTON_STATE_ONLY_RECORDER; + +/** + * ===================================== + * 作 者: 陈嘉桐 445263848@qq.com + * 版 本:1.0.4 + * 创建日期:2017/4/26 + * 描 述:集成各个控件的布局 + * ===================================== + */ + +public class CaptureLayout extends FrameLayout { + + private CaptureListener captureListener; //拍照按钮监听 + private TypeListener typeListener; //拍照或录制后接结果按钮监听 + private ClickListener leftClickListener; //左边按钮监听 + private ClickListener rightClickListener; //右边按钮监听 + + public void setTypeListener(TypeListener typeListener) { + this.typeListener = typeListener; + } + + public void setCaptureListener(CaptureListener captureListener) { + this.captureListener = captureListener; + } + + private CaptureButton btn_capture; //拍照按钮 + private TypeButton btn_confirm; //确认按钮 + private TypeButton btn_cancel; //取消按钮 + private ReturnButton btn_return; //返回按钮 + private ImageView iv_custom_left; //左边自定义按钮 + private ImageView iv_custom_right; //右边自定义按钮 + private TextView txt_tip; //提示文本 + + private int layout_width; + private int layout_height; + private int button_size; + private int iconLeft = 0; + private int iconRight = 0; + + public CaptureLayout(Context context) { + this(context, null); + } + + public CaptureLayout(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public CaptureLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + + WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); + DisplayMetrics outMetrics = new DisplayMetrics(); + manager.getDefaultDisplay().getMetrics(outMetrics); + + if (this.getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) { + layout_width = outMetrics.widthPixels; + } else { + layout_width = outMetrics.widthPixels / 2; + } + button_size = (int) (layout_width / 4.5f); + layout_height = button_size + (button_size / 5) * 2 + 100; + + initView(); + initEvent(); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(layout_width, layout_height); + } + + public void initEvent() { + //默认TypeButton为隐藏 + iv_custom_right.setVisibility(GONE); + btn_cancel.setVisibility(GONE); + btn_confirm.setVisibility(GONE); + } + + public void startTypeBtnAnimator() { + //拍照录制结果后的动画 + if (this.iconLeft != 0) + iv_custom_left.setVisibility(GONE); + else + btn_return.setVisibility(GONE); + if (this.iconRight != 0) + iv_custom_right.setVisibility(GONE); + btn_capture.setVisibility(GONE); + btn_cancel.setVisibility(VISIBLE); + btn_confirm.setVisibility(VISIBLE); + btn_cancel.setClickable(false); + btn_confirm.setClickable(false); + iv_custom_left.setVisibility(GONE); + ObjectAnimator animator_cancel = ObjectAnimator.ofFloat(btn_cancel, "translationX", layout_width / 4, 0); + ObjectAnimator animator_confirm = ObjectAnimator.ofFloat(btn_confirm, "translationX", -layout_width / 4, 0); + + AnimatorSet set = new AnimatorSet(); + set.playTogether(animator_cancel, animator_confirm); + set.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + btn_cancel.setClickable(true); + btn_confirm.setClickable(true); + } + }); + set.setDuration(500); + set.start(); + } + + + private void initView() { + setWillNotDraw(false); + //拍照按钮 + btn_capture = new CaptureButton(getContext(), button_size); + FrameLayout.LayoutParams btn_capture_param = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + btn_capture_param.gravity = Gravity.CENTER; + btn_capture.setLayoutParams(btn_capture_param); + btn_capture.setCaptureListener(new CaptureListener() { + @Override + public void takePictures() { + if (captureListener != null) { + captureListener.takePictures(); + } + startAlphaAnimation(); + } + + @Override + public void recordShort(long time) { + if (captureListener != null) { + captureListener.recordShort(time); + } + } + + @Override + public void recordStart() { + if (captureListener != null) { + captureListener.recordStart(); + } + startAlphaAnimation(); + } + + @Override + public void recordEnd(long time) { + if (captureListener != null) { + captureListener.recordEnd(time); + } + startTypeBtnAnimator(); + } + + @Override + public void recordZoom(float zoom) { + if (captureListener != null) { + captureListener.recordZoom(zoom); + } + } + + @Override + public void recordError() { + if (captureListener != null) { + captureListener.recordError(); + } + } + }); + + //取消按钮 + btn_cancel = new TypeButton(getContext(), TypeButton.TYPE_CANCEL, button_size); + final LayoutParams btn_cancel_param = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + btn_cancel_param.gravity = Gravity.CENTER_VERTICAL; + btn_cancel_param.setMargins((layout_width / 4) - button_size / 2, 0, 0, 0); + btn_cancel.setLayoutParams(btn_cancel_param); + btn_cancel.setOnClickListener(view -> { + if (typeListener != null) { + typeListener.cancel(); + } + }); + + //确认按钮 + btn_confirm = new TypeButton(getContext(), TypeButton.TYPE_CONFIRM, button_size); + LayoutParams btn_confirm_param = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); + btn_confirm_param.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; + btn_confirm_param.setMargins(0, 0, (layout_width / 4) - button_size / 2, 0); + btn_confirm.setLayoutParams(btn_confirm_param); + btn_confirm.setOnClickListener(view -> { + if (typeListener != null) { + typeListener.confirm(); + } + }); + + //返回按钮 + btn_return = new ReturnButton(getContext(), (int) (button_size / 2.5f)); + LayoutParams btn_return_param = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); + btn_return_param.gravity = Gravity.CENTER_VERTICAL; + btn_return_param.setMargins(layout_width / 6, 0, 0, 0); + btn_return.setLayoutParams(btn_return_param); + btn_return.setOnClickListener(v -> { + if (leftClickListener != null) { + leftClickListener.onClick(); + } + }); + //左边自定义按钮 + iv_custom_left = new ImageView(getContext()); + LayoutParams iv_custom_param_left = new LayoutParams((int) (button_size / 2.5f), (int) (button_size / 2.5f)); + iv_custom_param_left.gravity = Gravity.CENTER_VERTICAL; + iv_custom_param_left.setMargins(layout_width / 6, 0, 0, 0); + iv_custom_left.setLayoutParams(iv_custom_param_left); + iv_custom_left.setOnClickListener(v -> { + if (leftClickListener != null) { + leftClickListener.onClick(); + } + }); + + //右边自定义按钮 + iv_custom_right = new ImageView(getContext()); + LayoutParams iv_custom_param_right = new LayoutParams((int) (button_size / 2.5f), (int) (button_size / 2.5f)); + iv_custom_param_right.gravity = Gravity.CENTER_VERTICAL | Gravity.RIGHT; + iv_custom_param_right.setMargins(0, 0, layout_width / 6, 0); + iv_custom_right.setLayoutParams(iv_custom_param_right); + iv_custom_right.setOnClickListener(v -> { + if (rightClickListener != null) { + rightClickListener.onClick(); + } + }); + + txt_tip = new TextView(getContext()); + LayoutParams txt_param = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT); + txt_param.gravity = Gravity.CENTER_HORIZONTAL; + txt_param.setMargins(0, 0, 0, 0); + + txt_tip.setText(getCaptureTip()); + + txt_tip.setTextColor(0xFFFFFFFF); + txt_tip.setGravity(Gravity.CENTER); + txt_tip.setLayoutParams(txt_param); + + this.addView(btn_capture); + this.addView(btn_cancel); + this.addView(btn_confirm); + this.addView(btn_return); + this.addView(iv_custom_left); + this.addView(iv_custom_right); + this.addView(txt_tip); + + } + + private String getCaptureTip() { + int buttonFeatures = btn_capture.getButtonFeatures(); + switch (buttonFeatures) { + case BUTTON_STATE_ONLY_CAPTURE: + return getContext().getString(R.string.picture_photo_pictures); + case BUTTON_STATE_ONLY_RECORDER: + return getContext().getString(R.string.picture_photo_recording); + default: + return getContext().getString(R.string.picture_photo_camera); + } + } + + public void resetCaptureLayout() { + btn_capture.resetState(); + btn_cancel.setVisibility(GONE); + btn_confirm.setVisibility(GONE); + btn_capture.setVisibility(VISIBLE); + txt_tip.setText(getCaptureTip()); + txt_tip.setVisibility(View.VISIBLE); + if (this.iconLeft != 0) + iv_custom_left.setVisibility(VISIBLE); + else + btn_return.setVisibility(VISIBLE); + if (this.iconRight != 0) + iv_custom_right.setVisibility(VISIBLE); + } + + + public void startAlphaAnimation() { + txt_tip.setVisibility(View.INVISIBLE); + } + + public void setTextWithAnimation(String tip) { + txt_tip.setText(tip); + ObjectAnimator animator_txt_tip = ObjectAnimator.ofFloat(txt_tip, "alpha", 0f, 1f, 1f, 0f); + animator_txt_tip.addListener(new AnimatorListenerAdapter() { + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + txt_tip.setText(getCaptureTip()); + txt_tip.setAlpha(1f); + } + }); + animator_txt_tip.setDuration(2500); + animator_txt_tip.start(); + } + + public void setDuration(int duration) { + btn_capture.setDuration(duration); + } + + public void setMinDuration(int duration) { + btn_capture.setMinDuration(duration); + } + + public void setButtonFeatures(int state) { + btn_capture.setButtonFeatures(state); + txt_tip.setText(getCaptureTip()); + } + + public void setTip(String tip) { + txt_tip.setText(tip); + } + + public void showTip() { + txt_tip.setVisibility(VISIBLE); + } + + public void setIconSrc(int iconLeft, int iconRight) { + this.iconLeft = iconLeft; + this.iconRight = iconRight; + if (this.iconLeft != 0) { + iv_custom_left.setImageResource(iconLeft); + iv_custom_left.setVisibility(VISIBLE); + btn_return.setVisibility(GONE); + } else { + iv_custom_left.setVisibility(GONE); + btn_return.setVisibility(VISIBLE); + } + if (this.iconRight != 0) { + iv_custom_right.setImageResource(iconRight); + iv_custom_right.setVisibility(VISIBLE); + } else { + iv_custom_right.setVisibility(GONE); + } + } + + public void setLeftClickListener(ClickListener leftClickListener) { + this.leftClickListener = leftClickListener; + } + + public void setRightClickListener(ClickListener rightClickListener) { + this.rightClickListener = rightClickListener; + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/view/ReturnButton.java b/picture_library/src/main/java/com/luck/picture/lib/camera/view/ReturnButton.java new file mode 100644 index 0000000..f811d63 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/view/ReturnButton.java @@ -0,0 +1,63 @@ +package com.luck.picture.lib.camera.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.view.View; + +/** + * ===================================== + * 作 者: 陈嘉桐 445263848@qq.com + * 版 本:1.0.4 + * 创建日期:2017/4/26 + * 描 述:向下箭头的退出按钮 + * ===================================== + */ +public class ReturnButton extends View { + + private int size; + + private int center_X; + private int center_Y; + private float strokeWidth; + + private Paint paint; + Path path; + + public ReturnButton(Context context, int size) { + this(context); + this.size = size; + center_X = size / 2; + center_Y = size / 2; + + strokeWidth = size / 15f; + + paint = new Paint(); + paint.setAntiAlias(true); + paint.setColor(Color.WHITE); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(strokeWidth); + + path = new Path(); + } + + public ReturnButton(Context context) { + super(context); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + setMeasuredDimension(size, size / 2); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + path.moveTo(strokeWidth, strokeWidth / 2); + path.lineTo(center_X, center_Y - strokeWidth / 2); + path.lineTo(size - strokeWidth, strokeWidth / 2); + canvas.drawPath(path, paint); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/camera/view/TypeButton.java b/picture_library/src/main/java/com/luck/picture/lib/camera/view/TypeButton.java new file mode 100644 index 0000000..bcab339 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/camera/view/TypeButton.java @@ -0,0 +1,109 @@ +package com.luck.picture.lib.camera.view; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; +import android.graphics.RectF; +import android.view.View; + +/** + * ===================================== + * 作 者: 陈嘉桐 445263848@qq.com + * 版 本:1.0.4 + * 创建日期:2017/4/26 + * 描 述:拍照或录制完成后弹出的确认和返回按钮 + * ===================================== + */ +public class TypeButton extends View{ + public static final int TYPE_CANCEL = 0x001; + public static final int TYPE_CONFIRM = 0x002; + private int button_type; + private int button_size; + + private float center_X; + private float center_Y; + private float button_radius; + + private Paint mPaint; + private Path path; + private float strokeWidth; + + private float index; + private RectF rectF; + + public TypeButton(Context context) { + super(context); + } + + public TypeButton(Context context, int type, int size) { + super(context); + this.button_type = type; + button_size = size; + button_radius = size / 2.0f; + center_X = size / 2.0f; + center_Y = size / 2.0f; + + mPaint = new Paint(); + path = new Path(); + strokeWidth = size / 50f; + index = button_size / 12f; + rectF = new RectF(center_X, center_Y - index, center_X + index * 2, center_Y + index); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + setMeasuredDimension(button_size, button_size); + } + + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + //如果类型为取消,则绘制内部为返回箭头 + if (button_type == TYPE_CANCEL) { + mPaint.setAntiAlias(true); + mPaint.setColor(0xEEDCDCDC); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(center_X, center_Y, button_radius, mPaint); + + mPaint.setColor(Color.BLACK); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setStrokeWidth(strokeWidth); + + path.moveTo(center_X - index / 7, center_Y + index); + path.lineTo(center_X + index, center_Y + index); + + path.arcTo(rectF, 90, -180); + path.lineTo(center_X - index, center_Y - index); + canvas.drawPath(path, mPaint); + mPaint.setStyle(Paint.Style.FILL); + path.reset(); + path.moveTo(center_X - index, (float) (center_Y - index * 1.5)); + path.lineTo(center_X - index, (float) (center_Y - index / 2.3)); + path.lineTo((float) (center_X - index * 1.6), center_Y - index); + path.close(); + canvas.drawPath(path, mPaint); + + } + //如果类型为确认,则绘制绿色勾 + if (button_type == TYPE_CONFIRM) { + mPaint.setAntiAlias(true); + mPaint.setColor(0xFFFFFFFF); + mPaint.setStyle(Paint.Style.FILL); + canvas.drawCircle(center_X, center_Y, button_radius, mPaint); + mPaint.setAntiAlias(true); + mPaint.setStyle(Paint.Style.STROKE); + mPaint.setColor(0xFF00CC00); + mPaint.setStrokeWidth(strokeWidth); + + path.moveTo(center_X - button_size / 6f, center_Y); + path.lineTo(center_X - button_size / 21.2f, center_Y + button_size / 7.7f); + path.lineTo(center_X + button_size / 4.0f, center_Y - button_size / 8.5f); + path.lineTo(center_X - button_size / 21.2f, center_Y + button_size / 9.4f); + path.close(); + canvas.drawPath(path, mPaint); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/Checker.java b/picture_library/src/main/java/com/luck/picture/lib/compress/Checker.java new file mode 100644 index 0000000..02b75a6 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/Checker.java @@ -0,0 +1,237 @@ +package com.luck.picture.lib.compress; + +import android.graphics.BitmapFactory; +import android.text.TextUtils; +import android.util.Log; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.util.Arrays; + +enum Checker { + SINGLE; + + public final static String MIME_TYPE_JPEG = "image/jpeg"; + + public final static String MIME_TYPE_JPG = "image/jpg"; + + private static final String TAG = "Luban"; + + private static final String JPG = ".jpg"; + + private final byte[] JPEG_SIGNATURE = new byte[]{(byte) 0xFF, (byte) 0xD8, (byte) 0xFF}; + + /** + * Determine if it is JPG. + * + * @param is image file input stream + */ + boolean isJPG(InputStream is) { + return isJPG(toByteArray(is)); + } + + /** + * Determine if it is JPG. + * + * @param is image file mimeType + */ + boolean isJPG(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return false; + } + return mimeType.startsWith(MIME_TYPE_JPEG) || mimeType.startsWith(MIME_TYPE_JPG); + } + + /** + * Returns the degrees in clockwise. Values are 0, 90, 180, or 270. + */ + int getOrientation(InputStream is) { + return getOrientation(toByteArray(is)); + } + + private boolean isJPG(byte[] data) { + if (data == null || data.length < 3) { + return false; + } + byte[] signatureB = new byte[]{data[0], data[1], data[2]}; + return Arrays.equals(JPEG_SIGNATURE, signatureB); + } + + private int getOrientation(byte[] jpeg) { + if (jpeg == null) { + return 0; + } + + int offset = 0; + int length = 0; + + // ISO/IEC 10918-1:1993(E) + while (offset + 3 < jpeg.length && (jpeg[offset++] & 0xFF) == 0xFF) { + int marker = jpeg[offset] & 0xFF; + + // Check if the marker is a padding. + if (marker == 0xFF) { + continue; + } + offset++; + + // Check if the marker is SOI or TEM. + if (marker == 0xD8 || marker == 0x01) { + continue; + } + // Check if the marker is EOI or SOS. + if (marker == 0xD9 || marker == 0xDA) { + break; + } + + // Get the length and check if it is reasonable. + length = pack(jpeg, offset, 2, false); + if (length < 2 || offset + length > jpeg.length) { + Log.e(TAG, "Invalid length"); + return 0; + } + + // Break if the marker is EXIF in APP1. + if (marker == 0xE1 && length >= 8 + && pack(jpeg, offset + 2, 4, false) == 0x45786966 + && pack(jpeg, offset + 6, 2, false) == 0) { + offset += 8; + length -= 8; + break; + } + + // Skip other markers. + offset += length; + length = 0; + } + + // JEITA CP-3451 Exif Version 2.2 + if (length > 8) { + // Identify the byte order. + int tag = pack(jpeg, offset, 4, false); + if (tag != 0x49492A00 && tag != 0x4D4D002A) { + Log.e(TAG, "Invalid byte order"); + return 0; + } + boolean littleEndian = (tag == 0x49492A00); + + // Get the offset and check if it is reasonable. + int count = pack(jpeg, offset + 4, 4, littleEndian) + 2; + if (count < 10 || count > length) { + Log.e(TAG, "Invalid offset"); + return 0; + } + offset += count; + length -= count; + + // Get the count and go through all the elements. + count = pack(jpeg, offset - 2, 2, littleEndian); + while (count-- > 0 && length >= 12) { + // Get the tag and check if it is orientation. + tag = pack(jpeg, offset, 2, littleEndian); + if (tag == 0x0112) { + int orientation = pack(jpeg, offset + 8, 2, littleEndian); + switch (orientation) { + case 1: + return 0; + case 3: + return 180; + case 6: + return 90; + case 8: + return 270; + } + Log.e(TAG, "Unsupported orientation"); + return 0; + } + offset += 12; + length -= 12; + } + } + + Log.e(TAG, "Orientation not found"); + return 0; + } + + String extSuffix(InputStreamProvider input) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(input.open(), null, options); + return options.outMimeType.replace("image/", "."); + } catch (Exception e) { + return JPG; + } + } + + String extSuffix(String mimeType) { + try { + if (TextUtils.isEmpty(mimeType)) { + return JPG; + } + return mimeType.startsWith("video") ? mimeType.replace("video/", ".") + : mimeType.replace("image/", "."); + } catch (Exception e) { + return JPG; + } + } + + boolean needCompress(int leastCompressSize, String path) { + if (leastCompressSize > 0) { + File source = new File(path); + return source.exists() && source.length() > (leastCompressSize << 10); + } + return true; + } + + boolean needCompressToLocalMedia(int leastCompressSize, String path) { + if (leastCompressSize > 0 && !TextUtils.isEmpty(path)) { + File source = new File(path); + return source.exists() && source.length() > (leastCompressSize << 10); + } + return true; + } + + private int pack(byte[] bytes, int offset, int length, boolean littleEndian) { + int step = 1; + if (littleEndian) { + offset += length - 1; + step = -1; + } + + int value = 0; + while (length-- > 0) { + value = (value << 8) | (bytes[offset] & 0xFF); + offset += step; + } + return value; + } + + private byte[] toByteArray(InputStream is) { + if (is == null) { + return new byte[0]; + } + + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + + int read; + byte[] data = new byte[4096]; + + try { + while ((read = is.read(data, 0, data.length)) != -1) { + buffer.write(data, 0, read); + } + } catch (Exception ignored) { + return new byte[0]; + } finally { + try { + buffer.close(); + } catch (IOException ignored) { + } + } + + return buffer.toByteArray(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/CompressionPredicate.java b/picture_library/src/main/java/com/luck/picture/lib/compress/CompressionPredicate.java new file mode 100644 index 0000000..8e6ceeb --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/CompressionPredicate.java @@ -0,0 +1,19 @@ +package com.luck.picture.lib.compress; + +/** + * Created on 2018/1/3 19:43 + * + * @author andy + * + * A functional interface (callback) that returns true or false for the given input path should be compressed. + */ + +public interface CompressionPredicate { + + /** + * Determine the given input path should be compressed and return a boolean. + * @param path input path + * @return the boolean result + */ + boolean apply(String path); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/Engine.java b/picture_library/src/main/java/com/luck/picture/lib/compress/Engine.java new file mode 100644 index 0000000..e590a86 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/Engine.java @@ -0,0 +1,117 @@ +package com.luck.picture.lib.compress; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; +import android.media.ExifInterface; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; + +/** + * Responsible for starting compress and managing active and cached resources. + */ +class Engine { + private InputStreamProvider srcImg; + private File tagImg; + private int srcWidth; + private int srcHeight; + private boolean focusAlpha; + private static final int DEFAULT_QUALITY = 80; + private int compressQuality; + + Engine(InputStreamProvider srcImg, File tagImg, boolean focusAlpha, int compressQuality) throws IOException { + this.tagImg = tagImg; + this.srcImg = srcImg; + this.focusAlpha = focusAlpha; + this.compressQuality = compressQuality <= 0 ? DEFAULT_QUALITY : compressQuality; + + if (srcImg.getMedia() != null + && srcImg.getMedia().getWidth() > 0 + && srcImg.getMedia().getHeight() > 0) { + this.srcWidth = srcImg.getMedia().getWidth(); + this.srcHeight = srcImg.getMedia().getHeight(); + } else { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + options.inSampleSize = 1; + BitmapFactory.decodeStream(srcImg.open(), null, options); + this.srcWidth = options.outWidth; + this.srcHeight = options.outHeight; + } + } + + private int computeSize() { + srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth; + srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight; + + int longSide = Math.max(srcWidth, srcHeight); + int shortSide = Math.min(srcWidth, srcHeight); + + float scale = ((float) shortSide / longSide); + if (scale <= 1 && scale > 0.5625) { + if (longSide < 1664) { + return 1; + } else if (longSide < 4990) { + return 2; + } else if (longSide > 4990 && longSide < 10240) { + return 4; + } else { + return longSide / 1280; + } + } else if (scale <= 0.5625 && scale > 0.5) { + return longSide / 1280 == 0 ? 1 : longSide / 1280; + } else { + return (int) Math.ceil(longSide / (1280.0 / scale)); + } + } + + private Bitmap rotatingImage(Bitmap bitmap, int angle) { + Matrix matrix = new Matrix(); + + matrix.postRotate(angle); + + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + File compress() throws IOException { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = computeSize(); + Bitmap tagBitmap = BitmapFactory.decodeStream(srcImg.open(), null, options); + ByteArrayOutputStream stream = new ByteArrayOutputStream(); + if (srcImg.getMedia() != null && !srcImg.getMedia().isCut()) { + if (Checker.SINGLE.isJPG(srcImg.getMedia().getMimeType())) { + int orientation = srcImg.getMedia().getOrientation(); + if (orientation > 0) { + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + orientation = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + orientation = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + orientation = 270; + break; + default: + break; + } + tagBitmap = rotatingImage(tagBitmap, orientation); + } + } + } + if (tagBitmap != null) { + compressQuality = compressQuality <= 0 || compressQuality > 100 ? DEFAULT_QUALITY : compressQuality; + tagBitmap.compress(focusAlpha ? Bitmap.CompressFormat.PNG : Bitmap.CompressFormat.JPEG, compressQuality, stream); + tagBitmap.recycle(); + } + FileOutputStream fos = new FileOutputStream(tagImg); + fos.write(stream.toByteArray()); + fos.flush(); + fos.close(); + stream.close(); + return tagImg; + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamAdapter.java b/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamAdapter.java new file mode 100644 index 0000000..607ce30 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamAdapter.java @@ -0,0 +1,34 @@ +package com.luck.picture.lib.compress; + +import java.io.IOException; +import java.io.InputStream; + +/** + * Automatically close the previous InputStream when opening a new InputStream, + * and finally need to manually call {@link #close()} to release the resource. + */ +public abstract class InputStreamAdapter implements InputStreamProvider { + + private InputStream inputStream; + + @Override + public InputStream open() throws IOException { + close(); + inputStream = openInternal(); + return inputStream; + } + + public abstract InputStream openInternal() throws IOException; + + @Override + public void close() { + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException ignore) { + }finally { + inputStream = null; + } + } + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamProvider.java b/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamProvider.java new file mode 100644 index 0000000..b3d1b4b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/InputStreamProvider.java @@ -0,0 +1,23 @@ +package com.luck.picture.lib.compress; + +import com.luck.picture.lib.entity.LocalMedia; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 通过此接口获取输入流,以兼容文件、FileProvider方式获取到的图片 + *

+ * Get the input stream through this interface, and obtain the picture using compatible files and FileProvider + */ +public interface InputStreamProvider { + + InputStream open() throws IOException; + + void close(); + + String getPath(); + + LocalMedia getMedia(); + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/Luban.java b/picture_library/src/main/java/com/luck/picture/lib/compress/Luban.java new file mode 100644 index 0000000..d01fa46 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/Luban.java @@ -0,0 +1,638 @@ +package com.luck.picture.lib.compress; + +import android.content.Context; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Environment; +import android.os.Handler; +import android.os.Looper; +import android.os.Message; +import android.text.TextUtils; +import android.util.Log; + +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.tools.AndroidQTransformUtils; +import com.luck.picture.lib.tools.DateUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.StringUtils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +@SuppressWarnings("unused") +public class Luban implements Handler.Callback { + private static final String TAG = "Luban"; + + private static final int MSG_COMPRESS_SUCCESS = 0; + private static final int MSG_COMPRESS_START = 1; + private static final int MSG_COMPRESS_ERROR = 2; + + private String mTargetDir; + private String mNewFileName; + private boolean focusAlpha; + private boolean isCamera; + private int mLeastCompressSize; + private OnRenameListener mRenameListener; + private OnCompressListener mCompressListener; + private CompressionPredicate mCompressionPredicate; + private List mStreamProviders; + private List mPaths; + private List mediaList; + private int index = -1; + private int compressQuality; + private Handler mHandler; + private int dataCount; + + private Luban(Builder builder) { + this.mPaths = builder.mPaths; + this.mediaList = builder.mediaList; + this.dataCount = builder.dataCount; + this.mTargetDir = builder.mTargetDir; + this.mNewFileName = builder.mNewFileName; + this.mRenameListener = builder.mRenameListener; + this.mStreamProviders = builder.mStreamProviders; + this.mCompressListener = builder.mCompressListener; + this.mLeastCompressSize = builder.mLeastCompressSize; + this.mCompressionPredicate = builder.mCompressionPredicate; + this.compressQuality = builder.compressQuality; + this.focusAlpha = builder.focusAlpha; + this.isCamera = builder.isCamera; + this.mHandler = new Handler(Looper.getMainLooper(), this); + } + + public static Builder with(Context context) { + return new Builder(context); + } + + /** + * Returns a file with a cache image name in the private cache directory. + * + * @param context A context. + */ + private File getImageCacheFile(Context context, InputStreamProvider provider, String suffix) { + if (TextUtils.isEmpty(mTargetDir)) { + File imageCacheDir = getImageCacheDir(context); + if (imageCacheDir != null) { + mTargetDir = imageCacheDir.getAbsolutePath(); + } + } + String cacheBuilder = ""; + try { + LocalMedia media = provider.getMedia(); + String encryptionValue = StringUtils.getEncryptionValue(media.getPath(), media.getWidth(), media.getHeight()); + if (!TextUtils.isEmpty(encryptionValue) && !media.isCut()) { + cacheBuilder = mTargetDir + "/" + + "IMG_CMP_" + + encryptionValue + + (TextUtils.isEmpty(suffix) ? ".jpg" : suffix); + } else { + cacheBuilder = mTargetDir + + "/" + + DateUtils.getCreateFileName("IMG_CMP_") + + (TextUtils.isEmpty(suffix) ? ".jpg" : suffix); + } + } catch (Exception e) { + e.printStackTrace(); + } + + return new File(cacheBuilder); + } + + private File getImageCustomFile(Context context, String filename) { + if (TextUtils.isEmpty(mTargetDir)) { + mTargetDir = getImageCacheDir(context).getAbsolutePath(); + } + + String cacheBuilder = mTargetDir + "/" + filename; + + return new File(cacheBuilder); + } + + /** + * Returns a directory with the given name in the private cache directory of the application to + * use to store retrieved media and thumbnails. + * + * @param context A context. + * @see #getImageCacheDir(Context) + */ + private static File getImageCacheDir(Context context) { + File cacheDir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (cacheDir != null) { + if (!cacheDir.mkdirs() && (!cacheDir.exists() || !cacheDir.isDirectory())) { + // File wasn't able to create a directory, or the result exists but not a directory + return null; + } + return cacheDir; + } + if (Log.isLoggable(TAG, Log.ERROR)) { + Log.e(TAG, "default disk cache dir is null"); + } + return null; + } + + /** + * start asynchronous compress thread + */ + private void launch(final Context context) { + if (mStreamProviders == null || mPaths == null || mStreamProviders.size() == 0 && mCompressListener != null) { + mCompressListener.onError(new NullPointerException("image file cannot be null")); + } + Iterator iterator = mStreamProviders.iterator(); + // 当前压缩下标 + index = -1; + while (iterator.hasNext()) { + final InputStreamProvider path = iterator.next(); + AsyncTask.SERIAL_EXECUTOR.execute(() -> { + try { + index++; + mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_START)); + String newPath; + if (path.open() != null) { + if (path.getMedia().isCompressed() + && !TextUtils.isEmpty(path.getMedia().getCompressPath())) { + // 压缩过的图片不重复压缩 注意:如果是开启了裁剪 就算压缩过也要重新压缩 + boolean exists = !path.getMedia().isCut() && new File(path.getMedia().getCompressPath()).exists(); + + File result = exists ? new File(path.getMedia().getCompressPath()) + : compress(context, path); + newPath = result.getAbsolutePath(); + } else { + File result = PictureMimeType.isHasVideo(path.getMedia().getMimeType()) + ? new File(path.getPath()) : compress(context, path); + newPath = result.getAbsolutePath(); + } + } else { + // error + newPath = path.getPath(); + } + if (mediaList != null && mediaList.size() > 0) { + LocalMedia media = mediaList.get(index); + boolean isHasHttp = PictureMimeType.isHasHttp(newPath); + boolean isHasVideo = PictureMimeType.isHasVideo(media.getMimeType()); + media.setCompressed(!isHasHttp && !isHasVideo); + media.setCompressPath(isHasHttp || isHasVideo ? null : newPath); + media.setAndroidQToPath(SdkVersionUtils.checkedAndroid_Q() ? media.getCompressPath() : null); + boolean isLast = index == mediaList.size() - 1; + if (isLast) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_SUCCESS, mediaList)); + } + } else { + mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, new IOException())); + } + } catch (IOException e) { + mHandler.sendMessage(mHandler.obtainMessage(MSG_COMPRESS_ERROR, e)); + } + }); + + iterator.remove(); + } + } + + /** + * start compress and return the file + */ + private File get(InputStreamProvider input, Context context) throws IOException { + try { + return new Engine(input, getImageCacheFile(context, input, Checker.SINGLE.extSuffix(input)), focusAlpha, compressQuality).compress(); + } finally { + input.close(); + } + } + + private List get(Context context) throws IOException { + List results = new ArrayList<>(); + Iterator iterator = mStreamProviders.iterator(); + + while (iterator.hasNext()) { + InputStreamProvider provider = iterator.next(); + InputStream inputStream = provider.open(); + if (inputStream != null) { + if (provider.getMedia().isCompressed() + && !TextUtils.isEmpty(provider.getMedia().getCompressPath())) { + // 压缩过的图片不重复压缩 注意:如果是开启了裁剪 就算压缩过也要重新压缩 + boolean exists = !provider.getMedia().isCut() && new File(provider.getMedia().getCompressPath()).exists(); + File oldFile = exists ? new File(provider.getMedia().getCompressPath()) + : compress(context, provider); + results.add(oldFile); + } else { + boolean hasVideo = PictureMimeType.isHasVideo(provider.getMedia().getMimeType()); + results.add(hasVideo ? new File(provider.getMedia().getPath()) : compress(context, provider)); + } + } else { + // error + results.add(new File(provider.getMedia().getPath())); + } + iterator.remove(); + } + + return results; + } + + private File compress(Context context, InputStreamProvider path) throws IOException { + try { + return compressRealLocalMedia(context, path); + } finally { + path.close(); + } + } + + private File compressReal(Context context, InputStreamProvider path) throws IOException { + File result; + String suffix = Checker.SINGLE.extSuffix(path.getMedia() != null ? path.getMedia().getMimeType() : ""); + File outFile = getImageCacheFile(context, path, TextUtils.isEmpty(suffix) ? Checker.SINGLE.extSuffix(path) : suffix); + if (mRenameListener != null) { + String filename = mRenameListener.rename(path.getPath()); + outFile = getImageCustomFile(context, filename); + } + + if (mCompressionPredicate != null) { + if (mCompressionPredicate.apply(path.getPath()) + && Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath())) { + result = new Engine(path, outFile, focusAlpha, compressQuality).compress(); + } else { + result = new File(path.getPath()); + } + } else { + if (Checker.SINGLE.extSuffix(path).startsWith(".gif")) { + // GIF without compression + result = new File(path.getPath()); + } else { + result = Checker.SINGLE.needCompress(mLeastCompressSize, path.getPath()) ? + new Engine(path, outFile, focusAlpha, compressQuality).compress() : + new File(path.getPath()); + } + } + return result; + } + + private File compressRealLocalMedia(Context context, InputStreamProvider path) throws IOException { + File result = null; + LocalMedia media = path.getMedia(); + if (media == null) { + throw new NullPointerException("Luban Compress LocalMedia Can't be empty"); + } + String newPath = media.isCut() && !TextUtils.isEmpty(media.getCutPath()) ? media.getCutPath() : media.getRealPath(); + String suffix = Checker.SINGLE.extSuffix(media.getMimeType()); + File outFile = getImageCacheFile(context, path, TextUtils.isEmpty(suffix) ? Checker.SINGLE.extSuffix(path) : suffix); + String filename = ""; + if (!TextUtils.isEmpty(mNewFileName)) { + filename = isCamera || dataCount == 1 ? mNewFileName : StringUtils.rename(mNewFileName); + outFile = getImageCustomFile(context, filename); + } + // 如果文件存在直接返回不处理 + if (outFile.exists()) { + return outFile; + } + + if (mCompressionPredicate != null) { + if (Checker.SINGLE.extSuffix(path).startsWith(".gif")) { + // GIF without compression + if (SdkVersionUtils.checkedAndroid_Q()) { + if (media.isCut() && !TextUtils.isEmpty(media.getCutPath())) { + result = new File(media.getCutPath()); + } else { + String androidQToPath = AndroidQTransformUtils.copyPathToAndroidQ(context, path.getPath(), + media.getWidth(), media.getHeight(), media.getMimeType(), filename); + if (!TextUtils.isEmpty(androidQToPath)) { + result = new File(androidQToPath); + } + } + } else { + result = new File(newPath); + } + } else { + boolean isCompress = Checker.SINGLE.needCompressToLocalMedia(mLeastCompressSize, newPath); + if (mCompressionPredicate.apply(newPath) && isCompress) { + // 压缩 + result = new Engine(path, outFile, focusAlpha, compressQuality).compress(); + } else { + if (isCompress) { + // 压缩 + result = new Engine(path, outFile, focusAlpha, compressQuality).compress(); + } + } + } + } else { + if (Checker.SINGLE.extSuffix(path).startsWith(".gif")) { + // GIF without compression + if (SdkVersionUtils.checkedAndroid_Q()) { + String newFilePath = media.isCut() ? media.getCutPath() : + AndroidQTransformUtils.copyPathToAndroidQ(context, + path.getPath(), media.getWidth(), media.getHeight(), media.getMimeType(), filename); + if (!TextUtils.isEmpty(newFilePath)) { + result = new File(newFilePath); + } + } else { + result = new File(newPath); + } + } else { + boolean isCompress = Checker.SINGLE.needCompressToLocalMedia(mLeastCompressSize, newPath); + if (isCompress) { + // 压缩 + result = new Engine(path, outFile, focusAlpha, compressQuality).compress(); + } + } + } + return result; + } + + @Override + public boolean handleMessage(Message msg) { + if (mCompressListener == null) return false; + + switch (msg.what) { + case MSG_COMPRESS_START: + mCompressListener.onStart(); + break; + case MSG_COMPRESS_SUCCESS: + mCompressListener.onSuccess((List) msg.obj); + break; + case MSG_COMPRESS_ERROR: + mCompressListener.onError((Throwable) msg.obj); + break; + } + return false; + } + + public static class Builder { + private Context context; + private String mTargetDir; + private String mNewFileName; + private boolean focusAlpha; + private boolean isCamera; + private int compressQuality; + private int mLeastCompressSize = 100; + private OnRenameListener mRenameListener; + private OnCompressListener mCompressListener; + private CompressionPredicate mCompressionPredicate; + private List mStreamProviders; + private List mPaths; + private List mediaList; + private int dataCount; + private boolean isAndroidQ; + + Builder(Context context) { + this.context = context; + this.mPaths = new ArrayList<>(); + this.mediaList = new ArrayList<>(); + this.mStreamProviders = new ArrayList<>(); + this.isAndroidQ = SdkVersionUtils.checkedAndroid_Q(); + } + + private Luban build() { + return new Luban(this); + } + + public Builder load(InputStreamProvider inputStreamProvider) { + mStreamProviders.add(inputStreamProvider); + return this; + } + + /** + * 扩展符合PictureSelector的压缩策略 + * + * @param list LocalMedia集合 + * @param + * @return + */ + public Builder loadMediaData(List list) { + this.mediaList = list; + this.dataCount = list.size(); + for (LocalMedia src : list) { + load(src); + } + return this; + } + + + /** + * 扩展符合PictureSelector的压缩策略 + * + * @param media LocalMedia对象 + * @param + * @return + */ + private Builder load(final LocalMedia media) { + mStreamProviders.add(new InputStreamAdapter() { + @Override + public InputStream openInternal() throws IOException { + if (PictureMimeType.isContent(media.getPath()) && !media.isCut()) { + if (!TextUtils.isEmpty(media.getAndroidQToPath())) { + return new FileInputStream(media.getAndroidQToPath()); + } + return context.getContentResolver().openInputStream(Uri.parse(media.getPath())); + } else { + return PictureMimeType.isHasHttp(media.getPath()) ? null : new FileInputStream(media.isCut() ? media.getCutPath() : media.getPath()); + } + } + + @Override + public String getPath() { + if (media.isCut()) { + return media.getCutPath(); + } else { + return TextUtils.isEmpty(media.getAndroidQToPath()) ? media.getPath() : media.getAndroidQToPath(); + } + } + + @Override + public LocalMedia getMedia() { + return media; + } + }); + return this; + } + + public Builder load(final Uri uri) { + mStreamProviders.add(new InputStreamAdapter() { + @Override + public InputStream openInternal() throws IOException { + return context.getContentResolver().openInputStream(uri); + } + + @Override + public String getPath() { + return uri.getPath(); + } + + @Override + public LocalMedia getMedia() { + return null; + } + }); + return this; + } + + public Builder load(final File file) { + mStreamProviders.add(new InputStreamAdapter() { + @Override + public InputStream openInternal() throws IOException { + return new FileInputStream(file); + } + + @Override + public String getPath() { + return file.getAbsolutePath(); + } + + @Override + public LocalMedia getMedia() { + return null; + } + + }); + return this; + } + + public Builder load(final String string) { + mStreamProviders.add(new InputStreamAdapter() { + @Override + public InputStream openInternal() throws IOException { + return new FileInputStream(string); + } + + @Override + public String getPath() { + return string; + } + + @Override + public LocalMedia getMedia() { + return null; + } + + }); + return this; + } + + public Builder load(List list) { + for (T src : list) { + if (src instanceof String) { + load((String) src); + } else if (src instanceof File) { + load((File) src); + } else if (src instanceof Uri) { + load((Uri) src); + } else { + throw new IllegalArgumentException("Incoming data type exception, it must be String, File, Uri or Bitmap"); + } + } + return this; + } + + public Builder putGear(int gear) { + return this; + } + + @Deprecated + public Builder setRenameListener(OnRenameListener listener) { + this.mRenameListener = listener; + return this; + } + + public Builder setCompressListener(OnCompressListener listener) { + this.mCompressListener = listener; + return this; + } + + public Builder setTargetDir(String targetDir) { + this.mTargetDir = targetDir; + return this; + } + + public Builder setNewCompressFileName(String newFileName) { + this.mNewFileName = newFileName; + return this; + } + + public Builder isCamera(boolean isCamera) { + this.isCamera = isCamera; + return this; + } + + /** + * Do I need to keep the image's alpha channel + * + * @param focusAlpha

true - to keep alpha channel, the compress speed will be slow.

+ *

false - don't keep alpha channel, it might have a black background.

+ */ + public Builder setFocusAlpha(boolean focusAlpha) { + this.focusAlpha = focusAlpha; + return this; + } + + /** + * Image compressed output quality + * + * @param compressQuality The quality is better than + */ + public Builder setCompressQuality(int compressQuality) { + this.compressQuality = compressQuality; + return this; + } + + + /** + * do not compress when the origin image file size less than one value + * + * @param size the value of file size, unit KB, default 100K + */ + public Builder ignoreBy(int size) { + this.mLeastCompressSize = size; + return this; + } + + /** + * do compress image when return value was true, otherwise, do not compress the image file + * + * @param compressionPredicate A predicate callback that returns true or false for the given input path should be compressed. + */ + public Builder filter(CompressionPredicate compressionPredicate) { + this.mCompressionPredicate = compressionPredicate; + return this; + } + + + /** + * begin compress image with asynchronous + */ + public void launch() { + build().launch(context); + } + + public File get(final String path) throws IOException { + return build().get(new InputStreamAdapter() { + @Override + public InputStream openInternal() throws IOException { + return new FileInputStream(path); + } + + @Override + public String getPath() { + return path; + } + + @Override + public LocalMedia getMedia() { + return null; + } + + }, context); + } + + /** + * begin compress image with synchronize + * + * @return the thumb image file list + */ + public List get() throws IOException { + return build().get(context); + } + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/OnCompressListener.java b/picture_library/src/main/java/com/luck/picture/lib/compress/OnCompressListener.java new file mode 100644 index 0000000..d910e31 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/OnCompressListener.java @@ -0,0 +1,23 @@ +package com.luck.picture.lib.compress; + +import com.luck.picture.lib.entity.LocalMedia; + +import java.util.List; + +public interface OnCompressListener { + + /** + * Fired when the compression is started, override to handle in your own code + */ + void onStart(); + + /** + * Fired when a compression returns successfully, override to handle in your own code + */ + void onSuccess(List list); + + /** + * Fired when a compression fails to complete, override to handle in your own code + */ + void onError(Throwable e); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/compress/OnRenameListener.java b/picture_library/src/main/java/com/luck/picture/lib/compress/OnRenameListener.java new file mode 100644 index 0000000..efaea00 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/compress/OnRenameListener.java @@ -0,0 +1,22 @@ +package com.luck.picture.lib.compress; + +/** + * Author: zibin + * Datetime: 2018/5/18 + *

+ * 提供修改压缩图片命名接口 + *

+ * A functional interface (callback) that used to rename the file after compress. + */ +public interface OnRenameListener { + + /** + * 压缩前调用该方法用于修改压缩后文件名 + *

+ * Call before compression begins. + * + * @param filePath 传入文件路径/ file path + * @return 返回重命名后的字符串/ file name + */ + String rename(String filePath); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/config/PictureConfig.java b/picture_library/src/main/java/com/luck/picture/lib/config/PictureConfig.java new file mode 100644 index 0000000..7908090 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/config/PictureConfig.java @@ -0,0 +1,71 @@ +package com.luck.picture.lib.config; + +/** + * @author:luck + * @data:2017/5/24 1:00 + * @describe : constant + */ +public final class PictureConfig { + public final static int APPLY_STORAGE_PERMISSIONS_CODE = 1; + public final static int APPLY_CAMERA_PERMISSIONS_CODE = 2; + public final static int APPLY_AUDIO_PERMISSIONS_CODE = 3; + public final static int APPLY_RECORD_AUDIO_PERMISSIONS_CODE = 4; + public final static int APPLY_CAMERA_STORAGE_PERMISSIONS_CODE = 5; + + public final static String EXTRA_MEDIA_KEY = "mediaKey"; + public final static String EXTRA_MEDIA_PATH = "mediaPath"; + public final static String EXTRA_AUDIO_PATH = "audioPath"; + public final static String EXTRA_VIDEO_PATH = "videoPath"; + public final static String EXTRA_PREVIEW_VIDEO = "isExternalPreviewVideo"; + public final static String EXTRA_PREVIEW_DELETE_POSITION = "position"; + public final static String EXTRA_FC_TAG = "picture"; + public final static String EXTRA_RESULT_SELECTION = "extra_result_media"; + public final static String EXTRA_PREVIEW_SELECT_LIST = "previewSelectList"; + public final static String EXTRA_SELECT_LIST = "selectList"; + public final static String EXTRA_COMPLETE_SELECTED = "isCompleteOrSelected"; + public final static String EXTRA_CHANGE_SELECTED_DATA = "isChangeSelectedData"; + public final static String EXTRA_CHANGE_ORIGINAL = "isOriginal"; + public final static String EXTRA_POSITION = "position"; + public final static String EXTRA_OLD_CURRENT_LIST_SIZE = "oldCurrentListSize"; + public final static String EXTRA_DIRECTORY_PATH = "directory_path"; + public final static String EXTRA_BOTTOM_PREVIEW = "bottom_preview"; + public final static String EXTRA_CONFIG = "PictureSelectorConfig"; + public final static String EXTRA_SHOW_CAMERA = "isShowCamera"; + public final static String EXTRA_IS_CURRENT_DIRECTORY = "currentDirectory"; + public final static String EXTRA_BUCKET_ID = "bucket_id"; + public final static String EXTRA_PAGE = "page"; + public final static String EXTRA_DATA_COUNT = "count"; + public final static String CAMERA_FACING = "android.intent.extras.CAMERA_FACING"; + + public final static String EXTRA_ALL_FOLDER_SIZE = "all_folder_size"; + public final static String EXTRA_QUICK_CAPTURE = "android.intent.extra.quickCapture"; + + public final static int MAX_PAGE_SIZE = 60; + + public final static int MIN_PAGE_SIZE = 10; + + public final static int LOADED = 0; + + public final static int NORMAL = -1; + + public final static int CAMERA_BEFORE = 1; + + public final static int TYPE_ALL = 0; + public final static int TYPE_IMAGE = 1; + public final static int TYPE_VIDEO = 2; + + @Deprecated + public final static int TYPE_AUDIO = 3; + + public final static int MAX_COMPRESS_SIZE = 100; + + public final static int TYPE_CAMERA = 1; + public final static int TYPE_PICTURE = 2; + + public final static int SINGLE = 1; + public final static int MULTIPLE = 2; + + public final static int PREVIEW_VIDEO_CODE = 166; + public final static int CHOOSE_REQUEST = 188; + public final static int REQUEST_CAMERA = 909; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/config/PictureMimeType.java b/picture_library/src/main/java/com/luck/picture/lib/config/PictureMimeType.java new file mode 100644 index 0000000..bbfc235 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/config/PictureMimeType.java @@ -0,0 +1,345 @@ +package com.luck.picture.lib.config; + + +import android.content.Context; +import android.text.TextUtils; + +import com.luck.picture.lib.R; + +import java.io.File; + +/** + * @author:luck + * @date:2017-5-24 17:02 + * @describe:PictureMimeType + */ + +public final class PictureMimeType { + public static int ofAll() { + return PictureConfig.TYPE_ALL; + } + + public static int ofImage() { + return PictureConfig.TYPE_IMAGE; + } + + public static int ofVideo() { + return PictureConfig.TYPE_VIDEO; + } + + /** + * # No longer maintain audio related functions, + * but can continue to use but there will be phone compatibility issues + *

+ * 不再维护音频相关功能,但可以继续使用但会有机型兼容性问题 + */ + @Deprecated + public static int ofAudio() { + return PictureConfig.TYPE_AUDIO; + } + + + public static String ofPNG() { + return MIME_TYPE_PNG; + } + + public static String ofJPEG() { + return MIME_TYPE_JPEG; + } + + public static String ofBMP() { + return MIME_TYPE_BMP; + } + + public static String ofGIF() { + return MIME_TYPE_GIF; + } + + public static String ofWEBP() { + return MIME_TYPE_WEBP; + } + + public static String of3GP() { + return MIME_TYPE_3GP; + } + + public static String ofMP4() { + return MIME_TYPE_MP4; + } + + public static String ofMPEG() { + return MIME_TYPE_MPEG; + } + + public static String ofAVI() { + return MIME_TYPE_AVI; + } + + private final static String MIME_TYPE_PNG = "image/png"; + public final static String MIME_TYPE_JPEG = "image/jpeg"; + private final static String MIME_TYPE_JPG = "image/jpg"; + private final static String MIME_TYPE_BMP = "image/bmp"; + private final static String MIME_TYPE_GIF = "image/gif"; + private final static String MIME_TYPE_WEBP = "image/webp"; + + private final static String MIME_TYPE_3GP = "video/3gp"; + private final static String MIME_TYPE_MP4 = "video/mp4"; + private final static String MIME_TYPE_MPEG = "video/mpeg"; + private final static String MIME_TYPE_AVI = "video/avi"; + + + /** + * isGif + * + * @param mimeType + * @return + */ + public static boolean isGif(String mimeType) { + return mimeType != null && (mimeType.equals("image/gif") || mimeType.equals("image/GIF")); + } + + + /** + * isVideo + * + * @param mimeType + * @return + */ + public static boolean isHasVideo(String mimeType) { + return mimeType != null && mimeType.startsWith(MIME_TYPE_PREFIX_VIDEO); + } + + /** + * isVideo + * + * @param url + * @return + */ + public static boolean isUrlHasVideo(String url) { + return url.endsWith(".mp4"); + } + + /** + * isAudio + * + * @param mimeType + * @return + */ + public static boolean isHasAudio(String mimeType) { + return mimeType != null && mimeType.startsWith(MIME_TYPE_PREFIX_AUDIO); + } + + /** + * isImage + * + * @param mimeType + * @return + */ + public static boolean isHasImage(String mimeType) { + return mimeType != null && mimeType.startsWith(MIME_TYPE_PREFIX_IMAGE); + } + + /** + * Determine if it is JPG. + * + * @param is image file mimeType + */ + public static boolean isJPEG(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return false; + } + return mimeType.startsWith(MIME_TYPE_JPEG) || mimeType.startsWith(MIME_TYPE_JPG); + } + + /** + * Determine if it is JPG. + * + * @param is image file mimeType + */ + public static boolean isJPG(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return false; + } + return mimeType.startsWith(MIME_TYPE_JPG); + } + + + /** + * is Network image + * + * @param path + * @return + */ + public static boolean isHasHttp(String path) { + if (TextUtils.isEmpty(path)) { + return false; + } + return path.startsWith("http") + || path.startsWith("https") + || path.startsWith("/http") + || path.startsWith("/https"); + } + + /** + * Determine whether the file type is an image or a video + * + * @param cameraMimeType + * @return + */ + public static String getMimeType(int cameraMimeType) { + switch (cameraMimeType) { + case PictureConfig.TYPE_VIDEO: + return MIME_TYPE_VIDEO; + case PictureConfig.TYPE_AUDIO: + return MIME_TYPE_AUDIO; + default: + return MIME_TYPE_IMAGE; + } + } + + /** + * Determines if the file name is a picture + * + * @param name + * @return + */ + public static boolean isSuffixOfImage(String name) { + return !TextUtils.isEmpty(name) && name.endsWith(".PNG") || name.endsWith(".png") || name.endsWith(".jpeg") + || name.endsWith(".gif") || name.endsWith(".GIF") || name.endsWith(".jpg") + || name.endsWith(".webp") || name.endsWith(".WEBP") || name.endsWith(".JPEG") + || name.endsWith(".bmp"); + } + + /** + * Is it the same type + * + * @param oldMimeType + * @param newMimeType + * @return + */ + public static boolean isMimeTypeSame(String oldMimeType, String newMimeType) { + + return getMimeType(oldMimeType) == getMimeType(newMimeType); + } + + /** + * Get Image mimeType + * + * @param path + * @return + */ + public static String getImageMimeType(String path) { + try { + if (!TextUtils.isEmpty(path)) { + File file = new File(path); + String fileName = file.getName(); + int last = fileName.lastIndexOf(".") + 1; + String temp = fileName.substring(last); + return "image/" + temp; + } + } catch (Exception e) { + e.printStackTrace(); + return MIME_TYPE_IMAGE; + } + return MIME_TYPE_IMAGE; + } + + + /** + * Picture or video + * + * @return + */ + public static int getMimeType(String mimeType) { + if (TextUtils.isEmpty(mimeType)) { + return PictureConfig.TYPE_IMAGE; + } + if (mimeType.startsWith(MIME_TYPE_PREFIX_VIDEO)) { + return PictureConfig.TYPE_VIDEO; + } else if (mimeType.startsWith(MIME_TYPE_PREFIX_AUDIO)) { + return PictureConfig.TYPE_AUDIO; + } else { + return PictureConfig.TYPE_IMAGE; + } + } + + /** + * Get image suffix + * + * @param mineType + * @return + */ + public static String getLastImgSuffix(String mineType) { + String defaultSuffix = PNG; + try { + int index = mineType.lastIndexOf("/") + 1; + if (index > 0) { + return "." + mineType.substring(index); + } + } catch (Exception e) { + e.printStackTrace(); + return defaultSuffix; + } + return defaultSuffix; + } + + + /** + * is content:// + * + * @param url + * @return + */ + public static boolean isContent(String url) { + if (TextUtils.isEmpty(url)) { + return false; + } + return url.startsWith("content://"); + } + + /** + * Returns an error message by type + * + * @param context + * @param mimeType + * @return + */ + public static String s(Context context, String mimeType) { + Context ctx = context.getApplicationContext(); + if (isHasVideo(mimeType)) { + return ctx.getString(R.string.picture_video_error); + } else if (isHasAudio(mimeType)) { + return ctx.getString(R.string.picture_audio_error); + } else { + return ctx.getString(R.string.picture_error); + } + } + + public final static String JPEG = ".jpg"; + + private final static String PNG = ".png"; + + public final static String MP4 = ".mp4"; + + public final static String JPEG_Q = "image/jpeg"; + + public final static String PNG_Q = "image/png"; + + public final static String MP4_Q = "video/mp4"; + + public final static String AVI_Q = "video/avi"; + + public final static String DCIM = "DCIM/Camera"; + + public final static String CAMERA = "Camera"; + + public final static String MIME_TYPE_IMAGE = "image/jpeg"; + public final static String MIME_TYPE_VIDEO = "video/mp4"; + public final static String MIME_TYPE_AUDIO = "audio/mpeg"; + + + private final static String MIME_TYPE_PREFIX_IMAGE = "image"; + private final static String MIME_TYPE_PREFIX_VIDEO = "video"; + private final static String MIME_TYPE_PREFIX_AUDIO = "audio"; + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/config/PictureSelectionConfig.java b/picture_library/src/main/java/com/luck/picture/lib/config/PictureSelectionConfig.java new file mode 100644 index 0000000..36fdd31 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/config/PictureSelectionConfig.java @@ -0,0 +1,521 @@ +package com.luck.picture.lib.config; + +import android.content.pm.ActivityInfo; +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.ColorInt; +import androidx.annotation.StyleRes; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.camera.CustomCameraView; +import com.luck.picture.lib.engine.CacheResourcesEngine; +import com.luck.picture.lib.engine.ImageEngine; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnCustomCameraInterfaceListener; +import com.luck.picture.lib.listener.OnResultCallbackListener; +import com.luck.picture.lib.listener.OnVideoSelectedPlayCallback; +import com.luck.picture.lib.style.PictureCropParameterStyle; +import com.luck.picture.lib.style.PictureParameterStyle; +import com.luck.picture.lib.style.PictureWindowAnimationStyle; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2017-05-24 17:02 + * @describe:PictureSelector Config + */ + +public final class PictureSelectionConfig implements Parcelable { + public int chooseMode; + public boolean camera; + public boolean isSingleDirectReturn; + public PictureParameterStyle style; + public PictureCropParameterStyle cropStyle; + public PictureWindowAnimationStyle windowAnimationStyle; + public String compressSavePath; + public String suffixType; + public boolean focusAlpha; + public String renameCompressFileName; + public String renameCropFileName; + public String specifiedFormat; + public int requestedOrientation; + public int buttonFeatures; + public boolean isCameraAroundState; + public boolean isAndroidQTransform; + @StyleRes + public int themeStyleId; + public int selectionMode; + public int maxSelectNum; + public int minSelectNum; + public int maxVideoSelectNum; + public int minVideoSelectNum; + public int videoQuality; + public int cropCompressQuality; + public int videoMaxSecond; + public int videoMinSecond; + public int recordVideoSecond; + public int recordVideoMinSecond; + public int minimumCompressSize; + public int imageSpanCount; + public int aspect_ratio_x; + public int aspect_ratio_y; + public int cropWidth; + public int cropHeight; + public int compressQuality; + public float filterFileSize; + public int language; + public boolean isMultipleRecyclerAnimation; + public boolean isMultipleSkipCrop; + public boolean isWeChatStyle; + public boolean isUseCustomCamera; + public boolean zoomAnim; + public boolean isCompress; + public boolean isOriginalControl; + public boolean isCamera; + public boolean isGif; + public boolean enablePreview; + public boolean enPreviewVideo; + public boolean enablePreviewAudio; + public boolean checkNumMode; + public boolean openClickSound; + public boolean enableCrop; + public boolean freeStyleCropEnabled; + public boolean circleDimmedLayer; + @ColorInt + public int circleDimmedColor; + @ColorInt + public int circleDimmedBorderColor; + public int circleStrokeWidth; + public boolean showCropFrame; + public boolean showCropGrid; + public boolean hideBottomControls; + public boolean rotateEnabled; + public boolean scaleEnabled; + public boolean previewEggs; + public boolean synOrAsy; + public boolean returnEmpty; + public boolean isDragFrame; + public boolean isNotPreviewDownload; + public boolean isWithVideoImage; + public UCropOptions uCropOptions; + public static ImageEngine imageEngine; + public static CacheResourcesEngine cacheResourcesEngine; + public static OnResultCallbackListener listener; + public static OnVideoSelectedPlayCallback customVideoPlayCallback; + public static OnCustomCameraInterfaceListener onCustomCameraInterfaceListener; + public List selectionMedias; + public String cameraFileName; + public boolean isCheckOriginalImage; + @Deprecated + public int overrideWidth; + @Deprecated + public int overrideHeight; + @Deprecated + public float sizeMultiplier; + @Deprecated + public boolean isChangeStatusBarFontColor; + @Deprecated + public boolean isOpenStyleNumComplete; + @Deprecated + public boolean isOpenStyleCheckNumMode; + @Deprecated + public int titleBarBackgroundColor; + @Deprecated + public int pictureStatusBarColor; + @Deprecated + public int cropTitleBarBackgroundColor; + @Deprecated + public int cropStatusBarColorPrimaryDark; + @Deprecated + public int cropTitleColor; + @Deprecated + public int upResId; + @Deprecated + public int downResId; + public String outPutCameraPath; + + public String originalPath; + public String cameraPath; + public int cameraMimeType; + public int pageSize; + public boolean isPageStrategy; + public boolean isFilterInvalidFile; + public boolean isMaxSelectEnabledMask; + public int animationMode; + public boolean isAutomaticTitleRecyclerTop; + public boolean isCallbackMode; + public boolean isAndroidQChangeWH; + public boolean isAndroidQChangeVideoWH; + public boolean isQuickCapture; + /** + * 内测专用########### + */ + public boolean isFallbackVersion; + public boolean isFallbackVersion2; + public boolean isFallbackVersion3; + + protected void initDefaultValue() { + chooseMode = PictureMimeType.ofImage(); + camera = false; + themeStyleId = R.style.picture_default_style; + selectionMode = PictureConfig.MULTIPLE; + maxSelectNum = 9; + minSelectNum = 0; + maxVideoSelectNum = 0; + minVideoSelectNum = 0; + videoQuality = 1; + language = -1; + cropCompressQuality = 90; + videoMaxSecond = 0; + videoMinSecond = 0; + filterFileSize = -1; + recordVideoSecond = 60; + recordVideoMinSecond = 0; + compressQuality = 80; + minimumCompressSize = PictureConfig.MAX_COMPRESS_SIZE; + imageSpanCount = 4; + isCompress = false; + isOriginalControl = false; + aspect_ratio_x = 0; + aspect_ratio_y = 0; + cropWidth = 0; + cropHeight = 0; + requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR; + buttonFeatures = CustomCameraView.BUTTON_STATE_BOTH; //初始化按钮为可录制可拍照 + isCameraAroundState = false; + isWithVideoImage = false; + isAndroidQTransform = true; + isCamera = true; + isGif = false; + focusAlpha = false; + isCheckOriginalImage = false; + isSingleDirectReturn = false; + enablePreview = true; + enPreviewVideo = true; + enablePreviewAudio = true; + checkNumMode = false; + isNotPreviewDownload = false; + openClickSound = false; + isFallbackVersion = false; + isFallbackVersion2 = true; + isFallbackVersion3 = true; + enableCrop = false; + isWeChatStyle = false; + isUseCustomCamera = false; + isMultipleSkipCrop = true; + isMultipleRecyclerAnimation = true; + freeStyleCropEnabled = false; + circleDimmedLayer = false; + showCropFrame = true; + showCropGrid = true; + hideBottomControls = true; + rotateEnabled = true; + scaleEnabled = true; + previewEggs = false; + returnEmpty = false; + synOrAsy = true; + zoomAnim = true; + circleDimmedColor = 0; + circleDimmedBorderColor = 0; + circleStrokeWidth = 1; + isDragFrame = true; + compressSavePath = ""; + suffixType = ""; + cameraFileName = ""; + specifiedFormat = ""; + renameCompressFileName = ""; + renameCropFileName = ""; + selectionMedias = new ArrayList<>(); + uCropOptions = null; + style = null; + cropStyle = null; + windowAnimationStyle = null; + titleBarBackgroundColor = 0; + pictureStatusBarColor = 0; + cropTitleBarBackgroundColor = 0; + cropStatusBarColorPrimaryDark = 0; + cropTitleColor = 0; + upResId = 0; + downResId = 0; + isChangeStatusBarFontColor = false; + isOpenStyleNumComplete = false; + isOpenStyleCheckNumMode = false; + outPutCameraPath = ""; + sizeMultiplier = 0.5f; + overrideWidth = 0; + overrideHeight = 0; + originalPath = ""; + cameraPath = ""; + cameraMimeType = -1; + pageSize = PictureConfig.MAX_PAGE_SIZE; + isPageStrategy = true; + isFilterInvalidFile = false; + isMaxSelectEnabledMask = false; + animationMode = -1; + isAutomaticTitleRecyclerTop = true; + isCallbackMode = false; + isAndroidQChangeWH = true; + isAndroidQChangeVideoWH = false; + isQuickCapture = true; + } + + public static PictureSelectionConfig getInstance() { + return InstanceHolder.INSTANCE; + } + + public static PictureSelectionConfig getCleanInstance() { + PictureSelectionConfig selectionSpec = getInstance(); + selectionSpec.initDefaultValue(); + return selectionSpec; + } + + private static final class InstanceHolder { + private static final PictureSelectionConfig INSTANCE = new PictureSelectionConfig(); + } + + public PictureSelectionConfig() { + } + + /** + * 释放监听器 + */ + public static void destroy() { + PictureSelectionConfig.listener = null; + PictureSelectionConfig.customVideoPlayCallback = null; + PictureSelectionConfig.onCustomCameraInterfaceListener = null; + PictureSelectionConfig.onCustomCameraInterfaceListener = null; + PictureSelectionConfig.cacheResourcesEngine = null; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.chooseMode); + dest.writeByte(this.camera ? (byte) 1 : (byte) 0); + dest.writeByte(this.isSingleDirectReturn ? (byte) 1 : (byte) 0); + dest.writeParcelable(this.style, flags); + dest.writeParcelable(this.cropStyle, flags); + dest.writeParcelable(this.windowAnimationStyle, flags); + dest.writeString(this.compressSavePath); + dest.writeString(this.suffixType); + dest.writeByte(this.focusAlpha ? (byte) 1 : (byte) 0); + dest.writeString(this.renameCompressFileName); + dest.writeString(this.renameCropFileName); + dest.writeString(this.specifiedFormat); + dest.writeInt(this.requestedOrientation); + dest.writeInt(this.buttonFeatures); + dest.writeByte(this.isCameraAroundState ? (byte) 1 : (byte) 0); + dest.writeByte(this.isAndroidQTransform ? (byte) 1 : (byte) 0); + dest.writeInt(this.themeStyleId); + dest.writeInt(this.selectionMode); + dest.writeInt(this.maxSelectNum); + dest.writeInt(this.minSelectNum); + dest.writeInt(this.maxVideoSelectNum); + dest.writeInt(this.minVideoSelectNum); + dest.writeInt(this.videoQuality); + dest.writeInt(this.cropCompressQuality); + dest.writeInt(this.videoMaxSecond); + dest.writeInt(this.videoMinSecond); + dest.writeInt(this.recordVideoSecond); + dest.writeInt(this.recordVideoMinSecond); + dest.writeInt(this.minimumCompressSize); + dest.writeInt(this.imageSpanCount); + dest.writeInt(this.aspect_ratio_x); + dest.writeInt(this.aspect_ratio_y); + dest.writeInt(this.cropWidth); + dest.writeInt(this.cropHeight); + dest.writeInt(this.compressQuality); + dest.writeFloat(this.filterFileSize); + dest.writeInt(this.language); + dest.writeByte(this.isMultipleRecyclerAnimation ? (byte) 1 : (byte) 0); + dest.writeByte(this.isMultipleSkipCrop ? (byte) 1 : (byte) 0); + dest.writeByte(this.isWeChatStyle ? (byte) 1 : (byte) 0); + dest.writeByte(this.isUseCustomCamera ? (byte) 1 : (byte) 0); + dest.writeByte(this.zoomAnim ? (byte) 1 : (byte) 0); + dest.writeByte(this.isCompress ? (byte) 1 : (byte) 0); + dest.writeByte(this.isOriginalControl ? (byte) 1 : (byte) 0); + dest.writeByte(this.isCamera ? (byte) 1 : (byte) 0); + dest.writeByte(this.isGif ? (byte) 1 : (byte) 0); + dest.writeByte(this.enablePreview ? (byte) 1 : (byte) 0); + dest.writeByte(this.enPreviewVideo ? (byte) 1 : (byte) 0); + dest.writeByte(this.enablePreviewAudio ? (byte) 1 : (byte) 0); + dest.writeByte(this.checkNumMode ? (byte) 1 : (byte) 0); + dest.writeByte(this.openClickSound ? (byte) 1 : (byte) 0); + dest.writeByte(this.enableCrop ? (byte) 1 : (byte) 0); + dest.writeByte(this.freeStyleCropEnabled ? (byte) 1 : (byte) 0); + dest.writeByte(this.circleDimmedLayer ? (byte) 1 : (byte) 0); + dest.writeInt(this.circleDimmedColor); + dest.writeInt(this.circleDimmedBorderColor); + dest.writeInt(this.circleStrokeWidth); + dest.writeByte(this.showCropFrame ? (byte) 1 : (byte) 0); + dest.writeByte(this.showCropGrid ? (byte) 1 : (byte) 0); + dest.writeByte(this.hideBottomControls ? (byte) 1 : (byte) 0); + dest.writeByte(this.rotateEnabled ? (byte) 1 : (byte) 0); + dest.writeByte(this.scaleEnabled ? (byte) 1 : (byte) 0); + dest.writeByte(this.previewEggs ? (byte) 1 : (byte) 0); + dest.writeByte(this.synOrAsy ? (byte) 1 : (byte) 0); + dest.writeByte(this.returnEmpty ? (byte) 1 : (byte) 0); + dest.writeByte(this.isDragFrame ? (byte) 1 : (byte) 0); + dest.writeByte(this.isNotPreviewDownload ? (byte) 1 : (byte) 0); + dest.writeByte(this.isWithVideoImage ? (byte) 1 : (byte) 0); + dest.writeParcelable(this.uCropOptions, flags); + dest.writeTypedList(this.selectionMedias); + dest.writeString(this.cameraFileName); + dest.writeByte(this.isCheckOriginalImage ? (byte) 1 : (byte) 0); + dest.writeInt(this.overrideWidth); + dest.writeInt(this.overrideHeight); + dest.writeFloat(this.sizeMultiplier); + dest.writeByte(this.isChangeStatusBarFontColor ? (byte) 1 : (byte) 0); + dest.writeByte(this.isOpenStyleNumComplete ? (byte) 1 : (byte) 0); + dest.writeByte(this.isOpenStyleCheckNumMode ? (byte) 1 : (byte) 0); + dest.writeInt(this.titleBarBackgroundColor); + dest.writeInt(this.pictureStatusBarColor); + dest.writeInt(this.cropTitleBarBackgroundColor); + dest.writeInt(this.cropStatusBarColorPrimaryDark); + dest.writeInt(this.cropTitleColor); + dest.writeInt(this.upResId); + dest.writeInt(this.downResId); + dest.writeString(this.outPutCameraPath); + dest.writeString(this.originalPath); + dest.writeString(this.cameraPath); + dest.writeInt(this.cameraMimeType); + dest.writeInt(this.pageSize); + dest.writeByte(this.isPageStrategy ? (byte) 1 : (byte) 0); + dest.writeByte(this.isFilterInvalidFile ? (byte) 1 : (byte) 0); + dest.writeByte(this.isMaxSelectEnabledMask ? (byte) 1 : (byte) 0); + dest.writeInt(this.animationMode); + dest.writeByte(this.isAutomaticTitleRecyclerTop ? (byte) 1 : (byte) 0); + dest.writeByte(this.isCallbackMode ? (byte) 1 : (byte) 0); + dest.writeByte(this.isAndroidQChangeWH ? (byte) 1 : (byte) 0); + dest.writeByte(this.isAndroidQChangeVideoWH ? (byte) 1 : (byte) 0); + dest.writeByte(this.isQuickCapture ? (byte) 1 : (byte) 0); + dest.writeByte(this.isFallbackVersion ? (byte) 1 : (byte) 0); + dest.writeByte(this.isFallbackVersion2 ? (byte) 1 : (byte) 0); + dest.writeByte(this.isFallbackVersion3 ? (byte) 1 : (byte) 0); + } + + protected PictureSelectionConfig(Parcel in) { + this.chooseMode = in.readInt(); + this.camera = in.readByte() != 0; + this.isSingleDirectReturn = in.readByte() != 0; + this.style = in.readParcelable(PictureParameterStyle.class.getClassLoader()); + this.cropStyle = in.readParcelable(PictureCropParameterStyle.class.getClassLoader()); + this.windowAnimationStyle = in.readParcelable(PictureWindowAnimationStyle.class.getClassLoader()); + this.compressSavePath = in.readString(); + this.suffixType = in.readString(); + this.focusAlpha = in.readByte() != 0; + this.renameCompressFileName = in.readString(); + this.renameCropFileName = in.readString(); + this.specifiedFormat = in.readString(); + this.requestedOrientation = in.readInt(); + this.buttonFeatures = in.readInt(); + this.isCameraAroundState = in.readByte() != 0; + this.isAndroidQTransform = in.readByte() != 0; + this.themeStyleId = in.readInt(); + this.selectionMode = in.readInt(); + this.maxSelectNum = in.readInt(); + this.minSelectNum = in.readInt(); + this.maxVideoSelectNum = in.readInt(); + this.minVideoSelectNum = in.readInt(); + this.videoQuality = in.readInt(); + this.cropCompressQuality = in.readInt(); + this.videoMaxSecond = in.readInt(); + this.videoMinSecond = in.readInt(); + this.recordVideoSecond = in.readInt(); + this.recordVideoMinSecond = in.readInt(); + this.minimumCompressSize = in.readInt(); + this.imageSpanCount = in.readInt(); + this.aspect_ratio_x = in.readInt(); + this.aspect_ratio_y = in.readInt(); + this.cropWidth = in.readInt(); + this.cropHeight = in.readInt(); + this.compressQuality = in.readInt(); + this.filterFileSize = in.readFloat(); + this.language = in.readInt(); + this.isMultipleRecyclerAnimation = in.readByte() != 0; + this.isMultipleSkipCrop = in.readByte() != 0; + this.isWeChatStyle = in.readByte() != 0; + this.isUseCustomCamera = in.readByte() != 0; + this.zoomAnim = in.readByte() != 0; + this.isCompress = in.readByte() != 0; + this.isOriginalControl = in.readByte() != 0; + this.isCamera = in.readByte() != 0; + this.isGif = in.readByte() != 0; + this.enablePreview = in.readByte() != 0; + this.enPreviewVideo = in.readByte() != 0; + this.enablePreviewAudio = in.readByte() != 0; + this.checkNumMode = in.readByte() != 0; + this.openClickSound = in.readByte() != 0; + this.enableCrop = in.readByte() != 0; + this.freeStyleCropEnabled = in.readByte() != 0; + this.circleDimmedLayer = in.readByte() != 0; + this.circleDimmedColor = in.readInt(); + this.circleDimmedBorderColor = in.readInt(); + this.circleStrokeWidth = in.readInt(); + this.showCropFrame = in.readByte() != 0; + this.showCropGrid = in.readByte() != 0; + this.hideBottomControls = in.readByte() != 0; + this.rotateEnabled = in.readByte() != 0; + this.scaleEnabled = in.readByte() != 0; + this.previewEggs = in.readByte() != 0; + this.synOrAsy = in.readByte() != 0; + this.returnEmpty = in.readByte() != 0; + this.isDragFrame = in.readByte() != 0; + this.isNotPreviewDownload = in.readByte() != 0; + this.isWithVideoImage = in.readByte() != 0; + this.uCropOptions = in.readParcelable(UCropOptions.class.getClassLoader()); + this.selectionMedias = in.createTypedArrayList(LocalMedia.CREATOR); + this.cameraFileName = in.readString(); + this.isCheckOriginalImage = in.readByte() != 0; + this.overrideWidth = in.readInt(); + this.overrideHeight = in.readInt(); + this.sizeMultiplier = in.readFloat(); + this.isChangeStatusBarFontColor = in.readByte() != 0; + this.isOpenStyleNumComplete = in.readByte() != 0; + this.isOpenStyleCheckNumMode = in.readByte() != 0; + this.titleBarBackgroundColor = in.readInt(); + this.pictureStatusBarColor = in.readInt(); + this.cropTitleBarBackgroundColor = in.readInt(); + this.cropStatusBarColorPrimaryDark = in.readInt(); + this.cropTitleColor = in.readInt(); + this.upResId = in.readInt(); + this.downResId = in.readInt(); + this.outPutCameraPath = in.readString(); + this.originalPath = in.readString(); + this.cameraPath = in.readString(); + this.cameraMimeType = in.readInt(); + this.pageSize = in.readInt(); + this.isPageStrategy = in.readByte() != 0; + this.isFilterInvalidFile = in.readByte() != 0; + this.isMaxSelectEnabledMask = in.readByte() != 0; + this.animationMode = in.readInt(); + this.isAutomaticTitleRecyclerTop = in.readByte() != 0; + this.isCallbackMode = in.readByte() != 0; + this.isAndroidQChangeWH = in.readByte() != 0; + this.isAndroidQChangeVideoWH = in.readByte() != 0; + this.isQuickCapture = in.readByte() != 0; + this.isFallbackVersion = in.readByte() != 0; + this.isFallbackVersion2 = in.readByte() != 0; + this.isFallbackVersion3 = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public PictureSelectionConfig createFromParcel(Parcel source) { + return new PictureSelectionConfig(source); + } + + @Override + public PictureSelectionConfig[] newArray(int size) { + return new PictureSelectionConfig[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/config/UCropOptions.java b/picture_library/src/main/java/com/luck/picture/lib/config/UCropOptions.java new file mode 100644 index 0000000..e5bcf24 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/config/UCropOptions.java @@ -0,0 +1,41 @@ +package com.luck.picture.lib.config; + +import android.os.Parcel; +import android.os.Parcelable; + +import com.yalantis.ucrop.UCrop; + +/** + * @author:luck + * @date:2020-01-09 13:33 + * @describe: UCrop Configuration items + */ +public class UCropOptions extends UCrop.Options implements Parcelable { + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + } + + public UCropOptions() { + } + + protected UCropOptions(Parcel in) { + } + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + @Override + public UCropOptions createFromParcel(Parcel source) { + return new UCropOptions(source); + } + + @Override + public UCropOptions[] newArray(int size) { + return new UCropOptions[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/crash/PictureSelectorCrashUtils.java b/picture_library/src/main/java/com/luck/picture/lib/crash/PictureSelectorCrashUtils.java new file mode 100644 index 0000000..3004243 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/crash/PictureSelectorCrashUtils.java @@ -0,0 +1,207 @@ +package com.luck.picture.lib.crash; + +import android.content.pm.PackageInfo; +import android.content.pm.PackageManager; +import android.os.Build; +import android.os.Environment; + +import androidx.annotation.NonNull; + +import com.luck.picture.lib.app.PictureAppMaster; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.text.Format; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +/** + * @author:luck + * @date:2019-12-03 14:53 + * @describe:PictureSelector Crash Log collection class + */ +public class PictureSelectorCrashUtils { + private static boolean mInitialized; + private static String defaultDir; + private static String dir; + private static String versionName; + private static int versionCode; + + private static final String FILE_SEP = System.getProperty("file.separator"); + private static final Format FORMAT = new SimpleDateFormat("MM-dd HH-mm-ss", Locale.getDefault()); + + private static final String CRASH_HEAD; + + private static final Thread.UncaughtExceptionHandler UNCAUGHT_EXCEPTION_HANDLER; + + + static { + try { + PackageInfo pi = PictureAppMaster.getInstance().getAppContext() + .getPackageManager() + .getPackageInfo(PictureAppMaster.getInstance().getAppContext().getPackageName(), 0); + if (pi != null) { + versionName = pi.versionName; + versionCode = pi.versionCode; + } + } catch (PackageManager.NameNotFoundException e) { + e.printStackTrace(); + } + + CRASH_HEAD = "\n************* Crash Log Head ****************" + + "\nDevice Manufacturer: " + Build.MANUFACTURER + // 设备厂商 + "\nDevice Model : " + Build.MODEL + // 设备型号 + "\nAndroid Version : " + Build.VERSION.RELEASE + // 系统版本 + "\nAndroid SDK : " + Build.VERSION.SDK_INT + // SDK版本 + "\nApp VersionName : " + versionName + + "\nApp VersionCode : " + versionCode + + "\n************* Crash Log Head ****************\n\n"; + + + UNCAUGHT_EXCEPTION_HANDLER = new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(final Thread t, final Throwable e) { + + if (mFinishAppListener != null) { + mFinishAppListener.onFinishApp(t, e); + } + + Date now = new Date(System.currentTimeMillis()); + String fileName = FORMAT.format(now) + ".txt"; + final String fullPath = (dir == null ? defaultDir : dir) + fileName; + if (createOrExistsFile(fullPath)) { + PrintWriter pw = null; + try { + pw = new PrintWriter(new FileWriter(fullPath, false)); + pw.write(CRASH_HEAD); + e.printStackTrace(pw); + Throwable cause = e.getCause(); + while (cause != null) { + cause.printStackTrace(pw); + cause = cause.getCause(); + } + } catch (IOException ioe) { + ioe.printStackTrace(); + } finally { + if (pw != null) { + pw.close(); + } + } + } + android.os.Process.killProcess(android.os.Process.myPid()); + System.exit(0); + } + }; + } + + + private PictureSelectorCrashUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * 初始化 + *

需添加权限 {@code }

+ * + * @return {@code true}: 初始化成功
{@code false}: 初始化失败 + */ + public static boolean init() { + return init("", null); + } + + + /** + * 初始化 + *

需添加权限 {@code }

+ * + * @return {@code true}: 初始化成功
{@code false}: 初始化失败 + */ + public static boolean init(CrashAppListener listener) { + return init("", listener); + } + + + /** + * 初始化 + *

需添加权限 {@code }

+ * + * @param crashDir 崩溃文件存储目录 + * @return {@code true}: 初始化成功
{@code false}: 初始化失败 + */ + public static boolean init(@NonNull final File crashDir) { + return init(crashDir.getAbsolutePath() + FILE_SEP, null); + } + + /** + * 初始化 + *

需添加权限 {@code }

+ * + * @param crashDir 崩溃文件存储目录 + * @return {@code true}: 初始化成功
{@code false}: 初始化失败 + */ + public static boolean init(final String crashDir, CrashAppListener listener) { + mFinishAppListener = listener; + if (isSpace(crashDir)) { + dir = null; + } else { + dir = crashDir.endsWith(FILE_SEP) ? dir : dir + FILE_SEP; + } + if (mInitialized) { + return true; + } + if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState()) + && PictureAppMaster.getInstance().getAppContext().getExternalCacheDir() != null) { + defaultDir = PictureAppMaster.getInstance().getAppContext().getExternalCacheDir() + FILE_SEP + "crash" + FILE_SEP; + } else { + defaultDir = PictureAppMaster.getInstance().getAppContext().getCacheDir() + FILE_SEP + "crash" + FILE_SEP; + } + Thread.setDefaultUncaughtExceptionHandler(UNCAUGHT_EXCEPTION_HANDLER); + return mInitialized = true; + } + + private static boolean createOrExistsFile(String filePath) { + File file = new File(filePath); + if (file.exists()) { + return file.isFile(); + } + if (!createOrExistsDir(file.getParentFile())) { + return false; + } + try { + return file.createNewFile(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + private static boolean createOrExistsDir(File file) { + return file != null && (file.exists() ? file.isDirectory() : file.mkdirs()); + } + + private static boolean isSpace(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 CrashAppListener mFinishAppListener = null; + + public static void setCrashListener(CrashAppListener crashListener) { + mFinishAppListener = crashListener; + } + + public interface CrashAppListener { + void onFinishApp(final Thread t, final Throwable e); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/decoration/GridSpacingItemDecoration.java b/picture_library/src/main/java/com/luck/picture/lib/decoration/GridSpacingItemDecoration.java new file mode 100644 index 0000000..8c31327 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/decoration/GridSpacingItemDecoration.java @@ -0,0 +1,46 @@ +package com.luck.picture.lib.decoration; + +import android.graphics.Rect; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +/** + * @author:luck + * @data:2016/12/27 下午23:50 + * @describe:GridSpacingItemDecoration + */ + +public class GridSpacingItemDecoration extends RecyclerView.ItemDecoration { + + private int spanCount; + private int spacing; + private boolean includeEdge; + + public GridSpacingItemDecoration(int spanCount, int spacing, boolean includeEdge) { + this.spanCount = spanCount; + this.spacing = spacing; + this.includeEdge = includeEdge; + } + + @Override + public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) { + int position = parent.getChildAdapterPosition(view); + int column = position % spanCount; + if (includeEdge) { + outRect.left = spacing - column * spacing / spanCount; + outRect.right = (column + 1) * spacing / spanCount; + if (position < spanCount) { + outRect.top = spacing; + } + outRect.bottom = spacing; + } else { + outRect.left = column * spacing / spanCount; + outRect.right = spacing - (column + 1) * spacing / spanCount; + if (position < spanCount) { + outRect.top = spacing; + } + outRect.bottom = spacing; + } + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/decoration/WrapContentLinearLayoutManager.java b/picture_library/src/main/java/com/luck/picture/lib/decoration/WrapContentLinearLayoutManager.java new file mode 100644 index 0000000..24d4abf --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/decoration/WrapContentLinearLayoutManager.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2018. + * Author:Zhao + * Email:joeyzhao1005@gmail.com + */ + +package com.luck.picture.lib.decoration; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +/** + * Created by luck on 2017/12/4. + *

+ * RecyclerView Bug:IndexOutOfBoundsException: Inconsistency detected. Invalid view holder adapter的解决方案 + */ + +public class WrapContentLinearLayoutManager extends LinearLayoutManager { + public WrapContentLinearLayoutManager(Context context) { + super(context); + } + + public WrapContentLinearLayoutManager(Context context, int orientation, boolean reverseLayout) { + super(context, orientation, reverseLayout); + } + + public WrapContentLinearLayoutManager(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + public void onLayoutChildren(RecyclerView.Recycler recycler, RecyclerView.State state) { + try { + super.onLayoutChildren(recycler, state); + } catch (IndexOutOfBoundsException e) { + e.printStackTrace(); + } + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/dialog/PhotoItemSelectedDialog.java b/picture_library/src/main/java/com/luck/picture/lib/dialog/PhotoItemSelectedDialog.java new file mode 100644 index 0000000..1600078 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/dialog/PhotoItemSelectedDialog.java @@ -0,0 +1,110 @@ +package com.luck.picture.lib.dialog; + +import android.app.Dialog; +import android.os.Bundle; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentTransaction; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.listener.OnItemClickListener; +import com.luck.picture.lib.tools.ScreenUtils; + +/** + * @author:luck + * @date:2019-12-12 16:39 + * @describe:PhotoSelectedDialog + */ +public class PhotoItemSelectedDialog extends DialogFragment implements View.OnClickListener { + public static final int IMAGE_CAMERA = 0; + public static final int VIDEO_CAMERA = 1; + private TextView tvPicturePhoto, tvPictureVideo, tvPictureCancel; + + public static PhotoItemSelectedDialog newInstance() { + PhotoItemSelectedDialog selectedDialog = new PhotoItemSelectedDialog(); + return selectedDialog; + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + if (getDialog() != null) { + getDialog().requestWindowFeature(Window.FEATURE_NO_TITLE); + if (getDialog().getWindow() != null) { + getDialog().getWindow().setBackgroundDrawableResource(android.R.color.transparent); + } + } + return inflater.inflate(R.layout.picture_dialog_camera_selected, container); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + super.onViewCreated(view, savedInstanceState); + tvPicturePhoto = view.findViewById(R.id.picture_tv_photo); + tvPictureVideo = view.findViewById(R.id.picture_tv_video); + tvPictureCancel = view.findViewById(R.id.picture_tv_cancel); + tvPictureVideo.setOnClickListener(this); + tvPicturePhoto.setOnClickListener(this); + tvPictureCancel.setOnClickListener(this); + } + + @Override + public void onStart() { + super.onStart(); + initDialogStyle(); + } + + /** + * DialogFragment Style + */ + private void initDialogStyle() { + Dialog dialog = getDialog(); + if (dialog != null) { + Window window = dialog.getWindow(); + if (window != null) { + window.setLayout(ScreenUtils.getScreenWidth(getContext()), RelativeLayout.LayoutParams.WRAP_CONTENT); + window.setGravity(Gravity.BOTTOM); + window.setWindowAnimations(R.style.PictureThemeDialogFragmentAnim); + } + } + } + + private OnItemClickListener onItemClickListener; + + public void setOnItemClickListener(OnItemClickListener onItemClickListener) { + this.onItemClickListener = onItemClickListener; + } + + @Override + public void onClick(View v) { + int id = v.getId(); + if (onItemClickListener != null) { + if (id == R.id.picture_tv_photo) { + onItemClickListener.onItemClick(v, IMAGE_CAMERA); + } + if (id == R.id.picture_tv_video) { + onItemClickListener.onItemClick(v, VIDEO_CAMERA); + } + } + + dismissAllowingStateLoss(); + } + + @Override + public void show(FragmentManager manager, String tag) { + FragmentTransaction ft = manager.beginTransaction(); + ft.add(this, tag); + ft.commitAllowingStateLoss(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureCustomDialog.java b/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureCustomDialog.java new file mode 100644 index 0000000..82aea91 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureCustomDialog.java @@ -0,0 +1,24 @@ +package com.luck.picture.lib.dialog; + +import android.app.Dialog; +import android.content.Context; +import android.view.Gravity; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import com.luck.picture.lib.R; + +public class PictureCustomDialog extends Dialog { + + public PictureCustomDialog(Context context, int layout) { + super(context, R.style.Picture_Theme_Dialog); + setContentView(layout); + Window window = getWindow(); + WindowManager.LayoutParams params = window.getAttributes(); + params.width = ViewGroup.LayoutParams.WRAP_CONTENT; + params.height = ViewGroup.LayoutParams.WRAP_CONTENT; + params.gravity = Gravity.CENTER; + window.setAttributes(params); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureLoadingDialog.java b/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureLoadingDialog.java new file mode 100644 index 0000000..9554ecb --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/dialog/PictureLoadingDialog.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.dialog; + + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.view.Window; + +import com.luck.picture.lib.R; + +public class PictureLoadingDialog extends Dialog { + + public PictureLoadingDialog(Context context) { + super(context, R.style.Picture_Theme_AlertDialog); + setCancelable(true); + setCanceledOnTouchOutside(false); + Window window = getWindow(); + window.setWindowAnimations(R.style.PictureThemeDialogWindowStyle); + } + + + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.picture_alert_dialog); + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/engine/CacheResourcesEngine.java b/picture_library/src/main/java/com/luck/picture/lib/engine/CacheResourcesEngine.java new file mode 100644 index 0000000..07dea2d --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/engine/CacheResourcesEngine.java @@ -0,0 +1,18 @@ +package com.luck.picture.lib.engine; + +import android.content.Context; + +/** + * @author:luck + * @date:2020-03-24 09:36 + * @describe:CacheResourcesEngine + */ +public interface CacheResourcesEngine { + /** + * Get the cache path + * + * @param context + * @param url + */ + String onCachePath(Context context, String url); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/engine/ImageEngine.java b/picture_library/src/main/java/com/luck/picture/lib/engine/ImageEngine.java new file mode 100644 index 0000000..9fa7ac5 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/engine/ImageEngine.java @@ -0,0 +1,72 @@ +package com.luck.picture.lib.engine; + +import android.content.Context; +import android.widget.ImageView; + +import androidx.annotation.NonNull; + +import com.luck.picture.lib.listener.OnImageCompleteCallback; +import com.luck.picture.lib.widget.longimage.SubsamplingScaleImageView; + +/** + * @author:luck + * @date:2019-11-13 16:59 + * @describe:ImageEngine + */ +public interface ImageEngine { + /** + * Loading image + * + * @param context + * @param url + * @param imageView + */ + void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView); + + /** + * Loading image + * + * @param context + * @param url + * @param imageView + */ + void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView, OnImageCompleteCallback callback); + + /** + * Load network long graph adaption + * + * @param context + * @param url + * @param imageView + */ + @Deprecated + void loadImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView, SubsamplingScaleImageView longImageView); + + + /** + * Load album catalog pictures + * + * @param context + * @param url + * @param imageView + */ + void loadFolderImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView); + + /** + * Load GIF image + * + * @param context + * @param url + * @param imageView + */ + void loadAsGifImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView); + + /** + * Load picture list picture + * + * @param context + * @param url + * @param imageView + */ + void loadGridImage(@NonNull Context context, @NonNull String url, @NonNull ImageView imageView); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/engine/PictureSelectorEngine.java b/picture_library/src/main/java/com/luck/picture/lib/engine/PictureSelectorEngine.java new file mode 100644 index 0000000..bc20a85 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/engine/PictureSelectorEngine.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.engine; + +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnResultCallbackListener; + +/** + * @author:luck + * @date:2020/4/22 11:36 AM + * @describe:PictureSelectorEngine + */ +public interface PictureSelectorEngine { + + /** + * Create ImageLoad Engine + * + * @return + */ + ImageEngine createEngine(); + + /** + * Create Result Listener + * + * @return + */ + OnResultCallbackListener getResultCallbackListener(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMedia.java b/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMedia.java new file mode 100644 index 0000000..09b1ed4 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMedia.java @@ -0,0 +1,471 @@ +package com.luck.picture.lib.entity; + + +import android.os.Parcel; +import android.os.Parcelable; +import android.text.TextUtils; + +import com.luck.picture.lib.config.PictureConfig; + +/** + * @author:luck + * @date:2017-5-24 16:21 + * @describe:Media Entity + */ + +public class LocalMedia implements Parcelable { + /** + * file to ID + */ + private long id; + /** + * original path + */ + private String path; + + /** + * The real path,But you can't get access from AndroidQ + *

+ * It could be empty + *

+ */ + private String realPath; + + /** + * # Check the original button to get the return value + * original path + */ + private String originalPath; + /** + * compress path + */ + private String compressPath; + /** + * cut path + */ + private String cutPath; + + /** + * Note: this field is only returned in Android Q version + *

+ * Android Q image or video path + */ + private String androidQToPath; + /** + * video duration + */ + private long duration; + /** + * If the selected + * # Internal use + */ + private boolean isChecked; + /** + * If the cut + */ + private boolean isCut; + /** + * media position of list + */ + public int position; + /** + * The media number of qq choose styles + */ + private int num; + /** + * The media resource type + */ + private String mimeType; + + /** + * Gallery selection mode + */ + private int chooseModel; + + /** + * If the compressed + */ + private boolean compressed; + /** + * image or video width + *

+ * # If zero occurs, the developer needs to handle it extra + */ + private int width; + /** + * image or video height + *

+ * # If zero occurs, the developer needs to handle it extra + */ + private int height; + + /** + * file size + */ + private long size; + + /** + * Whether the original image is displayed + */ + private boolean isOriginal; + + /** + * file name + */ + private String fileName; + + /** + * Parent Folder Name + */ + private String parentFolderName; + + /** + * orientation info + * # For internal use only + */ + private int orientation = -1; + + /** + * loadLongImageStatus + * # For internal use only + */ + public int loadLongImageStatus = PictureConfig.NORMAL; + + /** + * isLongImage + * # For internal use only + */ + public boolean isLongImage; + + /** + * bucketId + */ + private long bucketId = -1; + + /** + * isMaxSelectEnabledMask + * # For internal use only + */ + private boolean isMaxSelectEnabledMask; + + public LocalMedia() { + + } + + public LocalMedia(String path, long duration, int chooseModel, String mimeType) { + this.path = path; + this.duration = duration; + this.chooseModel = chooseModel; + this.mimeType = mimeType; + } + + public LocalMedia(long id, String path, String fileName, String parentFolderName, long duration, int chooseModel, + String mimeType, int width, int height, long size) { + this.id = id; + this.path = path; + this.fileName = fileName; + this.parentFolderName = parentFolderName; + this.duration = duration; + this.chooseModel = chooseModel; + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.size = size; + } + + public LocalMedia(long id, String path, String absolutePath, String fileName, String parentFolderName, long duration, int chooseModel, + String mimeType, int width, int height, long size, long bucketId) { + this.id = id; + this.path = path; + this.realPath = absolutePath; + this.fileName = fileName; + this.parentFolderName = parentFolderName; + this.duration = duration; + this.chooseModel = chooseModel; + this.mimeType = mimeType; + this.width = width; + this.height = height; + this.size = size; + this.bucketId = bucketId; + } + + public LocalMedia(String path, long duration, + boolean isChecked, int position, int num, int chooseModel) { + this.path = path; + this.duration = duration; + this.isChecked = isChecked; + this.position = position; + this.num = num; + this.chooseModel = chooseModel; + } + + public String getPath() { + return path; + } + + public void setPath(String path) { + this.path = path; + } + + public String getCompressPath() { + return compressPath; + } + + public void setCompressPath(String compressPath) { + this.compressPath = compressPath; + } + + public String getCutPath() { + return cutPath; + } + + public void setCutPath(String cutPath) { + this.cutPath = cutPath; + } + + public String getAndroidQToPath() { + return androidQToPath; + } + + public void setAndroidQToPath(String androidQToPath) { + this.androidQToPath = androidQToPath; + } + + public long getDuration() { + return duration; + } + + public void setDuration(long duration) { + this.duration = duration; + } + + public String getRealPath() { + return realPath; + } + + public void setRealPath(String realPath) { + this.realPath = realPath; + } + + public boolean isChecked() { + return isChecked; + } + + public void setChecked(boolean checked) { + isChecked = checked; + } + + public boolean isCut() { + return isCut; + } + + public void setCut(boolean cut) { + isCut = cut; + } + + public int getPosition() { + return position; + } + + public void setPosition(int position) { + this.position = position; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + + public String getMimeType() { + return TextUtils.isEmpty(mimeType) ? "image/jpeg" : mimeType; + } + + public void setMimeType(String mimeType) { + this.mimeType = mimeType; + } + + public boolean isCompressed() { + return compressed; + } + + public void setCompressed(boolean compressed) { + this.compressed = compressed; + } + + public int getWidth() { + return width; + } + + public void setWidth(int width) { + this.width = width; + } + + public int getHeight() { + return height; + } + + public void setHeight(int height) { + this.height = height; + } + + public int getChooseModel() { + return chooseModel; + } + + public void setChooseModel(int chooseModel) { + this.chooseModel = chooseModel; + } + + public long getSize() { + return size; + } + + public void setSize(long size) { + this.size = size; + } + + public boolean isOriginal() { + return isOriginal; + } + + public void setOriginal(boolean original) { + isOriginal = original; + } + + public String getOriginalPath() { + return originalPath; + } + + public void setOriginalPath(String originalPath) { + this.originalPath = originalPath; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getParentFolderName() { + return parentFolderName; + } + + public void setParentFolderName(String parentFolderName) { + this.parentFolderName = parentFolderName; + } + + public int getOrientation() { + return orientation; + } + + public void setOrientation(int orientation) { + this.orientation = orientation; + } + + public long getBucketId() { + return bucketId; + } + + public void setBucketId(long bucketId) { + this.bucketId = bucketId; + } + + public boolean isMaxSelectEnabledMask() { + return isMaxSelectEnabledMask; + } + + public void setMaxSelectEnabledMask(boolean maxSelectEnabledMask) { + isMaxSelectEnabledMask = maxSelectEnabledMask; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(this.id); + dest.writeString(this.path); + dest.writeString(this.realPath); + dest.writeString(this.originalPath); + dest.writeString(this.compressPath); + dest.writeString(this.cutPath); + dest.writeString(this.androidQToPath); + dest.writeLong(this.duration); + dest.writeByte(this.isChecked ? (byte) 1 : (byte) 0); + dest.writeByte(this.isCut ? (byte) 1 : (byte) 0); + dest.writeInt(this.position); + dest.writeInt(this.num); + dest.writeString(this.mimeType); + dest.writeInt(this.chooseModel); + dest.writeByte(this.compressed ? (byte) 1 : (byte) 0); + dest.writeInt(this.width); + dest.writeInt(this.height); + dest.writeLong(this.size); + dest.writeByte(this.isOriginal ? (byte) 1 : (byte) 0); + dest.writeString(this.fileName); + dest.writeString(this.parentFolderName); + dest.writeInt(this.orientation); + dest.writeInt(this.loadLongImageStatus); + dest.writeByte(this.isLongImage ? (byte) 1 : (byte) 0); + dest.writeLong(this.bucketId); + dest.writeByte(this.isMaxSelectEnabledMask ? (byte) 1 : (byte) 0); + } + + protected LocalMedia(Parcel in) { + this.id = in.readLong(); + this.path = in.readString(); + this.realPath = in.readString(); + this.originalPath = in.readString(); + this.compressPath = in.readString(); + this.cutPath = in.readString(); + this.androidQToPath = in.readString(); + this.duration = in.readLong(); + this.isChecked = in.readByte() != 0; + this.isCut = in.readByte() != 0; + this.position = in.readInt(); + this.num = in.readInt(); + this.mimeType = in.readString(); + this.chooseModel = in.readInt(); + this.compressed = in.readByte() != 0; + this.width = in.readInt(); + this.height = in.readInt(); + this.size = in.readLong(); + this.isOriginal = in.readByte() != 0; + this.fileName = in.readString(); + this.parentFolderName = in.readString(); + this.orientation = in.readInt(); + this.loadLongImageStatus = in.readInt(); + this.isLongImage = in.readByte() != 0; + this.bucketId = in.readLong(); + this.isMaxSelectEnabledMask = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public LocalMedia createFromParcel(Parcel source) { + return new LocalMedia(source); + } + + @Override + public LocalMedia[] newArray(int size) { + return new LocalMedia[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMediaFolder.java b/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMediaFolder.java new file mode 100644 index 0000000..7daa09b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/entity/LocalMediaFolder.java @@ -0,0 +1,204 @@ +package com.luck.picture.lib.entity; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2016-12-31 15:21 + * @describe:MediaFolder Entity + */ + +public class LocalMediaFolder implements Parcelable { + /** + * bucketId + */ + private long bucketId = -1; + /** + * Folder name + */ + private String name; + /** + * Folder first path + */ + private String firstImagePath; + /** + * Folder media num + */ + private int imageNum; + /** + * If the selected num + */ + private int checkedNum; + /** + * If the selected + */ + private boolean isChecked; + + /** + * type + */ + private int ofAllType = -1; + /** + * Whether or not the camera + */ + private boolean isCameraFolder; + + /** + * data + */ + private List data = new ArrayList<>(); + + /** + * # Internal use + * setCurrentDataPage + */ + private int currentDataPage; + + /** + * # Internal use + * is load more + */ + private boolean isHasMore; + + public long getBucketId() { + return bucketId; + } + + public void setBucketId(long bucketId) { + this.bucketId = bucketId; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getFirstImagePath() { + return firstImagePath; + } + + public void setFirstImagePath(String firstImagePath) { + this.firstImagePath = firstImagePath; + } + + public int getImageNum() { + return imageNum; + } + + public void setImageNum(int imageNum) { + this.imageNum = imageNum; + } + + public int getCheckedNum() { + return checkedNum; + } + + public void setCheckedNum(int checkedNum) { + this.checkedNum = checkedNum; + } + + public boolean isChecked() { + return isChecked; + } + + public void setChecked(boolean checked) { + isChecked = checked; + } + + public int getOfAllType() { + return ofAllType; + } + + public void setOfAllType(int ofAllType) { + this.ofAllType = ofAllType; + } + + public boolean isCameraFolder() { + return isCameraFolder; + } + + public void setCameraFolder(boolean cameraFolder) { + isCameraFolder = cameraFolder; + } + + public List getData() { + return data; + } + + public void setData(List data) { + this.data = data; + } + + public int getCurrentDataPage() { + return currentDataPage; + } + + public void setCurrentDataPage(int currentDataPage) { + this.currentDataPage = currentDataPage; + } + + public boolean isHasMore() { + return isHasMore; + } + + public void setHasMore(boolean hasMore) { + isHasMore = hasMore; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeLong(this.bucketId); + dest.writeString(this.name); + dest.writeString(this.firstImagePath); + dest.writeInt(this.imageNum); + dest.writeInt(this.checkedNum); + dest.writeByte(this.isChecked ? (byte) 1 : (byte) 0); + dest.writeInt(this.ofAllType); + dest.writeByte(this.isCameraFolder ? (byte) 1 : (byte) 0); + dest.writeTypedList(this.data); + dest.writeInt(this.currentDataPage); + dest.writeByte(this.isHasMore ? (byte) 1 : (byte) 0); + } + + public LocalMediaFolder() { + } + + protected LocalMediaFolder(Parcel in) { + this.bucketId = in.readLong(); + this.name = in.readString(); + this.firstImagePath = in.readString(); + this.imageNum = in.readInt(); + this.checkedNum = in.readInt(); + this.isChecked = in.readByte() != 0; + this.ofAllType = in.readInt(); + this.isCameraFolder = in.readByte() != 0; + this.data = in.createTypedArrayList(LocalMedia.CREATOR); + this.currentDataPage = in.readInt(); + this.isHasMore = in.readByte() != 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public LocalMediaFolder createFromParcel(Parcel source) { + return new LocalMediaFolder(source); + } + + @Override + public LocalMediaFolder[] newArray(int size) { + return new LocalMediaFolder[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/entity/MediaData.java b/picture_library/src/main/java/com/luck/picture/lib/entity/MediaData.java new file mode 100644 index 0000000..2b7980d --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/entity/MediaData.java @@ -0,0 +1,32 @@ +package com.luck.picture.lib.entity; + +import java.util.List; + +/** + * @author:luck + * @date:2020-04-17 13:52 + * @describe:MediaData + */ +public class MediaData { + + /** + * Is there more + */ + public boolean isHasNextMore; + + /** + * data + */ + public List data; + + + public MediaData() { + super(); + } + + public MediaData(boolean isHasNextMore, List data) { + super(); + this.isHasNextMore = isHasNextMore; + this.data = data; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/immersive/ImmersiveManage.java b/picture_library/src/main/java/com/luck/picture/lib/immersive/ImmersiveManage.java new file mode 100644 index 0000000..e532b5e --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/immersive/ImmersiveManage.java @@ -0,0 +1,95 @@ +package com.luck.picture.lib.immersive; + +import android.graphics.Color; +import android.os.Build; +import android.view.Window; +import android.view.WindowManager; + +import androidx.appcompat.app.AppCompatActivity; + +/** + * @author:luck + * @data:2018/3/28 下午1:00 + * @描述: 沉浸式相关 + */ + +public class ImmersiveManage { + + /** + * 注意:使用最好将布局xml 跟布局加入 android:fitsSystemWindows="true" ,这样可以避免有些手机上布局顶边的问题 + * + * @param baseActivity 这个会留出来状态栏和底栏的空白 + * @param statusBarColor 状态栏的颜色 + * @param navigationBarColor 导航栏的颜色 + * @param isDarkStatusBarIcon 状态栏图标颜色是否是深(黑)色 false状态栏图标颜色为白色 + */ + public static void immersiveAboveAPI23(AppCompatActivity baseActivity, int statusBarColor, int navigationBarColor, boolean isDarkStatusBarIcon) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + immersiveAboveAPI23(baseActivity, false, false, statusBarColor, navigationBarColor, isDarkStatusBarIcon); + } + } + + + /** + * @param baseActivity + * @param statusBarColor 状态栏的颜色 + * @param navigationBarColor 导航栏的颜色 + */ + public static void immersiveAboveAPI23(AppCompatActivity baseActivity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, int statusBarColor, int navigationBarColor, boolean isDarkStatusBarIcon) { + try { + Window window = baseActivity.getWindow(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + //4.4版本及以上 5.0版本及以下 + window.setFlags( + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS, + WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS); + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (isMarginStatusBar && isMarginNavigationBar) { + //5.0版本及以上 + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + LightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar + , isMarginNavigationBar + , statusBarColor == Color.TRANSPARENT + , isDarkStatusBarIcon); + + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + } else if (!isMarginStatusBar && !isMarginNavigationBar) { + window.requestFeature(Window.FEATURE_NO_TITLE); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + + LightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar + , isMarginNavigationBar + , statusBarColor == Color.TRANSPARENT + , isDarkStatusBarIcon); + + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + + + } else if (!isMarginStatusBar && isMarginNavigationBar) { + window.requestFeature(Window.FEATURE_NO_TITLE); + window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS + | WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION); + LightStatusBarUtils.setLightStatusBar(baseActivity, isMarginStatusBar + , isMarginNavigationBar + , statusBarColor == Color.TRANSPARENT + , isDarkStatusBarIcon); + + window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); + + + } else { + //留出来状态栏 不留出来导航栏 没找到办法。。 + return; + } + + window.setStatusBarColor(statusBarColor); + window.setNavigationBarColor(navigationBarColor); + + } + } catch (Exception e) { + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/immersive/LightStatusBarUtils.java b/picture_library/src/main/java/com/luck/picture/lib/immersive/LightStatusBarUtils.java new file mode 100644 index 0000000..3f2b408 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/immersive/LightStatusBarUtils.java @@ -0,0 +1,189 @@ +package com.luck.picture.lib.immersive; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.os.Build; +import android.view.View; +import android.view.Window; +import android.view.WindowManager; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +/** + * @author:luck + * @data:2018/3/28 下午1:01 + * @描述: 沉浸式 + */ + +public class LightStatusBarUtils { + public static void setLightStatusBarAboveAPI23(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + setLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + } + } + + public static void setLightStatusBar(Activity activity, boolean dark) { + setLightStatusBar(activity, false, false, false, dark); + } + + public static void setLightStatusBar(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) { + switch (RomUtils.getLightStatausBarAvailableRomType()) { + case RomUtils.AvailableRomType.MIUI: + if (RomUtils.getMIUIVersionCode() >= 7) { + setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + } else { + setMIUILightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + } + break; + + case RomUtils.AvailableRomType.FLYME: + setFlymeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + break; + + case RomUtils.AvailableRomType.ANDROID_NATIVE: + setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + break; + + case RomUtils.AvailableRomType.NA: + // N/A do nothing + break; + } + } + + + private static boolean setMIUILightStatusBar(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean darkmode) { + initStatusBarStyle(activity, isMarginStatusBar, isMarginNavigationBar); + + Class clazz = activity.getWindow().getClass(); + try { + int darkModeFlag = 0; + Class layoutParams = Class.forName("android.view.MiuiWindowManager$LayoutParams"); + Field field = layoutParams.getField("EXTRA_FLAG_STATUS_BAR_DARK_MODE"); + darkModeFlag = field.getInt(layoutParams); + Method extraFlagField = clazz.getMethod("setExtraFlags", int.class, int.class); + extraFlagField.invoke(activity.getWindow(), darkmode ? darkModeFlag : 0, darkModeFlag); + return true; + } catch (Exception e) { + setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, darkmode); + } + return false; + } + + private static boolean setFlymeLightStatusBar(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean dark) { + boolean result = false; + if (activity != null) { + initStatusBarStyle(activity, isMarginStatusBar, isMarginNavigationBar); + try { + WindowManager.LayoutParams lp = activity.getWindow().getAttributes(); + Field darkFlag = WindowManager.LayoutParams.class + .getDeclaredField("MEIZU_FLAG_DARK_STATUS_BAR_ICON"); + Field meizuFlags = WindowManager.LayoutParams.class + .getDeclaredField("meizuFlags"); + darkFlag.setAccessible(true); + meizuFlags.setAccessible(true); + int bit = darkFlag.getInt(null); + int value = meizuFlags.getInt(lp); + if (dark) { + value |= bit; + } else { + value &= ~bit; + } + meizuFlags.setInt(lp, value); + activity.getWindow().setAttributes(lp); + result = true; + + if (RomUtils.getFlymeVersion() >= 7) { + setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + } + } catch (Exception e) { + setAndroidNativeLightStatusBar(activity, isMarginStatusBar, isMarginNavigationBar, isTransStatusBar, dark); + } + } + return result; + } + + @TargetApi(11) + private static void setAndroidNativeLightStatusBar(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar, boolean isTransStatusBar, boolean isDarkStatusBarIcon) { + + try { + if (isTransStatusBar) { + Window window = activity.getWindow(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + if (isMarginStatusBar && isMarginNavigationBar) { + //5.0版本及以上 + if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + } else if (!isMarginStatusBar && !isMarginNavigationBar) { + + if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN +// | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + + } else if (!isMarginStatusBar && isMarginNavigationBar) { + if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE + | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else { + window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } + + + } else { + //留出来状态栏 不留出来导航栏 没找到办法。。 + return; + } + } + } else { + View decor = activity.getWindow().getDecorView(); + if (isDarkStatusBarIcon && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } else { + // We want to change tint color to white again. + // You can also record the flags in advance so that you can turn UI back completely if + // you have set other flags before, such as translucent or full screen. + decor.setSystemUiVisibility(0); + } + } + } catch (Exception e) { + } + } + + private static void initStatusBarStyle(Activity activity, boolean isMarginStatusBar + , boolean isMarginNavigationBar) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (isMarginStatusBar && isMarginNavigationBar) { + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } else if (!isMarginStatusBar && !isMarginNavigationBar) { + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } else if (!isMarginStatusBar && isMarginNavigationBar) { + + activity.getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN + | View.SYSTEM_UI_FLAG_LAYOUT_STABLE); + } else { + //留出来状态栏 不留出来导航栏 没找到办法。。 + } + } + + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/immersive/NavBarUtils.java b/picture_library/src/main/java/com/luck/picture/lib/immersive/NavBarUtils.java new file mode 100644 index 0000000..6e6c929 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/immersive/NavBarUtils.java @@ -0,0 +1,31 @@ +package com.luck.picture.lib.immersive; + +import android.app.Activity; +import android.os.Build; +import android.view.Window; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +/** + * @author:luck + * @date:2019-11-25 20:58 + * @describe:NavBar工具类 + */ +public class NavBarUtils { + /** + * 动态设置 NavBar 色值 + * + * @param activity + * @param color + */ + public static void setNavBarColor(@NonNull final Activity activity, @ColorInt final int color) { + setNavBarColor(activity.getWindow(), color); + } + + public static void setNavBarColor(@NonNull final Window window, @ColorInt final int color) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + window.setNavigationBarColor(color); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/immersive/RomUtils.java b/picture_library/src/main/java/com/luck/picture/lib/immersive/RomUtils.java new file mode 100644 index 0000000..373533d --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/immersive/RomUtils.java @@ -0,0 +1,139 @@ +package com.luck.picture.lib.immersive; + +import android.os.Build; +import android.text.TextUtils; +import com.luck.picture.lib.tools.StringUtils; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; + +/** + * @author:luck + * @data:2018/3/28 下午1:02 + * @描述: Rom版本管理 + */ + +public class RomUtils { + public class AvailableRomType { + public static final int MIUI = 1; + public static final int FLYME = 2; + public static final int ANDROID_NATIVE = 3; + public static final int NA = 4; + } + + + private static Integer romType; + + public static int getLightStatausBarAvailableRomType() { + if (romType != null) { + return romType; + } + + if (isMIUIV6OrAbove()) { + romType = AvailableRomType.MIUI; + return romType; + } + + if (isFlymeV4OrAbove()) { + romType = AvailableRomType.FLYME; + return romType; + } + + if (isAndroid5OrAbove()) { + romType = AvailableRomType.ANDROID_NATIVE; + return romType; + } + + romType = AvailableRomType.NA; + return romType; + } + + //Flyme V4的displayId格式为 [Flyme OS 4.x.x.xA] + //Flyme V5的displayId格式为 [Flyme 5.x.x.x beta] + private static boolean isFlymeV4OrAbove() { + return (getFlymeVersion() >= 4); + } + + + //Flyme V4的displayId格式为 [Flyme OS 4.x.x.xA] + //Flyme V5的displayId格式为 [Flyme 5.x.x.x beta] + public static int getFlymeVersion() { + String displayId = Build.DISPLAY; + if (!TextUtils.isEmpty(displayId) && displayId.contains("Flyme")) { + displayId = displayId.replaceAll("Flyme", ""); + displayId = displayId.replaceAll("OS", ""); + displayId = displayId.replaceAll(" ", ""); + + + String version = displayId.substring(0, 1); + + if (version != null) { + return StringUtils.stringToInt(version); + } + } + return 0; + } + + //MIUI V6对应的versionCode是4 + //MIUI V7对应的versionCode是5 + private static boolean isMIUIV6OrAbove() { + String miuiVersionCodeStr = getSystemProperty("ro.miui.ui.version.code"); + if (!TextUtils.isEmpty(miuiVersionCodeStr)) { + try { + int miuiVersionCode = Integer.parseInt(miuiVersionCodeStr); + if (miuiVersionCode >= 4) { + return true; + } + } catch (Exception e) { + } + } + return false; + } + + + public static int getMIUIVersionCode() { + String miuiVersionCodeStr = getSystemProperty("ro.miui.ui.version.code"); + int miuiVersionCode = 0; + if (!TextUtils.isEmpty(miuiVersionCodeStr)) { + try { + miuiVersionCode = Integer.parseInt(miuiVersionCodeStr); + return miuiVersionCode; + } catch (Exception e) { + } + } + return miuiVersionCode; + } + + + //Android Api 23以上 + private static boolean isAndroid5OrAbove() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + return true; + } + return false; + } + + + public static String getSystemProperty(String propName) { + String line; + BufferedReader input = null; + try { + Process p = Runtime.getRuntime().exec("getprop " + propName); + input = new BufferedReader(new InputStreamReader(p.getInputStream()), 1024); + line = input.readLine(); + input.close(); + } catch (IOException ex) { + return null; + } finally { + if (input != null) { + try { + input.close(); + } catch (IOException e) { + } + } + } + return line; + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/language/LanguageConfig.java b/picture_library/src/main/java/com/luck/picture/lib/language/LanguageConfig.java new file mode 100644 index 0000000..17f0e99 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/language/LanguageConfig.java @@ -0,0 +1,46 @@ +package com.luck.picture.lib.language; + +/** + * @author:luck + * @date:2019-11-25 21:50 + * @describe:语言配制 + */ +public class LanguageConfig { + /** + * 简体中文 + */ + public static final int CHINESE = 0; + /** + * 繁体 + */ + public static final int TRADITIONAL_CHINESE = 1; + + // 英语 + public static final int ENGLISH = 2; + + /** + * 韩语 + */ + public static final int KOREA = 3; + + /** + * 德语 + */ + public static final int GERMANY = 4; + + /** + * 法语 + */ + public static final int FRANCE = 5; + + /** + * 日语 + */ + public static final int JAPAN = 6; + + /** + * 越语 + */ + public static final int VIETNAM = 7; + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/language/LocaleTransform.java b/picture_library/src/main/java/com/luck/picture/lib/language/LocaleTransform.java new file mode 100644 index 0000000..684eeac --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/language/LocaleTransform.java @@ -0,0 +1,39 @@ +package com.luck.picture.lib.language; + +import java.util.Locale; + +/** + * @author:luck + * @date:2019-11-25 21:58 + * @describe:语言转换 + */ +public class LocaleTransform { + public static Locale getLanguage(int language) { + switch (language) { + case LanguageConfig.ENGLISH: + // 英语-美国 + return Locale.ENGLISH; + case LanguageConfig.TRADITIONAL_CHINESE: + // 繁体中文 + return Locale.TRADITIONAL_CHINESE; + case LanguageConfig.KOREA: + // 韩语 + return Locale.KOREA; + case LanguageConfig.GERMANY: + // 德语 + return Locale.GERMANY; + case LanguageConfig.FRANCE: + // 法语 + return Locale.FRANCE; + case LanguageConfig.JAPAN: + // 日语 + return Locale.JAPAN; + case LanguageConfig.VIETNAM: + // 越南语 + return new Locale("vi"); + default: + // 简体中文 + return Locale.CHINESE; + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/language/PictureLanguageUtils.java b/picture_library/src/main/java/com/luck/picture/lib/language/PictureLanguageUtils.java new file mode 100644 index 0000000..cf5aaae --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/language/PictureLanguageUtils.java @@ -0,0 +1,118 @@ +package com.luck.picture.lib.language; + +import android.content.Context; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.os.Build; +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; + +import com.luck.picture.lib.tools.SPUtils; + +import java.lang.ref.WeakReference; +import java.util.Locale; + +/** + * @author:luck + * @data:2018/3/28 下午1:00 + * @描述: PictureLanguageUtils + */ +public class PictureLanguageUtils { + + private static final String KEY_LOCALE = "KEY_LOCALE"; + private static final String VALUE_FOLLOW_SYSTEM = "VALUE_FOLLOW_SYSTEM"; + + private PictureLanguageUtils() { + throw new UnsupportedOperationException("u can't instantiate me..."); + } + + /** + * init app the language + * + * @param context + * @param languageId + */ + public static void setAppLanguage(Context context, int languageId) { + WeakReference contextWeakReference = new WeakReference<>(context); + if (languageId >= 0) { + applyLanguage(contextWeakReference.get(), LocaleTransform.getLanguage(languageId)); + } else { + setDefaultLanguage(contextWeakReference.get()); + } + } + + /** + * Apply the language. + * + * @param locale The language of locale. + */ + private static void applyLanguage(@NonNull Context context, @NonNull final Locale locale) { + applyLanguage(context, locale, false); + } + + + private static void applyLanguage(@NonNull Context context, @NonNull final Locale locale, + final boolean isFollowSystem) { + if (isFollowSystem) { + SPUtils.getPictureSpUtils().put(KEY_LOCALE, VALUE_FOLLOW_SYSTEM); + } else { + String localLanguage = locale.getLanguage(); + String localCountry = locale.getCountry(); + SPUtils.getPictureSpUtils().put(KEY_LOCALE, localLanguage + "$" + localCountry); + } + + updateLanguage(context, locale); + } + + + private static void updateLanguage(Context context, Locale locale) { + Resources resources = context.getResources(); + Configuration config = resources.getConfiguration(); + Locale contextLocale = config.locale; + if (equals(contextLocale.getLanguage(), locale.getLanguage()) + && equals(contextLocale.getCountry(), locale.getCountry())) { + return; + } + DisplayMetrics dm = resources.getDisplayMetrics(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + config.setLocale(locale); + context.createConfigurationContext(config); + } else { + config.locale = locale; + } + resources.updateConfiguration(config, dm); + } + + /** + * set default language + * + * @param context + */ + private static void setDefaultLanguage(Context context) { + Resources resources = context.getResources(); + Configuration config = resources.getConfiguration(); + DisplayMetrics dm = resources.getDisplayMetrics(); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + config.setLocale(config.locale); + context.createConfigurationContext(config); + } + resources.updateConfiguration(config, dm); + } + + private static boolean equals(final CharSequence s1, final CharSequence s2) { + if (s1 == s2) return true; + int length; + if (s1 != null && s2 != null && (length = s1.length()) == s2.length()) { + if (s1 instanceof String && s2 instanceof String) { + return s1.equals(s2); + } else { + for (int i = 0; i < length; i++) { + if (s1.charAt(i) != s2.charAt(i)) return false; + } + return true; + } + } + return false; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnAlbumItemClickListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnAlbumItemClickListener.java new file mode 100644 index 0000000..bd79ceb --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnAlbumItemClickListener.java @@ -0,0 +1,24 @@ +package com.luck.picture.lib.listener; + +import com.luck.picture.lib.entity.LocalMedia; + +import java.util.List; + +/** + * @author:luck + * @date:2020-03-26 10:57 + * @describe:OnAlbumItemClickListener + */ +public interface OnAlbumItemClickListener { + /** + * Album catalog item click event + * + * @param position + * @param isCameraFolder + * @param bucketId + * @param folderName + * @param data + */ + void onItemClick(int position, boolean isCameraFolder, + long bucketId, String folderName, List data); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnCallbackListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnCallbackListener.java new file mode 100644 index 0000000..b9093f3 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnCallbackListener.java @@ -0,0 +1,13 @@ +package com.luck.picture.lib.listener; + +/** + * @author:luck + * @date:2020/4/24 11:48 AM + * @describe:OnCallbackListener + */ +public interface OnCallbackListener { + /** + * @param data + */ + void onCall(T data); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnCustomCameraInterfaceListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnCustomCameraInterfaceListener.java new file mode 100644 index 0000000..ce9604a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnCustomCameraInterfaceListener.java @@ -0,0 +1,21 @@ +package com.luck.picture.lib.listener; + +import android.content.Context; + +import com.luck.picture.lib.config.PictureSelectionConfig; + +/** + * @author:luck + * @date:2020/4/27 3:24 PM + * @describe:OnCustomCameraInterfaceListener + */ +public interface OnCustomCameraInterfaceListener { + /** + * Camera Menu + * + * @param context + * @param config + * @param type + */ + void onCameraClick(Context context, PictureSelectionConfig config, int type); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnImageCompleteCallback.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnImageCompleteCallback.java new file mode 100644 index 0000000..cc4b18f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnImageCompleteCallback.java @@ -0,0 +1,18 @@ +package com.luck.picture.lib.listener; + +/** + * @author:luck + * @date:2020-01-03 16:43 + * @describe:Image load complete callback + */ +public interface OnImageCompleteCallback { + /** + * Start loading + */ + void onShowLoading(); + + /** + * Stop loading + */ + void onHideLoading(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnItemClickListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnItemClickListener.java new file mode 100644 index 0000000..a0b4a8b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnItemClickListener.java @@ -0,0 +1,18 @@ +package com.luck.picture.lib.listener; + +import android.view.View; + +/** + * @author:luck + * @date:2020-03-26 10:50 + * @describe:OnItemClickListener + */ +public interface OnItemClickListener { + /** + * Item click event + * + * @param v + * @param position + */ + void onItemClick(View v, int position); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnPhotoSelectChangedListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnPhotoSelectChangedListener.java new file mode 100644 index 0000000..3279127 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnPhotoSelectChangedListener.java @@ -0,0 +1,30 @@ +package com.luck.picture.lib.listener; + +import java.util.List; + +/** + * @author:luck + * @date:2020-03-26 10:34 + * @describe:OnPhotoSelectChangedListener + */ +public interface OnPhotoSelectChangedListener { + /** + * Photo callback + */ + void onTakePhoto(); + + /** + * Selected LocalMedia callback + * + * @param data + */ + void onChange(List data); + + /** + * Image preview callback + * + * @param data + * @param position + */ + void onPictureClick(T data, int position); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnQueryDataResultListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnQueryDataResultListener.java new file mode 100644 index 0000000..146f1cb --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnQueryDataResultListener.java @@ -0,0 +1,19 @@ +package com.luck.picture.lib.listener; + +import java.util.List; + +/** + * @author:luck + * @date:2020-04-16 12:42 + * @describe:OnQueryMediaResultListener + */ +public interface OnQueryDataResultListener { + /** + * Query to complete The callback listener + * + * @param data The data source + * @param currentPage The page number + * @param isHasMore Is there more + */ + void onComplete(List data, int currentPage, boolean isHasMore); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnRecyclerViewPreloadMoreListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnRecyclerViewPreloadMoreListener.java new file mode 100644 index 0000000..d863610 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnRecyclerViewPreloadMoreListener.java @@ -0,0 +1,13 @@ +package com.luck.picture.lib.listener; + +/** + * @author:luck + * @date:2020-04-14 18:44 + * @describe:OnRecyclerViewPreloadMoreListener + */ +public interface OnRecyclerViewPreloadMoreListener { + /** + * load more + */ + void onRecyclerViewPreloadMore(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnResultCallbackListener.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnResultCallbackListener.java new file mode 100644 index 0000000..7f24fee --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnResultCallbackListener.java @@ -0,0 +1,22 @@ +package com.luck.picture.lib.listener; + +import java.util.List; + +/** + * @author:luck + * @date:2020-01-14 17:08 + * @describe:onResult Callback Listener + */ +public interface OnResultCallbackListener { + /** + * return LocalMedia result + * + * @param result + */ + void onResult(List result); + + /** + * Cancel + */ + void onCancel(); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/listener/OnVideoSelectedPlayCallback.java b/picture_library/src/main/java/com/luck/picture/lib/listener/OnVideoSelectedPlayCallback.java new file mode 100644 index 0000000..6032ee0 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/listener/OnVideoSelectedPlayCallback.java @@ -0,0 +1,16 @@ +package com.luck.picture.lib.listener; + + +/** + * @author:luck + * @date:2020-01-15 14:38 + * @describe:Custom video playback callback + */ +public interface OnVideoSelectedPlayCallback { + /** + * Play the video + * + * @param data + */ + void startPlayVideo(T data); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaLoader.java b/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaLoader.java new file mode 100644 index 0000000..6139d1e --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaLoader.java @@ -0,0 +1,407 @@ +package com.luck.picture.lib.model; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.ValueOf; + +import java.io.File; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * @author:luck + * @data:2016/12/31 19:12 + * @describe: Local media database query class + */ +@Deprecated +public final class LocalMediaLoader { + private static final String TAG = LocalMediaLoader.class.getSimpleName(); + private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); + private static final String ORDER_BY = MediaStore.Files.FileColumns._ID + " DESC"; + private static final String NOT_GIF = "!='image/gif'"; + /** + * Filter out recordings that are less than 500 milliseconds long + */ + private static final int AUDIO_DURATION = 500; + private Context mContext; + private boolean isAndroidQ; + private PictureSelectionConfig config; + /** + * unit + */ + private static final long FILE_SIZE_UNIT = 1024 * 1024L; + /** + * Media file database field + */ + private static final String[] PROJECTION = { + MediaStore.Files.FileColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.MIME_TYPE, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, + MediaStore.MediaColumns.DURATION, + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.BUCKET_DISPLAY_NAME, + MediaStore.MediaColumns.DISPLAY_NAME, + MediaStore.MediaColumns.BUCKET_ID}; + + /** + * Image + */ + private static final String SELECTION = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + + private static final String SELECTION_NOT_GIF = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0" + + " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF; + /** + * Queries for images with the specified suffix + */ + private static final String SELECTION_SPECIFIED_FORMAT = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0" + + " AND " + MediaStore.MediaColumns.MIME_TYPE; + + /** + * Query criteria (audio and video) + * + * @param time_condition + * @return + */ + private static String getSelectionArgsForSingleMediaCondition(String time_condition) { + return MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0" + + " AND " + time_condition; + } + + /** + * Query (video) + * + * @return + */ + private static String getSelectionArgsForSingleMediaCondition() { + return MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + + /** + * Query conditions in all modes + * + * @param time_condition + * @param isGif + * @return + */ + private static String getSelectionArgsForAllMediaCondition(String time_condition, boolean isGif) { + String condition = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + " OR " + + (MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + time_condition) + ")" + + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + return condition; + } + + /** + * Get pictures or videos + */ + private static final String[] SELECTION_ALL_ARGS = { + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), + }; + + /** + * Gets a file of the specified type + * + * @param mediaType + * @return + */ + private static String[] getSelectionArgsForSingleMediaType(int mediaType) { + return new String[]{String.valueOf(mediaType)}; + } + + + public LocalMediaLoader(Context context, PictureSelectionConfig config) { + this.mContext = context.getApplicationContext(); + this.isAndroidQ = SdkVersionUtils.checkedAndroid_Q(); + this.config = config; + } + + /** + * Query the local gallery data + * + * @return + */ + public List loadAllMedia() { + Cursor data = mContext.getContentResolver().query(QUERY_URI, PROJECTION, getSelection(), getSelectionArgs(), ORDER_BY); + try { + if (data != null) { + List imageFolders = new ArrayList<>(); + LocalMediaFolder allImageFolder = new LocalMediaFolder(); + List latelyImages = new ArrayList<>(); + int count = data.getCount(); + if (count > 0) { + data.moveToFirst(); + do { + long id = data.getLong + (data.getColumnIndexOrThrow(PROJECTION[0])); + + String absolutePath = data.getString + (data.getColumnIndexOrThrow(PROJECTION[1])); + + String url = isAndroidQ ? getRealPathAndroid_Q(id) : absolutePath; + + String mimeType = data.getString + (data.getColumnIndexOrThrow(PROJECTION[2])); + + mimeType = TextUtils.isEmpty(mimeType) ? PictureMimeType.ofJPEG() : mimeType; + // Here, it is solved that some models obtain mimeType and return the format of image / *, + // which makes it impossible to distinguish the specific type, such as mi 8,9,10 and other models + if (mimeType.endsWith("image/*")) { + if (PictureMimeType.isContent(url)) { + mimeType = PictureMimeType.getImageMimeType(absolutePath); + } else { + mimeType = PictureMimeType.getImageMimeType(url); + } + if (!config.isGif) { + boolean isGif = PictureMimeType.isGif(mimeType); + if (isGif) { + continue; + } + } + } + int width = data.getInt + (data.getColumnIndexOrThrow(PROJECTION[3])); + + int height = data.getInt + (data.getColumnIndexOrThrow(PROJECTION[4])); + + long duration = data.getLong + (data.getColumnIndexOrThrow(PROJECTION[5])); + + long size = data.getLong + (data.getColumnIndexOrThrow(PROJECTION[6])); + + String folderName = data.getString + (data.getColumnIndexOrThrow(PROJECTION[7])); + + String fileName = data.getString + (data.getColumnIndexOrThrow(PROJECTION[8])); + + long bucketId = data.getLong(data.getColumnIndexOrThrow(PROJECTION[9])); + + if (config.filterFileSize > 0) { + if (size > config.filterFileSize * FILE_SIZE_UNIT) { + continue; + } + } + if (PictureMimeType.isHasVideo(mimeType)) { + if (config.videoMinSecond > 0 && duration < config.videoMinSecond) { + // If you set the minimum number of seconds of video to display + continue; + } + if (config.videoMaxSecond > 0 && duration > config.videoMaxSecond) { + // If you set the maximum number of seconds of video to display + continue; + } + if (duration == 0) { + //If the length is 0, the corrupted video is processed and filtered out + continue; + } + if (size <= 0) { + // The video size is 0 to filter out + continue; + } + } + LocalMedia image = new LocalMedia + (id, url, absolutePath, fileName, folderName, duration, config.chooseMode, mimeType, width, height, size, bucketId); + LocalMediaFolder folder = getImageFolder(url, folderName, imageFolders); + folder.setBucketId(image.getBucketId()); + List images = folder.getData(); + images.add(image); + folder.setImageNum(folder.getImageNum() + 1); + folder.setBucketId(image.getBucketId()); + latelyImages.add(image); + int imageNum = allImageFolder.getImageNum(); + allImageFolder.setImageNum(imageNum + 1); + + } while (data.moveToNext()); + + if (latelyImages.size() > 0) { + sortFolder(imageFolders); + imageFolders.add(0, allImageFolder); + allImageFolder.setFirstImagePath + (latelyImages.get(0).getPath()); + String title = config.chooseMode == PictureMimeType.ofAudio() ? + mContext.getString(R.string.picture_all_audio) + : mContext.getString(R.string.picture_camera_roll); + allImageFolder.setName(title); + allImageFolder.setBucketId(-1); + allImageFolder.setOfAllType(config.chooseMode); + allImageFolder.setCameraFolder(true); + allImageFolder.setData(latelyImages); + } + } + return imageFolders; + } + } catch (Exception e) { + e.printStackTrace(); + Log.i(TAG, "loadAllMedia Data Error: " + e.getMessage()); + return null; + } finally { + if (data != null && !data.isClosed()) { + data.close(); + } + } + return null; + } + + private String getSelection() { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + // Get all, not including audio + return getSelectionArgsForAllMediaCondition(getDurationCondition(0, 0), config.isGif); + case PictureConfig.TYPE_IMAGE: + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // Gets the image of the specified type + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "'"; + } + return config.isGif ? SELECTION : SELECTION_NOT_GIF; + case PictureConfig.TYPE_VIDEO: + // Access to video + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // Gets the image of the specified type + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "'"; + } + return getSelectionArgsForSingleMediaCondition(); + case PictureConfig.TYPE_AUDIO: + // Access to the audio + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // Gets the image of the specified type + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "'"; + } + return getSelectionArgsForSingleMediaCondition(getDurationCondition(0, AUDIO_DURATION)); + } + return null; + } + + private String[] getSelectionArgs() { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + return SELECTION_ALL_ARGS; + case PictureConfig.TYPE_IMAGE: + // Get Image + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); + case PictureConfig.TYPE_VIDEO: + // Get Video + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO); + case PictureConfig.TYPE_AUDIO: + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO); + } + return null; + } + + /** + * Sort by the number of files + * + * @param imageFolders + */ + private void sortFolder(List imageFolders) { + Collections.sort(imageFolders, (lhs, rhs) -> { + if (lhs.getData() == null || rhs.getData() == null) { + return 0; + } + int lSize = lhs.getImageNum(); + int rSize = rhs.getImageNum(); + return Integer.compare(rSize, lSize); + }); + } + + /** + * Android Q + * + * @param id + * @return + */ + private String getRealPathAndroid_Q(long id) { + return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString(); + } + + /** + * Create folder + * + * @param path + * @param imageFolders + * @param folderName + * @return + */ + private LocalMediaFolder getImageFolder(String path, String folderName, List imageFolders) { + if (!config.isFallbackVersion) { + for (LocalMediaFolder folder : imageFolders) { + // Under the same folder, return yourself, otherwise create a new folder + String name = folder.getName(); + if (TextUtils.isEmpty(name)) { + continue; + } + if (name.equals(folderName)) { + return folder; + } + } + LocalMediaFolder newFolder = new LocalMediaFolder(); + newFolder.setName(folderName); + newFolder.setFirstImagePath(path); + imageFolders.add(newFolder); + return newFolder; + } else { + // Fault-tolerant processing + File imageFile = new File(path); + File folderFile = imageFile.getParentFile(); + for (LocalMediaFolder folder : imageFolders) { + // Under the same folder, return yourself, otherwise create a new folder + String name = folder.getName(); + if (TextUtils.isEmpty(name)) { + continue; + } + if (folderFile != null && name.equals(folderFile.getName())) { + return folder; + } + } + LocalMediaFolder newFolder = new LocalMediaFolder(); + newFolder.setName(folderFile != null ? folderFile.getName() : ""); + newFolder.setFirstImagePath(path); + imageFolders.add(newFolder); + return newFolder; + } + } + + /** + * Get video (maximum or minimum time) + * + * @param exMaxLimit + * @param exMinLimit + * @return + */ + private String getDurationCondition(long exMaxLimit, long exMinLimit) { + long maxS = config.videoMaxSecond == 0 ? Long.MAX_VALUE : config.videoMaxSecond; + if (exMaxLimit != 0) { + maxS = Math.min(maxS, exMaxLimit); + } + return String.format(Locale.CHINA, "%d <%s " + MediaStore.MediaColumns.DURATION + " and " + MediaStore.MediaColumns.DURATION + " <= %d", + Math.max(exMinLimit, config.videoMinSecond), + Math.max(exMinLimit, config.videoMinSecond) == 0 ? "" : "=", + maxS); + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaPageLoader.java b/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaPageLoader.java new file mode 100644 index 0000000..cd97ae3 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/model/LocalMediaPageLoader.java @@ -0,0 +1,713 @@ +package com.luck.picture.lib.model; + +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.entity.MediaData; +import com.luck.picture.lib.listener.OnQueryDataResultListener; +import com.luck.picture.lib.thread.PictureThreadUtils; +import com.luck.picture.lib.tools.PictureFileUtils; +import com.luck.picture.lib.tools.SdkVersionUtils; +import com.luck.picture.lib.tools.ValueOf; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Set; + +/** + * @author:luck + * @date:2020-04-13 15:06 + * @describe:Local media database query class,Support paging + */ +public final class LocalMediaPageLoader { + private static final String TAG = LocalMediaPageLoader.class.getSimpleName(); + + private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external"); + private static final String ORDER_BY = MediaStore.Files.FileColumns._ID + " DESC"; + private static final String NOT_GIF_UNKNOWN = "!='image/*'"; + private static final String NOT_GIF = "!='image/gif' AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF_UNKNOWN; + private static final String GROUP_BY_BUCKET_Id = " GROUP BY (bucket_id"; + private static final String COLUMN_COUNT = "count"; + private static final String COLUMN_BUCKET_ID = "bucket_id"; + private static final String COLUMN_BUCKET_DISPLAY_NAME = "bucket_display_name"; + + /** + * Filter out recordings that are less than 500 milliseconds long + */ + private static final int AUDIO_DURATION = 500; + private Context mContext; + private PictureSelectionConfig config; + /** + * unit + */ + private static final long FILE_SIZE_UNIT = 1024 * 1024L; + /** + * Image + */ + private static final String SELECTION = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? )" + + " AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + + private static final String SELECTION_29 = MediaStore.Files.FileColumns.MEDIA_TYPE + "=? " + + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + + private static final String SELECTION_NOT_GIF = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF + ") AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + + private static final String SELECTION_NOT_GIF_29 = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF + " AND " + MediaStore.MediaColumns.SIZE + ">0"; + /** + * Queries for images with the specified suffix + */ + private static final String SELECTION_SPECIFIED_FORMAT = "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.MIME_TYPE; + + /** + * Queries for images with the specified suffix targetSdk>=29 + */ + private static final String SELECTION_SPECIFIED_FORMAT_29 = MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.MIME_TYPE; + + /** + * Query criteria (audio and video) + * + * @param timeCondition + * @return + */ + private static String getSelectionArgsForSingleMediaCondition(String timeCondition) { + if (SdkVersionUtils.checkedAndroid_Q()) { + return MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + " AND " + MediaStore.MediaColumns.SIZE + ">0" + + " AND " + timeCondition; + } + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + ") AND " + MediaStore.MediaColumns.SIZE + ">0" + + " AND " + timeCondition + ")" + GROUP_BY_BUCKET_Id; + } + + /** + * All mode conditions + * + * @param timeCondition + * @param isGif + * @return + */ + private static String getSelectionArgsForAllMediaCondition(String timeCondition, boolean isGif) { + if (SdkVersionUtils.checkedAndroid_Q()) { + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + timeCondition + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + " OR " + (MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + timeCondition) + ")" + " AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + } + + /** + * Get pictures or videos + */ + private static final String[] SELECTION_ALL_ARGS = { + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), + }; + + /** + * Gets a file of the specified type + * + * @param mediaType + * @return + */ + private static String[] getSelectionArgsForSingleMediaType(int mediaType) { + return new String[]{String.valueOf(mediaType)}; + } + + /** + * Gets a file of the specified type + * + * @param mediaType + * @return + */ + private static String[] getSelectionArgsForPageSingleMediaType(int mediaType, long bucketId) { + return bucketId == -1 ? new String[]{String.valueOf(mediaType)} : new String[]{String.valueOf(mediaType), ValueOf.toString(bucketId)}; + } + + + public LocalMediaPageLoader(Context context, PictureSelectionConfig config) { + this.mContext = context; + this.config = config; + } + + private static final String[] PROJECTION_29 = { + MediaStore.Files.FileColumns._ID, + COLUMN_BUCKET_ID, + COLUMN_BUCKET_DISPLAY_NAME, + MediaStore.MediaColumns.MIME_TYPE}; + + private static final String[] PROJECTION = { + MediaStore.Files.FileColumns._ID, + MediaStore.MediaColumns.DATA, + COLUMN_BUCKET_ID, + COLUMN_BUCKET_DISPLAY_NAME, + MediaStore.MediaColumns.MIME_TYPE, + "COUNT(*) AS " + COLUMN_COUNT}; + + /** + * Media file database field + */ + private static final String[] PROJECTION_PAGE = { + MediaStore.Files.FileColumns._ID, + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.MIME_TYPE, + MediaStore.MediaColumns.WIDTH, + MediaStore.MediaColumns.HEIGHT, + MediaStore.MediaColumns.DURATION, + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.BUCKET_DISPLAY_NAME, + MediaStore.MediaColumns.DISPLAY_NAME, + COLUMN_BUCKET_ID}; + + /** + * Get the latest cover of an album catalog + * + * @param bucketId + * @return + */ + public String getFirstCover(long bucketId) { + Cursor data = null; + try { + String orderBy = MediaStore.Files.FileColumns._ID + " DESC limit 1 offset 0"; + data = mContext.getContentResolver().query(QUERY_URI, new String[]{ + MediaStore.Files.FileColumns._ID, + MediaStore.MediaColumns.DATA}, getPageSelection(bucketId), getPageSelectionArgs(bucketId), orderBy); + if (data != null && data.getCount() > 0) { + if (data.moveToFirst()) { + long id = data.getLong(data.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID)); + return SdkVersionUtils.checkedAndroid_Q() ? getRealPathAndroid_Q(id) : data.getString + (data.getColumnIndexOrThrow(MediaStore.MediaColumns.DATA)); + } + return null; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (data != null && !data.isClosed()) { + data.close(); + } + } + return null; + } + + /** + * Queries for data in the specified directory + * + * @param bucketId + * @param page + * @param limit + * @param listener + * @return + */ + public void loadPageMediaData(long bucketId, int page, int limit, OnQueryDataResultListener listener) { + loadPageMediaData(bucketId, page, limit, config.pageSize, listener); + } + + /** + * Queries for data in the specified directory + * + * @param bucketId + * @param listener + * @return + */ + public void loadPageMediaData(long bucketId, int page, OnQueryDataResultListener listener) { + loadPageMediaData(bucketId, page, config.pageSize, config.pageSize, listener); + } + + /** + * Queries for data in the specified directory (page) + * + * @param bucketId + * @param page + * @param limit + * @param pageSize + * @return + */ + public void loadPageMediaData(long bucketId, int page, int limit, int pageSize, OnQueryDataResultListener listener) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public MediaData doInBackground() { + Cursor data = null; + try { + String orderBy = page == -1 ? MediaStore.Files.FileColumns._ID + " DESC" : MediaStore.Files.FileColumns._ID + " DESC limit " + limit + " offset " + (page - 1) * pageSize; + data = mContext.getContentResolver().query(QUERY_URI, PROJECTION_PAGE, getPageSelection(bucketId), getPageSelectionArgs(bucketId), orderBy); + if (data != null) { + List result = new ArrayList<>(); + if (data.getCount() > 0) { + data.moveToFirst(); + do { + long id = data.getLong + (data.getColumnIndexOrThrow(PROJECTION_PAGE[0])); + + String absolutePath = data.getString + (data.getColumnIndexOrThrow(PROJECTION_PAGE[1])); + + String url = SdkVersionUtils.checkedAndroid_Q() ? getRealPathAndroid_Q(id) : absolutePath; + + if (config.isFilterInvalidFile) { + if (!PictureFileUtils.isFileExists(absolutePath)) { + continue; + } + } + String mimeType = data.getString + (data.getColumnIndexOrThrow(PROJECTION_PAGE[2])); + + mimeType = TextUtils.isEmpty(mimeType) ? PictureMimeType.ofJPEG() : mimeType; + // Here, it is solved that some models obtain mimeType and return the format of image / *, + // which makes it impossible to distinguish the specific type, such as mi 8,9,10 and other models + if (mimeType.endsWith("image/*")) { + if (PictureMimeType.isContent(url)) { + mimeType = PictureMimeType.getImageMimeType(absolutePath); + } else { + mimeType = PictureMimeType.getImageMimeType(url); + } + if (!config.isGif) { + boolean isGif = PictureMimeType.isGif(mimeType); + if (isGif) { + continue; + } + } + } + int width = data.getInt + (data.getColumnIndexOrThrow(PROJECTION_PAGE[3])); + + int height = data.getInt + (data.getColumnIndexOrThrow(PROJECTION_PAGE[4])); + + long duration = data.getLong + (data.getColumnIndexOrThrow(PROJECTION_PAGE[5])); + + long size = data.getLong + (data.getColumnIndexOrThrow(PROJECTION_PAGE[6])); + + String folderName = data.getString + (data.getColumnIndexOrThrow(PROJECTION_PAGE[7])); + + String fileName = data.getString + (data.getColumnIndexOrThrow(PROJECTION_PAGE[8])); + + long bucket_id = data.getLong + (data.getColumnIndexOrThrow(PROJECTION_PAGE[9])); + + if (config.filterFileSize > 0) { + if (size > config.filterFileSize * FILE_SIZE_UNIT) { + continue; + } + } + + if (PictureMimeType.isHasVideo(mimeType)) { + if (config.videoMinSecond > 0 && duration < config.videoMinSecond) { + // If you set the minimum number of seconds of video to display + continue; + } + if (config.videoMaxSecond > 0 && duration > config.videoMaxSecond) { + // If you set the maximum number of seconds of video to display + continue; + } + if (duration == 0) { + //If the length is 0, the corrupted video is processed and filtered out + continue; + } + if (size <= 0) { + // The video size is 0 to filter out + continue; + } + } + + LocalMedia image = new LocalMedia + (id, url, absolutePath, fileName, folderName, duration, config.chooseMode, mimeType, width, height, size, bucket_id); + + result.add(image); + + } while (data.moveToNext()); + } + return new MediaData(data.getCount() > 0, result); + } + } catch (Exception e) { + e.printStackTrace(); + Log.i(TAG, "loadMedia Page Data Error: " + e.getMessage()); + return null; + } finally { + if (data != null && !data.isClosed()) { + data.close(); + } + } + return null; + } + + @Override + public void onSuccess(MediaData result) { + if (listener != null && result != null) { + listener.onComplete(result.data, page, result.isHasNextMore); + } + } + }); + } + + /** + * Query the local gallery data + * + * @param listener + */ + public void loadAllMedia(OnQueryDataResultListener listener) { + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask>() { + @Override + public List doInBackground() { + Cursor data = mContext.getContentResolver().query(QUERY_URI, + SdkVersionUtils.checkedAndroid_Q() ? PROJECTION_29 : PROJECTION, + getSelection(), getSelectionArgs(), ORDER_BY); + try { + if (data != null) { + int count = data.getCount(); + int totalCount = 0; + List mediaFolders = new ArrayList<>(); + if (count > 0) { + if (SdkVersionUtils.checkedAndroid_Q()) { + Map countMap = new HashMap<>(); + while (data.moveToNext()) { + long bucketId = data.getLong(data.getColumnIndex(COLUMN_BUCKET_ID)); + Long newCount = countMap.get(bucketId); + if (newCount == null) { + newCount = 1L; + } else { + newCount++; + } + countMap.put(bucketId, newCount); + } + + if (data.moveToFirst()) { + Set hashSet = new HashSet<>(); + do { + long bucketId = data.getLong(data.getColumnIndex(COLUMN_BUCKET_ID)); + if (hashSet.contains(bucketId)) { + continue; + } + LocalMediaFolder mediaFolder = new LocalMediaFolder(); + mediaFolder.setBucketId(bucketId); + String bucketDisplayName = data.getString( + data.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME)); + long size = countMap.get(bucketId); + long id = data.getLong(data.getColumnIndex(MediaStore.Files.FileColumns._ID)); + mediaFolder.setName(bucketDisplayName); + mediaFolder.setImageNum(ValueOf.toInt(size)); + mediaFolder.setFirstImagePath(getRealPathAndroid_Q(id)); + mediaFolders.add(mediaFolder); + hashSet.add(bucketId); + totalCount += size; + } while (data.moveToNext()); + } + + } else { + data.moveToFirst(); + do { + LocalMediaFolder mediaFolder = new LocalMediaFolder(); + long bucketId = data.getLong(data.getColumnIndex(COLUMN_BUCKET_ID)); + String bucketDisplayName = data.getString(data.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME)); + int size = data.getInt(data.getColumnIndex(COLUMN_COUNT)); + mediaFolder.setBucketId(bucketId); + String url = data.getString(data.getColumnIndex(MediaStore.MediaColumns.DATA)); + mediaFolder.setFirstImagePath(url); + mediaFolder.setName(bucketDisplayName); + mediaFolder.setImageNum(size); + mediaFolders.add(mediaFolder); + totalCount += size; + } while (data.moveToNext()); + } + + sortFolder(mediaFolders); + + // 相机胶卷 + LocalMediaFolder allMediaFolder = new LocalMediaFolder(); + allMediaFolder.setImageNum(totalCount); + allMediaFolder.setChecked(true); + allMediaFolder.setBucketId(-1); + if (data.moveToFirst()) { + String firstUrl = SdkVersionUtils.checkedAndroid_Q() ? getFirstUri(data) : getFirstUrl(data); + allMediaFolder.setFirstImagePath(firstUrl); + } + String bucketDisplayName = config.chooseMode == PictureMimeType.ofAudio() ? + mContext.getString(R.string.picture_all_audio) + : mContext.getString(R.string.picture_camera_roll); + allMediaFolder.setName(bucketDisplayName); + allMediaFolder.setOfAllType(config.chooseMode); + allMediaFolder.setCameraFolder(true); + mediaFolders.add(0, allMediaFolder); + + return mediaFolders; + } + } + } catch (Exception e) { + e.printStackTrace(); + Log.i(TAG, "loadAllMedia Data Error: " + e.getMessage()); + return null; + } finally { + if (data != null && !data.isClosed()) { + data.close(); + } + } + return new ArrayList<>(); + } + + @Override + public void onSuccess(List result) { + if (listener != null && result != null) { + listener.onComplete(result, 1, false); + } + } + }); + } + + /** + * Get cover uri + * + * @param cursor + * @return + */ + private static String getFirstUri(Cursor cursor) { + long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)); + return getRealPathAndroid_Q(id); + } + + /** + * Get cover url + * + * @param cursor + * @return + */ + private static String getFirstUrl(Cursor cursor) { + return cursor.getString(cursor.getColumnIndex(MediaStore.Files.FileColumns.DATA)); + } + + + private String getPageSelection(long bucketId) { + String durationCondition = getDurationCondition(0, 0); + boolean isSpecifiedFormat = !TextUtils.isEmpty(config.specifiedFormat); + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + if (bucketId == -1) { + // ofAll + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + durationCondition + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + // Gets the specified album directory + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + " OR " + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + durationCondition + ") AND " + COLUMN_BUCKET_ID + "=? AND " + MediaStore.MediaColumns.SIZE + ">0"; + + case PictureConfig.TYPE_IMAGE: + // Gets the image of the specified type + if (bucketId == -1) { + // ofAll + if (isSpecifiedFormat) { + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF + " AND " + MediaStore.MediaColumns.MIME_TYPE + "='" + config.specifiedFormat + "'") + + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + // Gets the specified album directory + if (isSpecifiedFormat) { + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF + " AND " + MediaStore.MediaColumns.MIME_TYPE + "='" + config.specifiedFormat + "'") + + ") AND " + COLUMN_BUCKET_ID + "=? AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?" + + (config.isGif ? "" : " AND " + MediaStore.MediaColumns.MIME_TYPE + NOT_GIF) + + ") AND " + COLUMN_BUCKET_ID + "=? AND " + MediaStore.MediaColumns.SIZE + ">0"; + case PictureConfig.TYPE_VIDEO: + case PictureConfig.TYPE_AUDIO: + if (bucketId == -1) { + // ofAll + if (isSpecifiedFormat) { + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + MediaStore.MediaColumns.MIME_TYPE + "='" + config.specifiedFormat + "'" + " AND " + durationCondition + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + durationCondition + ") AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + // Gets the specified album directory + if (isSpecifiedFormat) { + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + MediaStore.MediaColumns.MIME_TYPE + "='" + config.specifiedFormat + "'" + " AND " + durationCondition + ") AND " + COLUMN_BUCKET_ID + "=? AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=? AND " + durationCondition + ") AND " + COLUMN_BUCKET_ID + "=? AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return null; + } + + private String[] getPageSelectionArgs(long bucketId) { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + if (bucketId == -1) { + // ofAll + return new String[]{ + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), + }; + } + // Gets the specified album directory + return new String[]{ + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE), + String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO), + ValueOf.toString(bucketId) + }; + case PictureConfig.TYPE_IMAGE: + // Get photo + return getSelectionArgsForPageSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE, bucketId); + case PictureConfig.TYPE_VIDEO: + // Get video + return getSelectionArgsForPageSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO, bucketId); + case PictureConfig.TYPE_AUDIO: + // Get audio + return getSelectionArgsForPageSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO, bucketId); + } + return null; + } + + + private String getSelection() { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + // Get all, not including audio + return getSelectionArgsForAllMediaCondition(getDurationCondition(0, 0), config.isGif); + case PictureConfig.TYPE_IMAGE: + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // 获取指定类型的图片 + if (SdkVersionUtils.checkedAndroid_Q()) { + return SELECTION_SPECIFIED_FORMAT_29 + "='" + config.specifiedFormat + "' AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "') AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + } + if (SdkVersionUtils.checkedAndroid_Q()) { + return config.isGif ? SELECTION_29 : SELECTION_NOT_GIF_29; + } + return config.isGif ? SELECTION : SELECTION_NOT_GIF; + case PictureConfig.TYPE_VIDEO: + // 获取视频 + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // Gets the specified album directory + if (SdkVersionUtils.checkedAndroid_Q()) { + return SELECTION_SPECIFIED_FORMAT_29 + "='" + config.specifiedFormat + "' AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "') AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + } + return getSelectionArgsForSingleMediaCondition(getDurationCondition(0, 0)); + case PictureConfig.TYPE_AUDIO: + // Get Audio + if (!TextUtils.isEmpty(config.specifiedFormat)) { + // Gets the specified album directory + if (SdkVersionUtils.checkedAndroid_Q()) { + return SELECTION_SPECIFIED_FORMAT_29 + "='" + config.specifiedFormat + "' AND " + MediaStore.MediaColumns.SIZE + ">0"; + } + return SELECTION_SPECIFIED_FORMAT + "='" + config.specifiedFormat + "') AND " + MediaStore.MediaColumns.SIZE + ">0)" + GROUP_BY_BUCKET_Id; + } + return getSelectionArgsForSingleMediaCondition(getDurationCondition(0, AUDIO_DURATION)); + } + return null; + } + + private String[] getSelectionArgs() { + switch (config.chooseMode) { + case PictureConfig.TYPE_ALL: + return SELECTION_ALL_ARGS; + case PictureConfig.TYPE_IMAGE: + // Get photo + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE); + case PictureConfig.TYPE_VIDEO: + // Get video + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO); + case PictureConfig.TYPE_AUDIO: + return getSelectionArgsForSingleMediaType(MediaStore.Files.FileColumns.MEDIA_TYPE_AUDIO); + } + return null; + } + + /** + * Sort by number of files + * + * @param imageFolders + */ + private void sortFolder(List imageFolders) { + Collections.sort(imageFolders, (lhs, rhs) -> { + if (lhs.getData() == null || rhs.getData() == null) { + return 0; + } + int lSize = lhs.getImageNum(); + int rSize = rhs.getImageNum(); + return Integer.compare(rSize, lSize); + }); + } + + /** + * Android Q + * + * @param id + * @return + */ + private static String getRealPathAndroid_Q(long id) { + return QUERY_URI.buildUpon().appendPath(ValueOf.toString(id)).build().toString(); + } + + /** + * Get video (maximum or minimum time) + * + * @param exMaxLimit + * @param exMinLimit + * @return + */ + private String getDurationCondition(long exMaxLimit, long exMinLimit) { + long maxS = config.videoMaxSecond == 0 ? Long.MAX_VALUE : config.videoMaxSecond; + if (exMaxLimit != 0) { + maxS = Math.min(maxS, exMaxLimit); + } + return String.format(Locale.CHINA, "%d <%s " + MediaStore.MediaColumns.DURATION + " and " + MediaStore.MediaColumns.DURATION + " <= %d", + Math.max(exMinLimit, config.videoMinSecond), + Math.max(exMinLimit, config.videoMinSecond) == 0 ? "" : "=", + maxS); + } + + + private static LocalMediaPageLoader instance; + + public static LocalMediaPageLoader getInstance(Context context, PictureSelectionConfig + config) { + if (instance == null) { + synchronized (LocalMediaPageLoader.class) { + if (instance == null) { + instance = new LocalMediaPageLoader(context.getApplicationContext(), config); + } + } + } + return instance; + } + + /** + * set empty + */ + public static void setInstanceNull() { + instance = null; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/observable/ImagesObservable.java b/picture_library/src/main/java/com/luck/picture/lib/observable/ImagesObservable.java new file mode 100644 index 0000000..81efaac --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/observable/ImagesObservable.java @@ -0,0 +1,52 @@ +package com.luck.picture.lib.observable; + +import com.luck.picture.lib.entity.LocalMedia; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author:luck + * @date:2017-1-12 21:30 + * @describe:解决预览时传值过大问题 + */ +public class ImagesObservable { + private List data; + private static ImagesObservable sObserver; + + public static ImagesObservable getInstance() { + if (sObserver == null) { + synchronized (ImagesObservable.class) { + if (sObserver == null) { + sObserver = new ImagesObservable(); + } + } + } + return sObserver; + } + + /** + * 存储图片用于预览时用 + * + * @param data + */ + public void savePreviewMediaData(List data) { + this.data = data; + } + + /** + * 读取预览的图片 + */ + public List readPreviewMediaData() { + return data == null ? new ArrayList<>() : data; + } + + /** + * 清空预览的图片 + */ + public void clearPreviewMediaData() { + if (data != null) { + data.clear(); + } + } +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/permissions/PermissionChecker.java b/picture_library/src/main/java/com/luck/picture/lib/permissions/PermissionChecker.java new file mode 100644 index 0000000..9773ae4 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/permissions/PermissionChecker.java @@ -0,0 +1,62 @@ +package com.luck.picture.lib.permissions; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.provider.Settings; + +import androidx.annotation.NonNull; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; + +/** + * @author:luck + * @date:2019-11-20 19:07 + * @describe:权限检查 + */ +public class PermissionChecker { + + /** + * 检查是否有某个权限 + * + * @param ctx + * @param permission + * @return + */ + public static boolean checkSelfPermission(Context ctx, String permission) { + return ContextCompat.checkSelfPermission(ctx.getApplicationContext(), permission) + == PackageManager.PERMISSION_GRANTED; + } + + + /** + * 动态申请多个权限 + * + * @param activity + * @param code + */ + public static void requestPermissions(Activity activity, @NonNull String[] permissions, int code) { + ActivityCompat.requestPermissions(activity, permissions, code); + } + + + /** + * Launch the application's details settings. + */ + public static void launchAppDetailsSettings(Context context) { + Context applicationContext = context.getApplicationContext(); + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.parse("package:" + applicationContext.getPackageName())); + if (!isIntentAvailable(context, intent)) return; + applicationContext.startActivity(intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)); + } + + private static boolean isIntentAvailable(Context context, final Intent intent) { + return context.getApplicationContext() + .getPackageManager() + .queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY) + .size() > 0; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/Compat.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/Compat.java new file mode 100644 index 0000000..66ab719 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/Compat.java @@ -0,0 +1,39 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * 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.luck.picture.lib.photoview; + +import android.annotation.TargetApi; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.view.View; + +class Compat { + + private static final int SIXTY_FPS_INTERVAL = 1000 / 60; + + public static void postOnAnimation(View view, Runnable runnable) { + if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) { + postOnAnimationJellyBean(view, runnable); + } else { + view.postDelayed(runnable, SIXTY_FPS_INTERVAL); + } + } + + @TargetApi(16) + private static void postOnAnimationJellyBean(View view, Runnable runnable) { + view.postOnAnimation(runnable); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/CustomGestureDetector.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/CustomGestureDetector.java new file mode 100644 index 0000000..26a5f3f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/CustomGestureDetector.java @@ -0,0 +1,206 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + *

+ * 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.luck.picture.lib.photoview; + +import android.content.Context; +import android.view.MotionEvent; +import android.view.ScaleGestureDetector; +import android.view.VelocityTracker; +import android.view.ViewConfiguration; + +/** + * Does a whole lot of gesture detecting. + */ +class CustomGestureDetector { + + private static final int INVALID_POINTER_ID = -1; + + private int mActivePointerId = INVALID_POINTER_ID; + private int mActivePointerIndex = 0; + private final ScaleGestureDetector mDetector; + + private VelocityTracker mVelocityTracker; + private boolean mIsDragging; + private float mLastTouchX; + private float mLastTouchY; + private final float mTouchSlop; + private final float mMinimumVelocity; + private OnGestureListener mListener; + + CustomGestureDetector(Context context, OnGestureListener listener) { + final ViewConfiguration configuration = ViewConfiguration + .get(context); + mMinimumVelocity = configuration.getScaledMinimumFlingVelocity(); + mTouchSlop = configuration.getScaledTouchSlop(); + + mListener = listener; + ScaleGestureDetector.OnScaleGestureListener mScaleListener = new ScaleGestureDetector.OnScaleGestureListener() { + + @Override + public boolean onScale(ScaleGestureDetector detector) { + float scaleFactor = detector.getScaleFactor(); + + if (Float.isNaN(scaleFactor) || Float.isInfinite(scaleFactor)) + return false; + + if (scaleFactor >= 0) { + mListener.onScale(scaleFactor, + detector.getFocusX(), detector.getFocusY()); + } + return true; + } + + @Override + public boolean onScaleBegin(ScaleGestureDetector detector) { + return true; + } + + @Override + public void onScaleEnd(ScaleGestureDetector detector) { + // NO-OP + } + }; + mDetector = new ScaleGestureDetector(context, mScaleListener); + } + + private float getActiveX(MotionEvent ev) { + try { + return ev.getX(mActivePointerIndex); + } catch (Exception e) { + return ev.getX(); + } + } + + private float getActiveY(MotionEvent ev) { + try { + return ev.getY(mActivePointerIndex); + } catch (Exception e) { + return ev.getY(); + } + } + + public boolean isScaling() { + return mDetector.isInProgress(); + } + + public boolean isDragging() { + return mIsDragging; + } + + public boolean onTouchEvent(MotionEvent ev) { + try { + mDetector.onTouchEvent(ev); + return processTouchEvent(ev); + } catch (IllegalArgumentException e) { + // Fix for support lib bug, happening when onDestroy is called + return true; + } + } + + private boolean processTouchEvent(MotionEvent ev) { + final int action = ev.getAction(); + switch (action & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + mActivePointerId = ev.getPointerId(0); + + mVelocityTracker = VelocityTracker.obtain(); + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + mIsDragging = false; + break; + case MotionEvent.ACTION_MOVE: + final float x = getActiveX(ev); + final float y = getActiveY(ev); + final float dx = x - mLastTouchX, dy = y - mLastTouchY; + + if (!mIsDragging) { + // Use Pythagoras to see if drag length is larger than + // touch slop + mIsDragging = Math.sqrt((dx * dx) + (dy * dy)) >= mTouchSlop; + } + + if (mIsDragging) { + mListener.onDrag(dx, dy); + mLastTouchX = x; + mLastTouchY = y; + + if (null != mVelocityTracker) { + mVelocityTracker.addMovement(ev); + } + } + break; + case MotionEvent.ACTION_CANCEL: + mActivePointerId = INVALID_POINTER_ID; + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + case MotionEvent.ACTION_UP: + mActivePointerId = INVALID_POINTER_ID; + if (mIsDragging) { + if (null != mVelocityTracker) { + mLastTouchX = getActiveX(ev); + mLastTouchY = getActiveY(ev); + + // Compute velocity within the last 1000ms + mVelocityTracker.addMovement(ev); + mVelocityTracker.computeCurrentVelocity(1000); + + final float vX = mVelocityTracker.getXVelocity(), vY = mVelocityTracker + .getYVelocity(); + + // If the velocity is greater than minVelocity, call + // listener + if (Math.max(Math.abs(vX), Math.abs(vY)) >= mMinimumVelocity) { + mListener.onFling(mLastTouchX, mLastTouchY, -vX, + -vY); + } + } + } + + // Recycle Velocity Tracker + if (null != mVelocityTracker) { + mVelocityTracker.recycle(); + mVelocityTracker = null; + } + break; + case MotionEvent.ACTION_POINTER_UP: + final int pointerIndex = Util.getPointerIndex(ev.getAction()); + final int pointerId = ev.getPointerId(pointerIndex); + if (pointerId == mActivePointerId) { + // This was our active pointer going up. Choose a new + // active pointer and adjust accordingly. + final int newPointerIndex = pointerIndex == 0 ? 1 : 0; + mActivePointerId = ev.getPointerId(newPointerIndex); + mLastTouchX = ev.getX(newPointerIndex); + mLastTouchY = ev.getY(newPointerIndex); + } + break; + } + + mActivePointerIndex = ev + .findPointerIndex(mActivePointerId != INVALID_POINTER_ID ? mActivePointerId + : 0); + return true; + } +} + diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnGestureListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnGestureListener.java new file mode 100644 index 0000000..6dbc9cc --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnGestureListener.java @@ -0,0 +1,27 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + * + * 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.luck.picture.lib.photoview; + +interface OnGestureListener { + + void onDrag(float dx, float dy); + + void onFling(float startX, float startY, float velocityX, + float velocityY); + + void onScale(float scaleFactor, float focusX, float focusY); + +} \ No newline at end of file diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnMatrixChangedListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnMatrixChangedListener.java new file mode 100644 index 0000000..1391942 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnMatrixChangedListener.java @@ -0,0 +1,18 @@ +package com.luck.picture.lib.photoview; + +import android.graphics.RectF; + +/** + * Interface definition for a callback to be invoked when the internal Matrix has changed for + * this View. + */ +public interface OnMatrixChangedListener { + + /** + * Callback for when the Matrix displaying the Drawable has changed. This could be because + * the View's bounds have changed, or the user has zoomed. + * + * @param rect - Rectangle displaying the Drawable's new bounds. + */ + void onMatrixChanged(RectF rect); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnOutsidePhotoTapListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnOutsidePhotoTapListener.java new file mode 100644 index 0000000..be187c5 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnOutsidePhotoTapListener.java @@ -0,0 +1,14 @@ +package com.luck.picture.lib.photoview; + +import android.widget.ImageView; + +/** + * Callback when the user tapped outside of the photo + */ +public interface OnOutsidePhotoTapListener { + + /** + * The outside of the photo has been tapped + */ + void onOutsidePhotoTap(ImageView imageView); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnPhotoTapListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnPhotoTapListener.java new file mode 100644 index 0000000..8db0570 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnPhotoTapListener.java @@ -0,0 +1,22 @@ +package com.luck.picture.lib.photoview; + +import android.widget.ImageView; + +/** + * A callback to be invoked when the Photo is tapped with a single + * tap. + */ +public interface OnPhotoTapListener { + + /** + * A callback to receive where the user taps on a photo. You will only receive a callback if + * the user taps on the actual photo, tapping on 'whitespace' will be ignored. + * + * @param view ImageView the user tapped. + * @param x where the user tapped from the of the Drawable, as percentage of the + * Drawable width. + * @param y where the user tapped from the top of the Drawable, as percentage of the + * Drawable height. + */ + void onPhotoTap(ImageView view, float x, float y); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnScaleChangedListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnScaleChangedListener.java new file mode 100644 index 0000000..815f341 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnScaleChangedListener.java @@ -0,0 +1,17 @@ +package com.luck.picture.lib.photoview; + + +/** + * Interface definition for callback to be invoked when attached ImageView scale changes + */ +public interface OnScaleChangedListener { + + /** + * Callback for when the scale changes + * + * @param scaleFactor the scale factor (less than 1 for zoom out, greater than 1 for zoom in) + * @param focusX focal point X position + * @param focusY focal point Y position + */ + void onScaleChange(float scaleFactor, float focusX, float focusY); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnSingleFlingListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnSingleFlingListener.java new file mode 100644 index 0000000..bd60f6b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnSingleFlingListener.java @@ -0,0 +1,21 @@ +package com.luck.picture.lib.photoview; + +import android.view.MotionEvent; + +/** + * A callback to be invoked when the ImageView is flung with a single + * touch + */ +public interface OnSingleFlingListener { + + /** + * A callback to receive where the user flings on a ImageView. You will receive a callback if + * the user flings anywhere on the view. + * + * @param e1 MotionEvent the user first touch. + * @param e2 MotionEvent the user last touch. + * @param velocityX distance of user's horizontal fling. + * @param velocityY distance of user's vertical fling. + */ + boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewDragListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewDragListener.java new file mode 100644 index 0000000..4023f9c --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewDragListener.java @@ -0,0 +1,16 @@ +package com.luck.picture.lib.photoview; + +/** + * Interface definition for a callback to be invoked when the photo is experiencing a drag event + */ +public interface OnViewDragListener { + + /** + * Callback for when the photo is experiencing a drag event. This cannot be invoked when the + * user is scaling. + * + * @param dx The change of the coordinates in the x-direction + * @param dy The change of the coordinates in the y-direction + */ + void onDrag(float dx, float dy); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewTapListener.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewTapListener.java new file mode 100644 index 0000000..8f918cc --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/OnViewTapListener.java @@ -0,0 +1,16 @@ +package com.luck.picture.lib.photoview; + +import android.view.View; + +public interface OnViewTapListener { + + /** + * A callback to receive where the user taps on a ImageView. You will receive a callback if + * the user taps anywhere on the view, tapping on 'whitespace' will not be ignored. + * + * @param view - View the user tapped. + * @param x - where the user tapped from the left of the View. + * @param y - where the user tapped from the top of the View. + */ + void onViewTap(View view, float x, float y); +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoView.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoView.java new file mode 100644 index 0000000..72455ac --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoView.java @@ -0,0 +1,257 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + *

+ * 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.luck.picture.lib.photoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.util.AttributeSet; +import android.view.GestureDetector; +import android.widget.ImageView; + +import androidx.appcompat.widget.AppCompatImageView; + +/** + * A zoomable {@link ImageView}. See {@link PhotoViewAttacher} for most of the details on how the zooming + * is accomplished + */ +public class PhotoView extends AppCompatImageView { + + private PhotoViewAttacher attacher; + private ScaleType pendingScaleType; + + public PhotoView(Context context) { + this(context, null); + } + + public PhotoView(Context context, AttributeSet attr) { + this(context, attr, 0); + } + + public PhotoView(Context context, AttributeSet attr, int defStyle) { + super(context, attr, defStyle); + init(); + } + + private void init() { + attacher = new PhotoViewAttacher(this); + //We always pose as a Matrix scale type, though we can change to another scale type + //via the attacher + super.setScaleType(ScaleType.MATRIX); + //apply the previously applied scale type + if (pendingScaleType != null) { + setScaleType(pendingScaleType); + pendingScaleType = null; + } + } + + /** + * Get the current {@link PhotoViewAttacher} for this view. Be wary of holding on to references + * to this attacher, as it has a reference to this view, which, if a reference is held in the + * wrong place, can cause memory leaks. + * + * @return the attacher. + */ + public PhotoViewAttacher getAttacher() { + return attacher; + } + + @Override + public ScaleType getScaleType() { + return attacher.getScaleType(); + } + + @Override + public Matrix getImageMatrix() { + return attacher.getImageMatrix(); + } + + @Override + public void setOnLongClickListener(OnLongClickListener l) { + attacher.setOnLongClickListener(l); + } + + @Override + public void setOnClickListener(OnClickListener l) { + attacher.setOnClickListener(l); + } + + @Override + public void setScaleType(ScaleType scaleType) { + if (attacher == null) { + pendingScaleType = scaleType; + } else { + attacher.setScaleType(scaleType); + } + } + + @Override + public void setImageDrawable(Drawable drawable) { + super.setImageDrawable(drawable); + // setImageBitmap calls through to this method + if (attacher != null) { + attacher.update(); + } + } + + @Override + public void setImageResource(int resId) { + super.setImageResource(resId); + if (attacher != null) { + attacher.update(); + } + } + + @Override + public void setImageURI(Uri uri) { + super.setImageURI(uri); + if (attacher != null) { + attacher.update(); + } + } + + @Override + protected boolean setFrame(int l, int t, int r, int b) { + boolean changed = super.setFrame(l, t, r, b); + if (changed) { + attacher.update(); + } + return changed; + } + + public void setRotationTo(float rotationDegree) { + attacher.setRotationTo(rotationDegree); + } + + public void setRotationBy(float rotationDegree) { + attacher.setRotationBy(rotationDegree); + } + + public boolean isZoomable() { + return attacher.isZoomable(); + } + + public void setZoomable(boolean zoomable) { + attacher.setZoomable(zoomable); + } + + public RectF getDisplayRect() { + return attacher.getDisplayRect(); + } + + public void getDisplayMatrix(Matrix matrix) { + attacher.getDisplayMatrix(matrix); + } + + @SuppressWarnings("UnusedReturnValue") + public boolean setDisplayMatrix(Matrix finalRectangle) { + return attacher.setDisplayMatrix(finalRectangle); + } + + public void getSuppMatrix(Matrix matrix) { + attacher.getSuppMatrix(matrix); + } + + public boolean setSuppMatrix(Matrix matrix) { + return attacher.setDisplayMatrix(matrix); + } + + public float getMinimumScale() { + return attacher.getMinimumScale(); + } + + public float getMediumScale() { + return attacher.getMediumScale(); + } + + public float getMaximumScale() { + return attacher.getMaximumScale(); + } + + public float getScale() { + return attacher.getScale(); + } + + public void setAllowParentInterceptOnEdge(boolean allow) { + attacher.setAllowParentInterceptOnEdge(allow); + } + + public void setMinimumScale(float minimumScale) { + attacher.setMinimumScale(minimumScale); + } + + public void setMediumScale(float mediumScale) { + attacher.setMediumScale(mediumScale); + } + + public void setMaximumScale(float maximumScale) { + attacher.setMaximumScale(maximumScale); + } + + public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { + attacher.setScaleLevels(minimumScale, mediumScale, maximumScale); + } + + public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { + attacher.setOnMatrixChangeListener(listener); + } + + public void setOnPhotoTapListener(OnPhotoTapListener listener) { + attacher.setOnPhotoTapListener(listener); + } + + public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener listener) { + attacher.setOnOutsidePhotoTapListener(listener); + } + + public void setOnViewTapListener(OnViewTapListener listener) { + attacher.setOnViewTapListener(listener); + } + + public void setOnViewDragListener(OnViewDragListener listener) { + attacher.setOnViewDragListener(listener); + } + + public void setScale(float scale) { + attacher.setScale(scale); + } + + public void setScale(float scale, boolean animate) { + attacher.setScale(scale, animate); + } + + public void setScale(float scale, float focalX, float focalY, boolean animate) { + attacher.setScale(scale, focalX, focalY, animate); + } + + public void setZoomTransitionDuration(int milliseconds) { + attacher.setZoomTransitionDuration(milliseconds); + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener onDoubleTapListener) { + attacher.setOnDoubleTapListener(onDoubleTapListener); + } + + public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangedListener) { + attacher.setOnScaleChangeListener(onScaleChangedListener); + } + + public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { + attacher.setOnSingleFlingListener(onSingleFlingListener); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoViewAttacher.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoViewAttacher.java new file mode 100644 index 0000000..aa36b43 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/PhotoViewAttacher.java @@ -0,0 +1,817 @@ +/******************************************************************************* + * Copyright 2011, 2012 Chris Banes. + *

+ * 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.luck.picture.lib.photoview; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Matrix.ScaleToFit; +import android.graphics.RectF; +import android.graphics.drawable.Drawable; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnLongClickListener; +import android.view.ViewParent; +import android.view.animation.AccelerateDecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.ImageView; +import android.widget.ImageView.ScaleType; +import android.widget.OverScroller; + +/** + * The component of {@link PhotoView} which does the work allowing for zooming, scaling, panning, etc. + * It is made public in case you need to subclass something other than {@link ImageView} and still + * gain the functionality that {@link PhotoView} offers + */ +public class PhotoViewAttacher implements View.OnTouchListener, + View.OnLayoutChangeListener { + + private static float DEFAULT_MAX_SCALE = 3.0f; + private static float DEFAULT_MID_SCALE = 1.75f; + private static float DEFAULT_MIN_SCALE = 1.0f; + private static int DEFAULT_ZOOM_DURATION = 200; + + private static final int HORIZONTAL_EDGE_NONE = -1; + private static final int HORIZONTAL_EDGE_LEFT = 0; + private static final int HORIZONTAL_EDGE_RIGHT = 1; + private static final int HORIZONTAL_EDGE_BOTH = 2; + private static final int VERTICAL_EDGE_NONE = -1; + private static final int VERTICAL_EDGE_TOP = 0; + private static final int VERTICAL_EDGE_BOTTOM = 1; + private static final int VERTICAL_EDGE_BOTH = 2; + private static int SINGLE_TOUCH = 1; + + private Interpolator mInterpolator = new AccelerateDecelerateInterpolator(); + private int mZoomDuration = DEFAULT_ZOOM_DURATION; + private float mMinScale = DEFAULT_MIN_SCALE; + private float mMidScale = DEFAULT_MID_SCALE; + private float mMaxScale = DEFAULT_MAX_SCALE; + + private boolean mAllowParentInterceptOnEdge = true; + private boolean mBlockParentIntercept = false; + + private ImageView mImageView; + + // Gesture Detectors + private GestureDetector mGestureDetector; + private CustomGestureDetector mScaleDragDetector; + + // These are set so we don't keep allocating them on the heap + private final Matrix mBaseMatrix = new Matrix(); + private final Matrix mDrawMatrix = new Matrix(); + private final Matrix mSuppMatrix = new Matrix(); + private final RectF mDisplayRect = new RectF(); + private final float[] mMatrixValues = new float[9]; + + // Listeners + private OnMatrixChangedListener mMatrixChangeListener; + private OnPhotoTapListener mPhotoTapListener; + private OnOutsidePhotoTapListener mOutsidePhotoTapListener; + private OnViewTapListener mViewTapListener; + private View.OnClickListener mOnClickListener; + private OnLongClickListener mLongClickListener; + private OnScaleChangedListener mScaleChangeListener; + private OnSingleFlingListener mSingleFlingListener; + private OnViewDragListener mOnViewDragListener; + + private FlingRunnable mCurrentFlingRunnable; + private int mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; + private int mVerticalScrollEdge = VERTICAL_EDGE_BOTH; + private float mBaseRotation; + + private boolean mZoomEnabled = true; + private ScaleType mScaleType = ScaleType.FIT_CENTER; + + private OnGestureListener onGestureListener = new OnGestureListener() { + @Override + public void onDrag(float dx, float dy) { + if (mScaleDragDetector.isScaling()) { + return; // Do not drag if we are already scaling + } + if (mOnViewDragListener != null) { + mOnViewDragListener.onDrag(dx, dy); + } + mSuppMatrix.postTranslate(dx, dy); + checkAndDisplayMatrix(); + + /* + * Here we decide whether to let the ImageView's parent to start taking + * over the touch event. + * + * First we check whether this function is enabled. We never want the + * parent to take over if we're scaling. We then check the edge we're + * on, and the direction of the scroll (i.e. if we're pulling against + * the edge, aka 'overscrolling', let the parent take over). + */ + ViewParent parent = mImageView.getParent(); + if (mAllowParentInterceptOnEdge && !mScaleDragDetector.isScaling() && !mBlockParentIntercept) { + if (mHorizontalScrollEdge == HORIZONTAL_EDGE_BOTH + || (mHorizontalScrollEdge == HORIZONTAL_EDGE_LEFT && dx >= 1f) + || (mHorizontalScrollEdge == HORIZONTAL_EDGE_RIGHT && dx <= -1f) + || (mVerticalScrollEdge == VERTICAL_EDGE_TOP && dy >= 1f) + || (mVerticalScrollEdge == VERTICAL_EDGE_BOTTOM && dy <= -1f)) { + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(false); + } + } + } else { + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + } + } + + @Override + public void onFling(float startX, float startY, float velocityX, float velocityY) { + mCurrentFlingRunnable = new FlingRunnable(mImageView.getContext()); + mCurrentFlingRunnable.fling(getImageViewWidth(mImageView), + getImageViewHeight(mImageView), (int) velocityX, (int) velocityY); + mImageView.post(mCurrentFlingRunnable); + } + + @Override + public void onScale(float scaleFactor, float focusX, float focusY) { + if (getScale() < mMaxScale || scaleFactor < 1f) { + if (mScaleChangeListener != null) { + mScaleChangeListener.onScaleChange(scaleFactor, focusX, focusY); + } + mSuppMatrix.postScale(scaleFactor, scaleFactor, focusX, focusY); + checkAndDisplayMatrix(); + } + } + }; + + public PhotoViewAttacher(ImageView imageView) { + mImageView = imageView; + imageView.setOnTouchListener(this); + imageView.addOnLayoutChangeListener(this); + if (imageView.isInEditMode()) { + return; + } + mBaseRotation = 0.0f; + // Create Gesture Detectors... + mScaleDragDetector = new CustomGestureDetector(imageView.getContext(), onGestureListener); + mGestureDetector = new GestureDetector(imageView.getContext(), new GestureDetector.SimpleOnGestureListener() { + + // forward long click listener + @Override + public void onLongPress(MotionEvent e) { + if (mLongClickListener != null) { + mLongClickListener.onLongClick(mImageView); + } + } + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, + float velocityX, float velocityY) { + if (mSingleFlingListener != null) { + if (getScale() > DEFAULT_MIN_SCALE) { + return false; + } + if (e1.getPointerCount() > SINGLE_TOUCH + || e2.getPointerCount() > SINGLE_TOUCH) { + return false; + } + return mSingleFlingListener.onFling(e1, e2, velocityX, velocityY); + } + return false; + } + }); + mGestureDetector.setOnDoubleTapListener(new GestureDetector.OnDoubleTapListener() { + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + if (mOnClickListener != null) { + mOnClickListener.onClick(mImageView); + } + final RectF displayRect = getDisplayRect(); + final float x = e.getX(), y = e.getY(); + if (mViewTapListener != null) { + mViewTapListener.onViewTap(mImageView, x, y); + } + if (displayRect != null) { + // Check to see if the user tapped on the photo + if (displayRect.contains(x, y)) { + float xResult = (x - displayRect.left) + / displayRect.width(); + float yResult = (y - displayRect.top) + / displayRect.height(); + if (mPhotoTapListener != null) { + mPhotoTapListener.onPhotoTap(mImageView, xResult, yResult); + } + return true; + } else { + if (mOutsidePhotoTapListener != null) { + mOutsidePhotoTapListener.onOutsidePhotoTap(mImageView); + } + } + } + return false; + } + + @Override + public boolean onDoubleTap(MotionEvent ev) { + try { + float scale = getScale(); + float x = ev.getX(); + float y = ev.getY(); + if (scale < getMediumScale()) { + setScale(getMediumScale(), x, y, true); + } else if (scale >= getMediumScale() && scale < getMaximumScale()) { + setScale(getMaximumScale(), x, y, true); + } else { + setScale(getMinimumScale(), x, y, true); + } + } catch (ArrayIndexOutOfBoundsException e) { + // Can sometimes happen when getX() and getY() is called + } + return true; + } + + @Override + public boolean onDoubleTapEvent(MotionEvent e) { + // Wait for the confirmed onDoubleTap() instead + return false; + } + }); + } + + public void setOnDoubleTapListener(GestureDetector.OnDoubleTapListener newOnDoubleTapListener) { + this.mGestureDetector.setOnDoubleTapListener(newOnDoubleTapListener); + } + + public void setOnScaleChangeListener(OnScaleChangedListener onScaleChangeListener) { + this.mScaleChangeListener = onScaleChangeListener; + } + + public void setOnSingleFlingListener(OnSingleFlingListener onSingleFlingListener) { + this.mSingleFlingListener = onSingleFlingListener; + } + + @Deprecated + public boolean isZoomEnabled() { + return mZoomEnabled; + } + + public RectF getDisplayRect() { + checkMatrixBounds(); + return getDisplayRect(getDrawMatrix()); + } + + public boolean setDisplayMatrix(Matrix finalMatrix) { + if (finalMatrix == null) { + throw new IllegalArgumentException("Matrix cannot be null"); + } + if (mImageView.getDrawable() == null) { + return false; + } + mSuppMatrix.set(finalMatrix); + checkAndDisplayMatrix(); + return true; + } + + public void setBaseRotation(final float degrees) { + mBaseRotation = degrees % 360; + update(); + setRotationBy(mBaseRotation); + checkAndDisplayMatrix(); + } + + public void setRotationTo(float degrees) { + mSuppMatrix.setRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public void setRotationBy(float degrees) { + mSuppMatrix.postRotate(degrees % 360); + checkAndDisplayMatrix(); + } + + public float getMinimumScale() { + return mMinScale; + } + + public float getMediumScale() { + return mMidScale; + } + + public float getMaximumScale() { + return mMaxScale; + } + + public float getScale() { + return (float) Math.sqrt((float) Math.pow(getValue(mSuppMatrix, Matrix.MSCALE_X), 2) + (float) Math.pow + (getValue(mSuppMatrix, Matrix.MSKEW_Y), 2)); + } + + public ScaleType getScaleType() { + return mScaleType; + } + + @Override + public void onLayoutChange(View v, int left, int top, int right, int bottom, int oldLeft, int oldTop, int + oldRight, int oldBottom) { + // Update our base matrix, as the bounds have changed + if (left != oldLeft || top != oldTop || right != oldRight || bottom != oldBottom) { + updateBaseMatrix(mImageView.getDrawable()); + } + } + + @Override + public boolean onTouch(View v, MotionEvent ev) { + boolean handled = false; + if (mZoomEnabled && Util.hasDrawable((ImageView) v)) { + switch (ev.getAction()) { + case MotionEvent.ACTION_DOWN: + ViewParent parent = v.getParent(); + // First, disable the Parent from intercepting the touch + // event + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(true); + } + // If we're flinging, and the user presses down, cancel + // fling + cancelFling(); + break; + case MotionEvent.ACTION_CANCEL: + case MotionEvent.ACTION_UP: + // If the user has zoomed less than min scale, zoom back + // to min scale + if (getScale() < mMinScale) { + RectF rect = getDisplayRect(); + if (rect != null) { + v.post(new AnimatedZoomRunnable(getScale(), mMinScale, + rect.centerX(), rect.centerY())); + handled = true; + } + } else if (getScale() > mMaxScale) { + RectF rect = getDisplayRect(); + if (rect != null) { + v.post(new AnimatedZoomRunnable(getScale(), mMaxScale, + rect.centerX(), rect.centerY())); + handled = true; + } + } + break; + } + // Try the Scale/Drag detector + if (mScaleDragDetector != null) { + boolean wasScaling = mScaleDragDetector.isScaling(); + boolean wasDragging = mScaleDragDetector.isDragging(); + handled = mScaleDragDetector.onTouchEvent(ev); + boolean didntScale = !wasScaling && !mScaleDragDetector.isScaling(); + boolean didntDrag = !wasDragging && !mScaleDragDetector.isDragging(); + mBlockParentIntercept = didntScale && didntDrag; + } + // Check to see if the user double tapped + if (mGestureDetector != null && mGestureDetector.onTouchEvent(ev)) { + handled = true; + } + + } + return handled; + } + + public void setAllowParentInterceptOnEdge(boolean allow) { + mAllowParentInterceptOnEdge = allow; + } + + public void setMinimumScale(float minimumScale) { + Util.checkZoomLevels(minimumScale, mMidScale, mMaxScale); + mMinScale = minimumScale; + } + + public void setMediumScale(float mediumScale) { + Util.checkZoomLevels(mMinScale, mediumScale, mMaxScale); + mMidScale = mediumScale; + } + + public void setMaximumScale(float maximumScale) { + Util.checkZoomLevels(mMinScale, mMidScale, maximumScale); + mMaxScale = maximumScale; + } + + public void setScaleLevels(float minimumScale, float mediumScale, float maximumScale) { + Util.checkZoomLevels(minimumScale, mediumScale, maximumScale); + mMinScale = minimumScale; + mMidScale = mediumScale; + mMaxScale = maximumScale; + } + + public void setOnLongClickListener(OnLongClickListener listener) { + mLongClickListener = listener; + } + + public void setOnClickListener(View.OnClickListener listener) { + mOnClickListener = listener; + } + + public void setOnMatrixChangeListener(OnMatrixChangedListener listener) { + mMatrixChangeListener = listener; + } + + public void setOnPhotoTapListener(OnPhotoTapListener listener) { + mPhotoTapListener = listener; + } + + public void setOnOutsidePhotoTapListener(OnOutsidePhotoTapListener mOutsidePhotoTapListener) { + this.mOutsidePhotoTapListener = mOutsidePhotoTapListener; + } + + public void setOnViewTapListener(OnViewTapListener listener) { + mViewTapListener = listener; + } + + public void setOnViewDragListener(OnViewDragListener listener) { + mOnViewDragListener = listener; + } + + public void setScale(float scale) { + setScale(scale, false); + } + + public void setScale(float scale, boolean animate) { + setScale(scale, + (mImageView.getRight()) / 2, + (mImageView.getBottom()) / 2, + animate); + } + + public void setScale(float scale, float focalX, float focalY, + boolean animate) { + // Check to see if the scale is within bounds + if (scale < mMinScale || scale > mMaxScale) { + throw new IllegalArgumentException("Scale must be within the range of minScale and maxScale"); + } + if (animate) { + mImageView.post(new AnimatedZoomRunnable(getScale(), scale, + focalX, focalY)); + } else { + mSuppMatrix.setScale(scale, scale, focalX, focalY); + checkAndDisplayMatrix(); + } + } + + /** + * Set the zoom interpolator + * + * @param interpolator the zoom interpolator + */ + public void setZoomInterpolator(Interpolator interpolator) { + mInterpolator = interpolator; + } + + public void setScaleType(ScaleType scaleType) { + if (Util.isSupportedScaleType(scaleType) && scaleType != mScaleType) { + mScaleType = scaleType; + update(); + } + } + + public boolean isZoomable() { + return mZoomEnabled; + } + + public void setZoomable(boolean zoomable) { + mZoomEnabled = zoomable; + update(); + } + + public void update() { + if (mZoomEnabled) { + // Update the base matrix using the current drawable + updateBaseMatrix(mImageView.getDrawable()); + } else { + // Reset the Matrix... + resetMatrix(); + } + } + + /** + * Get the display matrix + * + * @param matrix target matrix to copy to + */ + public void getDisplayMatrix(Matrix matrix) { + matrix.set(getDrawMatrix()); + } + + /** + * Get the current support matrix + */ + public void getSuppMatrix(Matrix matrix) { + matrix.set(mSuppMatrix); + } + + private Matrix getDrawMatrix() { + mDrawMatrix.set(mBaseMatrix); + mDrawMatrix.postConcat(mSuppMatrix); + return mDrawMatrix; + } + + public Matrix getImageMatrix() { + return mDrawMatrix; + } + + public void setZoomTransitionDuration(int milliseconds) { + this.mZoomDuration = milliseconds; + } + + /** + * Helper method that 'unpacks' a Matrix and returns the required value + * + * @param matrix Matrix to unpack + * @param whichValue Which value from Matrix.M* to return + * @return returned value + */ + private float getValue(Matrix matrix, int whichValue) { + matrix.getValues(mMatrixValues); + return mMatrixValues[whichValue]; + } + + /** + * Resets the Matrix back to FIT_CENTER, and then displays its contents + */ + private void resetMatrix() { + mSuppMatrix.reset(); + setRotationBy(mBaseRotation); + setImageViewMatrix(getDrawMatrix()); + checkMatrixBounds(); + } + + private void setImageViewMatrix(Matrix matrix) { + mImageView.setImageMatrix(matrix); + // Call MatrixChangedListener if needed + if (mMatrixChangeListener != null) { + RectF displayRect = getDisplayRect(matrix); + if (displayRect != null) { + mMatrixChangeListener.onMatrixChanged(displayRect); + } + } + } + + /** + * Helper method that simply checks the Matrix, and then displays the result + */ + private void checkAndDisplayMatrix() { + if (checkMatrixBounds()) { + setImageViewMatrix(getDrawMatrix()); + } + } + + /** + * Helper method that maps the supplied Matrix to the current Drawable + * + * @param matrix - Matrix to map Drawable against + * @return RectF - Displayed Rectangle + */ + private RectF getDisplayRect(Matrix matrix) { + Drawable d = mImageView.getDrawable(); + if (d != null) { + mDisplayRect.set(0, 0, d.getIntrinsicWidth(), + d.getIntrinsicHeight()); + matrix.mapRect(mDisplayRect); + return mDisplayRect; + } + return null; + } + + /** + * Calculate Matrix for FIT_CENTER + * + * @param drawable - Drawable being displayed + */ + private void updateBaseMatrix(Drawable drawable) { + if (drawable == null) { + return; + } + final float viewWidth = getImageViewWidth(mImageView); + final float viewHeight = getImageViewHeight(mImageView); + final int drawableWidth = drawable.getIntrinsicWidth(); + final int drawableHeight = drawable.getIntrinsicHeight(); + mBaseMatrix.reset(); + final float widthScale = viewWidth / drawableWidth; + final float heightScale = viewHeight / drawableHeight; + if (mScaleType == ScaleType.CENTER) { + mBaseMatrix.postTranslate((viewWidth - drawableWidth) / 2F, + (viewHeight - drawableHeight) / 2F); + + } else if (mScaleType == ScaleType.CENTER_CROP) { + float scale = Math.max(widthScale, heightScale); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, + (viewHeight - drawableHeight * scale) / 2F); + + } else if (mScaleType == ScaleType.CENTER_INSIDE) { + float scale = Math.min(1.0f, Math.min(widthScale, heightScale)); + mBaseMatrix.postScale(scale, scale); + mBaseMatrix.postTranslate((viewWidth - drawableWidth * scale) / 2F, + (viewHeight - drawableHeight * scale) / 2F); + + } else { + RectF mTempSrc = new RectF(0, 0, drawableWidth, drawableHeight); + RectF mTempDst = new RectF(0, 0, viewWidth, viewHeight); + if ((int) mBaseRotation % 180 != 0) { + mTempSrc = new RectF(0, 0, drawableHeight, drawableWidth); + } + switch (mScaleType) { + case FIT_CENTER: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.CENTER); + break; + case FIT_START: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.START); + break; + case FIT_END: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.END); + break; + case FIT_XY: + mBaseMatrix.setRectToRect(mTempSrc, mTempDst, ScaleToFit.FILL); + break; + default: + break; + } + } + resetMatrix(); + } + + private boolean checkMatrixBounds() { + final RectF rect = getDisplayRect(getDrawMatrix()); + if (rect == null) { + return false; + } + final float height = rect.height(), width = rect.width(); + float deltaX = 0, deltaY = 0; + final int viewHeight = getImageViewHeight(mImageView); + if (height <= viewHeight) { + switch (mScaleType) { + case FIT_START: + deltaY = -rect.top; + break; + case FIT_END: + deltaY = viewHeight - height - rect.top; + break; + default: + deltaY = (viewHeight - height) / 2 - rect.top; + break; + } + mVerticalScrollEdge = VERTICAL_EDGE_BOTH; + } else if (rect.top > 0) { + mVerticalScrollEdge = VERTICAL_EDGE_TOP; + deltaY = -rect.top; + } else if (rect.bottom < viewHeight) { + mVerticalScrollEdge = VERTICAL_EDGE_BOTTOM; + deltaY = viewHeight - rect.bottom; + } else { + mVerticalScrollEdge = VERTICAL_EDGE_NONE; + } + final int viewWidth = getImageViewWidth(mImageView); + if (width <= viewWidth) { + switch (mScaleType) { + case FIT_START: + deltaX = -rect.left; + break; + case FIT_END: + deltaX = viewWidth - width - rect.left; + break; + default: + deltaX = (viewWidth - width) / 2 - rect.left; + break; + } + mHorizontalScrollEdge = HORIZONTAL_EDGE_BOTH; + } else if (rect.left > 0) { + mHorizontalScrollEdge = HORIZONTAL_EDGE_LEFT; + deltaX = -rect.left; + } else if (rect.right < viewWidth) { + deltaX = viewWidth - rect.right; + mHorizontalScrollEdge = HORIZONTAL_EDGE_RIGHT; + } else { + mHorizontalScrollEdge = HORIZONTAL_EDGE_NONE; + } + // Finally actually translate the matrix + mSuppMatrix.postTranslate(deltaX, deltaY); + return true; + } + + private int getImageViewWidth(ImageView imageView) { + return imageView.getWidth() - imageView.getPaddingLeft() - imageView.getPaddingRight(); + } + + private int getImageViewHeight(ImageView imageView) { + return imageView.getHeight() - imageView.getPaddingTop() - imageView.getPaddingBottom(); + } + + private void cancelFling() { + if (mCurrentFlingRunnable != null) { + mCurrentFlingRunnable.cancelFling(); + mCurrentFlingRunnable = null; + } + } + + private class AnimatedZoomRunnable implements Runnable { + + private final float mFocalX, mFocalY; + private final long mStartTime; + private final float mZoomStart, mZoomEnd; + + public AnimatedZoomRunnable(final float currentZoom, final float targetZoom, + final float focalX, final float focalY) { + mFocalX = focalX; + mFocalY = focalY; + mStartTime = System.currentTimeMillis(); + mZoomStart = currentZoom; + mZoomEnd = targetZoom; + } + + @Override + public void run() { + float t = interpolate(); + float scale = mZoomStart + t * (mZoomEnd - mZoomStart); + float deltaScale = scale / getScale(); + onGestureListener.onScale(deltaScale, mFocalX, mFocalY); + // We haven't hit our target scale yet, so post ourselves again + if (t < 1f) { + Compat.postOnAnimation(mImageView, this); + } + } + + private float interpolate() { + float t = 1f * (System.currentTimeMillis() - mStartTime) / mZoomDuration; + t = Math.min(1f, t); + t = mInterpolator.getInterpolation(t); + return t; + } + } + + private class FlingRunnable implements Runnable { + + private final OverScroller mScroller; + private int mCurrentX, mCurrentY; + + public FlingRunnable(Context context) { + mScroller = new OverScroller(context); + } + + public void cancelFling() { + mScroller.forceFinished(true); + } + + public void fling(int viewWidth, int viewHeight, int velocityX, + int velocityY) { + final RectF rect = getDisplayRect(); + if (rect == null) { + return; + } + final int startX = Math.round(-rect.left); + final int minX, maxX, minY, maxY; + if (viewWidth < rect.width()) { + minX = 0; + maxX = Math.round(rect.width() - viewWidth); + } else { + minX = maxX = startX; + } + final int startY = Math.round(-rect.top); + if (viewHeight < rect.height()) { + minY = 0; + maxY = Math.round(rect.height() - viewHeight); + } else { + minY = maxY = startY; + } + mCurrentX = startX; + mCurrentY = startY; + // If we actually can move, fling the scroller + if (startX != maxX || startY != maxY) { + mScroller.fling(startX, startY, velocityX, velocityY, minX, + maxX, minY, maxY, 0, 0); + } + } + + @Override + public void run() { + if (mScroller.isFinished()) { + return; // remaining post that should not be handled + } + if (mScroller.computeScrollOffset()) { + final int newX = mScroller.getCurrX(); + final int newY = mScroller.getCurrY(); + mSuppMatrix.postTranslate(mCurrentX - newX, mCurrentY - newY); + checkAndDisplayMatrix(); + mCurrentX = newX; + mCurrentY = newY; + // Post On animation + Compat.postOnAnimation(mImageView, this); + } + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/photoview/Util.java b/picture_library/src/main/java/com/luck/picture/lib/photoview/Util.java new file mode 100644 index 0000000..58175c5 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/photoview/Util.java @@ -0,0 +1,37 @@ +package com.luck.picture.lib.photoview; + +import android.view.MotionEvent; +import android.widget.ImageView; + +class Util { + + static void checkZoomLevels(float minZoom, float midZoom, + float maxZoom) { + if (minZoom >= midZoom) { + throw new IllegalArgumentException( + "Minimum zoom has to be less than Medium zoom. Call setMinimumZoom() with a more appropriate value"); + } else if (midZoom >= maxZoom) { + throw new IllegalArgumentException( + "Medium zoom has to be less than Maximum zoom. Call setMaximumZoom() with a more appropriate value"); + } + } + + static boolean hasDrawable(ImageView imageView) { + return imageView.getDrawable() != null; + } + + static boolean isSupportedScaleType(final ImageView.ScaleType scaleType) { + if (scaleType == null) { + return false; + } + switch (scaleType) { + case MATRIX: + throw new IllegalStateException("Matrix scale type is not supported"); + } + return true; + } + + static int getPointerIndex(int action) { + return (action & MotionEvent.ACTION_POINTER_INDEX_MASK) >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/style/PictureCropParameterStyle.java b/picture_library/src/main/java/com/luck/picture/lib/style/PictureCropParameterStyle.java new file mode 100644 index 0000000..f307f0a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/style/PictureCropParameterStyle.java @@ -0,0 +1,104 @@ +package com.luck.picture.lib.style; + + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.ColorInt; + +/** + * @author:luck + * @date:2019-11-22 17:24 + * @describe:裁剪动态样式参数设置 + */ +public class PictureCropParameterStyle implements Parcelable { + /** + * 是否改变状态栏字体颜色 黑白切换 + */ + public boolean isChangeStatusBarFontColor; + + /** + * 裁剪页标题背景颜色 + */ + @ColorInt + public int cropTitleBarBackgroundColor; + /** + * 裁剪页状态栏颜色 + */ + @ColorInt + public int cropStatusBarColorPrimaryDark; + /** + * 裁剪页标题栏字体颜色 + */ + @ColorInt + public int cropTitleColor; + + /** + * # SDK Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP有效 + * 裁剪导航条颜色 + */ + @ColorInt + public int cropNavBarColor; + + + public PictureCropParameterStyle() { + super(); + } + + public PictureCropParameterStyle(int cropTitleBarBackgroundColor, + int cropStatusBarColorPrimaryDark, + int cropTitleColor, + boolean isChangeStatusBarFontColor) { + this.cropTitleBarBackgroundColor = cropTitleBarBackgroundColor; + this.cropStatusBarColorPrimaryDark = cropStatusBarColorPrimaryDark; + this.cropTitleColor = cropTitleColor; + this.isChangeStatusBarFontColor = isChangeStatusBarFontColor; + } + + public PictureCropParameterStyle(int cropTitleBarBackgroundColor, + int cropStatusBarColorPrimaryDark, + int cropNavBarColor, + int cropTitleColor, + boolean isChangeStatusBarFontColor) { + this.cropTitleBarBackgroundColor = cropTitleBarBackgroundColor; + this.cropNavBarColor = cropNavBarColor; + this.cropStatusBarColorPrimaryDark = cropStatusBarColorPrimaryDark; + this.cropTitleColor = cropTitleColor; + this.isChangeStatusBarFontColor = isChangeStatusBarFontColor; + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte(this.isChangeStatusBarFontColor ? (byte) 1 : (byte) 0); + dest.writeInt(this.cropTitleBarBackgroundColor); + dest.writeInt(this.cropStatusBarColorPrimaryDark); + dest.writeInt(this.cropTitleColor); + dest.writeInt(this.cropNavBarColor); + } + + protected PictureCropParameterStyle(Parcel in) { + this.isChangeStatusBarFontColor = in.readByte() != 0; + this.cropTitleBarBackgroundColor = in.readInt(); + this.cropStatusBarColorPrimaryDark = in.readInt(); + this.cropTitleColor = in.readInt(); + this.cropNavBarColor = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PictureCropParameterStyle createFromParcel(Parcel source) { + return new PictureCropParameterStyle(source); + } + + @Override + public PictureCropParameterStyle[] newArray(int size) { + return new PictureCropParameterStyle[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/style/PictureParameterStyle.java b/picture_library/src/main/java/com/luck/picture/lib/style/PictureParameterStyle.java new file mode 100644 index 0000000..f91b227 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/style/PictureParameterStyle.java @@ -0,0 +1,396 @@ +package com.luck.picture.lib.style; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.ColorInt; +import androidx.annotation.DrawableRes; + +/** + * @author:luck + * @date:2019-11-22 17:24 + * @describe:相册动态样式参数设置 + */ +public class PictureParameterStyle implements Parcelable { + /** + * 是否改变状态栏字体颜色 黑白切换 + */ + public boolean isChangeStatusBarFontColor; + /** + * 是否开启 已完成(0/9) 模式 + */ + public boolean isOpenCompletedNumStyle; + /** + * 是否开启QQ 数字选择风格 + */ + public boolean isOpenCheckNumStyle; + + /** + * 状态栏色值 + */ + @ColorInt + public int pictureStatusBarColor; + + /** + * 标题栏背景色 + */ + @ColorInt + public int pictureTitleBarBackgroundColor; + + /** + * 相册父容器背景色 + */ + @ColorInt + public int pictureContainerBackgroundColor; + + /** + * 相册标题色值 + */ + @ColorInt + public int pictureTitleTextColor; + + /** + * 相册标题字体大小 + */ + public int pictureTitleTextSize; + + /** + * 相册取消按钮色值 + */ + @ColorInt + @Deprecated + public int pictureCancelTextColor; + + /** + * 相册右侧按钮色值 + */ + @ColorInt + public int pictureRightDefaultTextColor; + + /** + * 相册右侧文字字体大小 + */ + public int pictureRightTextSize; + + /** + * 相册右侧按钮文本 + */ + public String pictureRightDefaultText; + + /** + * 相册右侧按钮色值 + */ + @ColorInt + public int pictureRightSelectedTextColor; + + /** + * 相册列表底部背景色 + */ + @ColorInt + public int pictureBottomBgColor; + + /** + * 相册列表已完成按钮色值 + */ + @ColorInt + public int pictureCompleteTextColor; + + /** + * 相册列表未完成按钮色值 + */ + @ColorInt + public int pictureUnCompleteTextColor; + + /** + * 相册列表完成按钮字体大小 + */ + public int pictureCompleteTextSize; + + /** + * 相册列表不可预览文字颜色 + */ + @ColorInt + public int pictureUnPreviewTextColor; + + /** + * 相册列表预览文字大小 + */ + public int picturePreviewTextSize; + + /** + * 相册列表未完成按钮文本 + */ + public String pictureUnCompleteText; + + /** + * 相册列表已完成按钮文本 + */ + public String pictureCompleteText; + + /** + * 相册列表预览文字颜色 + */ + @ColorInt + public int picturePreviewTextColor; + + /** + * 相册列表不可预览文字 + */ + public String pictureUnPreviewText; + + /** + * 相册列表预览文字 + */ + public String picturePreviewText; + + /** + * 相册列表预览界面底部背景色 + */ + @ColorInt + public int picturePreviewBottomBgColor; + + /** + * # SDK Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP有效 + * 相册导航条颜色 + */ + @ColorInt + public int pictureNavBarColor; + + /** + * 原图字体颜色 + */ + @ColorInt + public int pictureOriginalFontColor; + + /** + * 原图字体大小 + */ + public int pictureOriginalTextSize; + + /** + * 相册右侧按钮不可点击背景样式 + */ + @DrawableRes + public int pictureUnCompleteBackgroundStyle; + + /** + * 相册右侧按钮可点击背景样式 + */ + @DrawableRes + public int pictureCompleteBackgroundStyle; + + /** + * 相册标题右侧箭头 + */ + @DrawableRes + public int pictureTitleUpResId; + /** + * 相册标题右侧箭头 + */ + @DrawableRes + public int pictureTitleDownResId; + + /** + * 相册返回图标 + */ + @DrawableRes + public int pictureLeftBackIcon; + + /** + * 相册勾选CheckBox drawable样式 + */ + @DrawableRes + public int pictureCheckedStyle; + + /** + * 是否使用(%1$d/%2$d)字符串 + */ + public boolean isCompleteReplaceNum; + + /** + * WeChatStyle 预览右下角 勾选CheckBox drawable样式 + */ + @DrawableRes + public int pictureWeChatChooseStyle; + + /** + * WeChatStyle 预览界面返回键样式 + */ + @DrawableRes + public int pictureWeChatLeftBackStyle; + + /** + * WeChatStyle 相册界面标题背景样式 + */ + @DrawableRes + public int pictureWeChatTitleBackgroundStyle; + + /** + * WeChatStyle 自定义预览页右下角选择文字大小 + */ + public int pictureWeChatPreviewSelectedTextSize; + + /** + * WeChatStyle 自定义预览页右下角选择文字文案 + */ + public String pictureWeChatPreviewSelectedText; + + /** + * 图片已选数量圆点背景色 + */ + @DrawableRes + public int pictureCheckNumBgStyle; + + /** + * 相册文件夹列表选中圆点 + */ + @DrawableRes + public int pictureFolderCheckedDotStyle; + + /** + * 外部预览图片删除按钮样式 + */ + @DrawableRes + public int pictureExternalPreviewDeleteStyle; + + /** + * 原图勾选样式 + */ + @DrawableRes + public int pictureOriginalControlStyle; + + + /** + * 外部预览图片是否显示删除按钮 + */ + public boolean pictureExternalPreviewGonePreviewDelete; + + + /** + * 选择相册目录背景样式 + */ + @DrawableRes + public int pictureAlbumStyle; + + + public PictureParameterStyle() { + super(); + } + + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeByte(this.isChangeStatusBarFontColor ? (byte) 1 : (byte) 0); + dest.writeByte(this.isOpenCompletedNumStyle ? (byte) 1 : (byte) 0); + dest.writeByte(this.isOpenCheckNumStyle ? (byte) 1 : (byte) 0); + dest.writeInt(this.pictureStatusBarColor); + dest.writeInt(this.pictureTitleBarBackgroundColor); + dest.writeInt(this.pictureContainerBackgroundColor); + dest.writeInt(this.pictureTitleTextColor); + dest.writeInt(this.pictureTitleTextSize); + dest.writeInt(this.pictureCancelTextColor); + dest.writeInt(this.pictureRightDefaultTextColor); + dest.writeInt(this.pictureRightTextSize); + dest.writeString(this.pictureRightDefaultText); + dest.writeInt(this.pictureRightSelectedTextColor); + dest.writeInt(this.pictureBottomBgColor); + dest.writeInt(this.pictureCompleteTextColor); + dest.writeInt(this.pictureUnCompleteTextColor); + dest.writeInt(this.pictureCompleteTextSize); + dest.writeInt(this.pictureUnPreviewTextColor); + dest.writeInt(this.picturePreviewTextSize); + dest.writeString(this.pictureUnCompleteText); + dest.writeString(this.pictureCompleteText); + dest.writeInt(this.picturePreviewTextColor); + dest.writeString(this.pictureUnPreviewText); + dest.writeString(this.picturePreviewText); + dest.writeInt(this.picturePreviewBottomBgColor); + dest.writeInt(this.pictureNavBarColor); + dest.writeInt(this.pictureOriginalFontColor); + dest.writeInt(this.pictureOriginalTextSize); + dest.writeInt(this.pictureUnCompleteBackgroundStyle); + dest.writeInt(this.pictureCompleteBackgroundStyle); + dest.writeInt(this.pictureTitleUpResId); + dest.writeInt(this.pictureTitleDownResId); + dest.writeInt(this.pictureLeftBackIcon); + dest.writeInt(this.pictureCheckedStyle); + dest.writeByte(this.isCompleteReplaceNum ? (byte) 1 : (byte) 0); + dest.writeInt(this.pictureWeChatChooseStyle); + dest.writeInt(this.pictureWeChatLeftBackStyle); + dest.writeInt(this.pictureWeChatTitleBackgroundStyle); + dest.writeInt(this.pictureWeChatPreviewSelectedTextSize); + dest.writeString(this.pictureWeChatPreviewSelectedText); + dest.writeInt(this.pictureCheckNumBgStyle); + dest.writeInt(this.pictureFolderCheckedDotStyle); + dest.writeInt(this.pictureExternalPreviewDeleteStyle); + dest.writeInt(this.pictureOriginalControlStyle); + dest.writeByte(this.pictureExternalPreviewGonePreviewDelete ? (byte) 1 : (byte) 0); + dest.writeInt(this.pictureAlbumStyle); + } + + protected PictureParameterStyle(Parcel in) { + this.isChangeStatusBarFontColor = in.readByte() != 0; + this.isOpenCompletedNumStyle = in.readByte() != 0; + this.isOpenCheckNumStyle = in.readByte() != 0; + this.pictureStatusBarColor = in.readInt(); + this.pictureTitleBarBackgroundColor = in.readInt(); + this.pictureContainerBackgroundColor = in.readInt(); + this.pictureTitleTextColor = in.readInt(); + this.pictureTitleTextSize = in.readInt(); + this.pictureCancelTextColor = in.readInt(); + this.pictureRightDefaultTextColor = in.readInt(); + this.pictureRightTextSize = in.readInt(); + this.pictureRightDefaultText = in.readString(); + this.pictureRightSelectedTextColor = in.readInt(); + this.pictureBottomBgColor = in.readInt(); + this.pictureCompleteTextColor = in.readInt(); + this.pictureUnCompleteTextColor = in.readInt(); + this.pictureCompleteTextSize = in.readInt(); + this.pictureUnPreviewTextColor = in.readInt(); + this.picturePreviewTextSize = in.readInt(); + this.pictureUnCompleteText = in.readString(); + this.pictureCompleteText = in.readString(); + this.picturePreviewTextColor = in.readInt(); + this.pictureUnPreviewText = in.readString(); + this.picturePreviewText = in.readString(); + this.picturePreviewBottomBgColor = in.readInt(); + this.pictureNavBarColor = in.readInt(); + this.pictureOriginalFontColor = in.readInt(); + this.pictureOriginalTextSize = in.readInt(); + this.pictureUnCompleteBackgroundStyle = in.readInt(); + this.pictureCompleteBackgroundStyle = in.readInt(); + this.pictureTitleUpResId = in.readInt(); + this.pictureTitleDownResId = in.readInt(); + this.pictureLeftBackIcon = in.readInt(); + this.pictureCheckedStyle = in.readInt(); + this.isCompleteReplaceNum = in.readByte() != 0; + this.pictureWeChatChooseStyle = in.readInt(); + this.pictureWeChatLeftBackStyle = in.readInt(); + this.pictureWeChatTitleBackgroundStyle = in.readInt(); + this.pictureWeChatPreviewSelectedTextSize = in.readInt(); + this.pictureWeChatPreviewSelectedText = in.readString(); + this.pictureCheckNumBgStyle = in.readInt(); + this.pictureFolderCheckedDotStyle = in.readInt(); + this.pictureExternalPreviewDeleteStyle = in.readInt(); + this.pictureOriginalControlStyle = in.readInt(); + this.pictureExternalPreviewGonePreviewDelete = in.readByte() != 0; + this.pictureAlbumStyle = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PictureParameterStyle createFromParcel(Parcel source) { + return new PictureParameterStyle(source); + } + + @Override + public PictureParameterStyle[] newArray(int size) { + return new PictureParameterStyle[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/style/PictureWindowAnimationStyle.java b/picture_library/src/main/java/com/luck/picture/lib/style/PictureWindowAnimationStyle.java new file mode 100644 index 0000000..4478f2f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/style/PictureWindowAnimationStyle.java @@ -0,0 +1,126 @@ +package com.luck.picture.lib.style; + +import android.os.Parcel; +import android.os.Parcelable; + +import androidx.annotation.AnimRes; + +/** + * @author:luck + * @date:2019-11-25 18:17 + * @describe:PictureSelector Activity动画管理Style + */ +public class PictureWindowAnimationStyle implements Parcelable { + /** + * 相册启动动画 + */ + @AnimRes + public int activityEnterAnimation; + + /** + * 相册退出动画 + */ + @AnimRes + public int activityExitAnimation; + + /** + * 预览界面启动动画 + */ + @AnimRes + public int activityPreviewEnterAnimation; + + /** + * 预览界面退出动画 + */ + @AnimRes + public int activityPreviewExitAnimation; + + /** + * 裁剪界面启动动画 + */ + @AnimRes + public int activityCropEnterAnimation; + + /** + * 裁剪界面退出动画 + */ + @AnimRes + public int activityCropExitAnimation; + + + public PictureWindowAnimationStyle() { + super(); + } + + public PictureWindowAnimationStyle(@AnimRes int activityEnterAnimation, + @AnimRes int activityExitAnimation) { + super(); + this.activityEnterAnimation = activityEnterAnimation; + this.activityExitAnimation = activityExitAnimation; + } + + public PictureWindowAnimationStyle(@AnimRes int activityEnterAnimation, + @AnimRes int activityExitAnimation, + @AnimRes int activityPreviewEnterAnimation, + @AnimRes int activityPreviewExitAnimation) { + super(); + this.activityEnterAnimation = activityEnterAnimation; + this.activityExitAnimation = activityExitAnimation; + this.activityPreviewEnterAnimation = activityPreviewEnterAnimation; + this.activityPreviewExitAnimation = activityPreviewExitAnimation; + } + + /** + * 全局所有动画样式 + * + * @param enterAnimation + * @param exitAnimation + */ + public void ofAllAnimation(int enterAnimation, int exitAnimation) { + this.activityEnterAnimation = enterAnimation; + this.activityExitAnimation = exitAnimation; + + this.activityPreviewEnterAnimation = enterAnimation; + this.activityPreviewExitAnimation = exitAnimation; + + this.activityCropEnterAnimation = enterAnimation; + this.activityCropExitAnimation = exitAnimation; + + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(this.activityEnterAnimation); + dest.writeInt(this.activityExitAnimation); + dest.writeInt(this.activityPreviewEnterAnimation); + dest.writeInt(this.activityPreviewExitAnimation); + dest.writeInt(this.activityCropEnterAnimation); + dest.writeInt(this.activityCropExitAnimation); + } + + protected PictureWindowAnimationStyle(Parcel in) { + this.activityEnterAnimation = in.readInt(); + this.activityExitAnimation = in.readInt(); + this.activityPreviewEnterAnimation = in.readInt(); + this.activityPreviewExitAnimation = in.readInt(); + this.activityCropEnterAnimation = in.readInt(); + this.activityCropExitAnimation = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public PictureWindowAnimationStyle createFromParcel(Parcel source) { + return new PictureWindowAnimationStyle(source); + } + + @Override + public PictureWindowAnimationStyle[] newArray(int size) { + return new PictureWindowAnimationStyle[size]; + } + }; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/thread/PictureThreadUtils.java b/picture_library/src/main/java/com/luck/picture/lib/thread/PictureThreadUtils.java new file mode 100644 index 0000000..90cdaf8 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/thread/PictureThreadUtils.java @@ -0,0 +1,1335 @@ +package com.luck.picture.lib.thread; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; + +import androidx.annotation.CallSuper; +import androidx.annotation.IntRange; +import androidx.annotation.NonNull; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +/** + *

+ *     author: Blankj
+ *     blog  : http://blankj.com
+ *     time  : 2018/05/08
+ *     desc  : utils about thread
+ * 
+ */ +public final class PictureThreadUtils { + + private static final Map> TYPE_PRIORITY_POOLS = new HashMap<>(); + + private static final Map TASK_TASKINFO_MAP = new ConcurrentHashMap<>(); + + private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + private static final Timer TIMER = new Timer(); + + private static final byte TYPE_SINGLE = -1; + private static final byte TYPE_CACHED = -2; + private static final byte TYPE_IO = -4; + private static final byte TYPE_CPU = -8; + + private static Executor sDeliver; + + /** + * Return whether the thread is the main thread. + * + * @return {@code true}: yes
{@code false}: no + */ + public static boolean isMainThread() { + return Looper.myLooper() == Looper.getMainLooper(); + } + + /** + * Return a thread pool that reuses a fixed number of threads + * operating off a shared unbounded queue, using the provided + * ThreadFactory to create new threads when needed. + * + * @param size The size of thread in the pool. + * @return a fixed thread pool + */ + public static ExecutorService getFixedPool(@IntRange(from = 1) final int size) { + return getPoolByTypeAndPriority(size); + } + + /** + * Return a thread pool that reuses a fixed number of threads + * operating off a shared unbounded queue, using the provided + * ThreadFactory to create new threads when needed. + * + * @param size The size of thread in the pool. + * @param priority The priority of thread in the poll. + * @return a fixed thread pool + */ + public static ExecutorService getFixedPool(@IntRange(from = 1) final int size, + @IntRange(from = 1, to = 10) final int priority) { + return getPoolByTypeAndPriority(size, priority); + } + + /** + * Return a thread pool that uses a single worker thread operating + * off an unbounded queue, and uses the provided ThreadFactory to + * create a new thread when needed. + * + * @return a single thread pool + */ + public static ExecutorService getSinglePool() { + return getPoolByTypeAndPriority(TYPE_SINGLE); + } + + /** + * Return a thread pool that uses a single worker thread operating + * off an unbounded queue, and uses the provided ThreadFactory to + * create a new thread when needed. + * + * @param priority The priority of thread in the poll. + * @return a single thread pool + */ + public static ExecutorService getSinglePool(@IntRange(from = 1, to = 10) final int priority) { + return getPoolByTypeAndPriority(TYPE_SINGLE, priority); + } + + /** + * Return a thread pool that creates new threads as needed, but + * will reuse previously constructed threads when they are + * available. + * + * @return a cached thread pool + */ + public static ExecutorService getCachedPool() { + return getPoolByTypeAndPriority(TYPE_CACHED); + } + + /** + * Return a thread pool that creates new threads as needed, but + * will reuse previously constructed threads when they are + * available. + * + * @param priority The priority of thread in the poll. + * @return a cached thread pool + */ + public static ExecutorService getCachedPool(@IntRange(from = 1, to = 10) final int priority) { + return getPoolByTypeAndPriority(TYPE_CACHED, priority); + } + + /** + * Return a thread pool that creates (2 * CPU_COUNT + 1) threads + * operating off a queue which size is 128. + * + * @return a IO thread pool + */ + public static ExecutorService getIoPool() { + return getPoolByTypeAndPriority(TYPE_IO); + } + + /** + * Return a thread pool that creates (2 * CPU_COUNT + 1) threads + * operating off a queue which size is 128. + * + * @param priority The priority of thread in the poll. + * @return a IO thread pool + */ + public static ExecutorService getIoPool(@IntRange(from = 1, to = 10) final int priority) { + return getPoolByTypeAndPriority(TYPE_IO, priority); + } + + /** + * Return a thread pool that creates (CPU_COUNT + 1) threads + * operating off a queue which size is 128 and the maximum + * number of threads equals (2 * CPU_COUNT + 1). + * + * @return a cpu thread pool for + */ + public static ExecutorService getCpuPool() { + return getPoolByTypeAndPriority(TYPE_CPU); + } + + /** + * Return a thread pool that creates (CPU_COUNT + 1) threads + * operating off a queue which size is 128 and the maximum + * number of threads equals (2 * CPU_COUNT + 1). + * + * @param priority The priority of thread in the poll. + * @return a cpu thread pool for + */ + public static ExecutorService getCpuPool(@IntRange(from = 1, to = 10) final int priority) { + return getPoolByTypeAndPriority(TYPE_CPU, priority); + } + + /** + * Executes the given task in a fixed thread pool. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeByFixed(@IntRange(from = 1) final int size, final Task task) { + execute(getPoolByTypeAndPriority(size), task); + } + + /** + * Executes the given task in a fixed thread pool. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByFixed(@IntRange(from = 1) final int size, + final Task task, + @IntRange(from = 1, to = 10) final int priority) { + execute(getPoolByTypeAndPriority(size, priority), task); + } + + /** + * Executes the given task in a fixed thread pool after the given delay. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeByFixedWithDelay(@IntRange(from = 1) final int size, + final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(getPoolByTypeAndPriority(size), task, delay, unit); + } + + /** + * Executes the given task in a fixed thread pool after the given delay. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByFixedWithDelay(@IntRange(from = 1) final int size, + final Task task, + final long delay, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeWithDelay(getPoolByTypeAndPriority(size, priority), task, delay, unit); + } + + /** + * Executes the given task in a fixed thread pool at fix rate. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeByFixedAtFixRate(@IntRange(from = 1) final int size, + final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(size), task, 0, period, unit); + } + + /** + * Executes the given task in a fixed thread pool at fix rate. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByFixedAtFixRate(@IntRange(from = 1) final int size, + final Task task, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(size, priority), task, 0, period, unit); + } + + /** + * Executes the given task in a fixed thread pool at fix rate. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeByFixedAtFixRate(@IntRange(from = 1) final int size, + final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(size), task, initialDelay, period, unit); + } + + /** + * Executes the given task in a fixed thread pool at fix rate. + * + * @param size The size of thread in the fixed thread pool. + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByFixedAtFixRate(@IntRange(from = 1) final int size, + final Task task, + long initialDelay, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(size, priority), task, initialDelay, period, unit); + } + + /** + * Executes the given task in a single thread pool. + * + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeBySingle(final Task task) { + execute(getPoolByTypeAndPriority(TYPE_SINGLE), task); + } + + /** + * Executes the given task in a single thread pool. + * + * @param task The task to execute. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeBySingle(final Task task, + @IntRange(from = 1, to = 10) final int priority) { + execute(getPoolByTypeAndPriority(TYPE_SINGLE, priority), task); + } + + /** + * Executes the given task in a single thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeBySingleWithDelay(final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_SINGLE), task, delay, unit); + } + + /** + * Executes the given task in a single thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeBySingleWithDelay(final Task task, + final long delay, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_SINGLE, priority), task, delay, unit); + } + + /** + * Executes the given task in a single thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeBySingleAtFixRate(final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_SINGLE), task, 0, period, unit); + } + + /** + * Executes the given task in a single thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeBySingleAtFixRate(final Task task, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_SINGLE, priority), task, 0, period, unit); + } + + /** + * Executes the given task in a single thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeBySingleAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_SINGLE), task, initialDelay, period, unit); + } + + /** + * Executes the given task in a single thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeBySingleAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate( + getPoolByTypeAndPriority(TYPE_SINGLE, priority), task, initialDelay, period, unit + ); + } + + /** + * Executes the given task in a cached thread pool. + * + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeByCached(final Task task) { + execute(getPoolByTypeAndPriority(TYPE_CACHED), task); + } + + /** + * Executes the given task in a cached thread pool. + * + * @param task The task to execute. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCached(final Task task, + @IntRange(from = 1, to = 10) final int priority) { + execute(getPoolByTypeAndPriority(TYPE_CACHED, priority), task); + } + + /** + * Executes the given task in a cached thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeByCachedWithDelay(final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_CACHED), task, delay, unit); + } + + /** + * Executes the given task in a cached thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCachedWithDelay(final Task task, + final long delay, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_CACHED, priority), task, delay, unit); + } + + /** + * Executes the given task in a cached thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeByCachedAtFixRate(final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CACHED), task, 0, period, unit); + } + + /** + * Executes the given task in a cached thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCachedAtFixRate(final Task task, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CACHED, priority), task, 0, period, unit); + } + + /** + * Executes the given task in a cached thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeByCachedAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CACHED), task, initialDelay, period, unit); + } + + /** + * Executes the given task in a cached thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCachedAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate( + getPoolByTypeAndPriority(TYPE_CACHED, priority), task, initialDelay, period, unit + ); + } + + /** + * Executes the given task in an IO thread pool. + * + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeByIo(final Task task) { + execute(getPoolByTypeAndPriority(TYPE_IO), task); + } + + /** + * Executes the given task in an IO thread pool. + * + * @param task The task to execute. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByIo(final Task task, + @IntRange(from = 1, to = 10) final int priority) { + execute(getPoolByTypeAndPriority(TYPE_IO, priority), task); + } + + /** + * Executes the given task in an IO thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeByIoWithDelay(final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_IO), task, delay, unit); + } + + /** + * Executes the given task in an IO thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByIoWithDelay(final Task task, + final long delay, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_IO, priority), task, delay, unit); + } + + /** + * Executes the given task in an IO thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeByIoAtFixRate(final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_IO), task, 0, period, unit); + } + + /** + * Executes the given task in an IO thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByIoAtFixRate(final Task task, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_IO, priority), task, 0, period, unit); + } + + /** + * Executes the given task in an IO thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeByIoAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_IO), task, initialDelay, period, unit); + } + + /** + * Executes the given task in an IO thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByIoAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate( + getPoolByTypeAndPriority(TYPE_IO, priority), task, initialDelay, period, unit + ); + } + + /** + * Executes the given task in a cpu thread pool. + * + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeByCpu(final Task task) { + execute(getPoolByTypeAndPriority(TYPE_CPU), task); + } + + /** + * Executes the given task in a cpu thread pool. + * + * @param task The task to execute. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCpu(final Task task, + @IntRange(from = 1, to = 10) final int priority) { + execute(getPoolByTypeAndPriority(TYPE_CPU, priority), task); + } + + /** + * Executes the given task in a cpu thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeByCpuWithDelay(final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_CPU), task, delay, unit); + } + + /** + * Executes the given task in a cpu thread pool after the given delay. + * + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCpuWithDelay(final Task task, + final long delay, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeWithDelay(getPoolByTypeAndPriority(TYPE_CPU, priority), task, delay, unit); + } + + /** + * Executes the given task in a cpu thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeByCpuAtFixRate(final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CPU), task, 0, period, unit); + } + + /** + * Executes the given task in a cpu thread pool at fix rate. + * + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCpuAtFixRate(final Task task, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CPU, priority), task, 0, period, unit); + } + + /** + * Executes the given task in a cpu thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeByCpuAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(getPoolByTypeAndPriority(TYPE_CPU), task, initialDelay, period, unit); + } + + /** + * Executes the given task in a cpu thread pool at fix rate. + * + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param priority The priority of thread in the poll. + * @param The type of the task's result. + */ + public static void executeByCpuAtFixRate(final Task task, + long initialDelay, + final long period, + final TimeUnit unit, + @IntRange(from = 1, to = 10) final int priority) { + executeAtFixedRate( + getPoolByTypeAndPriority(TYPE_CPU, priority), task, initialDelay, period, unit + ); + } + + /** + * Executes the given task in a custom thread pool. + * + * @param pool The custom thread pool. + * @param task The task to execute. + * @param The type of the task's result. + */ + public static void executeByCustom(final ExecutorService pool, final Task task) { + execute(pool, task); + } + + /** + * Executes the given task in a custom thread pool after the given delay. + * + * @param pool The custom thread pool. + * @param task The task to execute. + * @param delay The time from now to delay execution. + * @param unit The time unit of the delay parameter. + * @param The type of the task's result. + */ + public static void executeByCustomWithDelay(final ExecutorService pool, + final Task task, + final long delay, + final TimeUnit unit) { + executeWithDelay(pool, task, delay, unit); + } + + /** + * Executes the given task in a custom thread pool at fix rate. + * + * @param pool The custom thread pool. + * @param task The task to execute. + * @param period The period between successive executions. + * @param unit The time unit of the period parameter. + * @param The type of the task's result. + */ + public static void executeByCustomAtFixRate(final ExecutorService pool, + final Task task, + final long period, + final TimeUnit unit) { + executeAtFixedRate(pool, task, 0, period, unit); + } + + /** + * Executes the given task in a custom thread pool at fix rate. + * + * @param pool The custom thread pool. + * @param task The task to execute. + * @param initialDelay The time to delay first execution. + * @param period The period between successive executions. + * @param unit The time unit of the initialDelay and period parameters. + * @param The type of the task's result. + */ + public static void executeByCustomAtFixRate(final ExecutorService pool, + final Task task, + long initialDelay, + final long period, + final TimeUnit unit) { + executeAtFixedRate(pool, task, initialDelay, period, unit); + } + + /** + * Cancel the given task. + * + * @param task The task to cancel. + */ + public static void cancel(final Task task) { + if (task == null) return; + task.cancel(); + } + + /** + * Cancel the given tasks. + * + * @param tasks The tasks to cancel. + */ + public static void cancel(final Task... tasks) { + if (tasks == null || tasks.length == 0) return; + for (Task task : tasks) { + if (task == null) continue; + task.cancel(); + } + } + + /** + * Cancel the given tasks. + * + * @param tasks The tasks to cancel. + */ + public static void cancel(final List tasks) { + if (tasks == null || tasks.size() == 0) return; + for (Task task : tasks) { + if (task == null) continue; + task.cancel(); + } + } + + /** + * Cancel the tasks in pool. + * + * @param executorService The pool. + */ + public static void cancel(ExecutorService executorService) { + if (executorService instanceof ThreadPoolExecutor4Util) { + for (Map.Entry taskTaskInfoEntry : TASK_TASKINFO_MAP.entrySet()) { + if (taskTaskInfoEntry.getValue().mService == executorService) { + cancel(taskTaskInfoEntry.getKey()); + } + } + } else { + Log.e("LogUtils", "The executorService is not PictureThreadUtils's pool."); + } + } + + /** + * Set the deliver. + * + * @param deliver The deliver. + */ + public static void setDeliver(final Executor deliver) { + sDeliver = deliver; + } + + private static void execute(final ExecutorService pool, final Task task) { + execute(pool, task, 0, 0, null); + } + + private static void executeWithDelay(final ExecutorService pool, + final Task task, + final long delay, + final TimeUnit unit) { + execute(pool, task, delay, 0, unit); + } + + private static void executeAtFixedRate(final ExecutorService pool, + final Task task, + long delay, + final long period, + final TimeUnit unit) { + execute(pool, task, delay, period, unit); + } + + private static void execute(final ExecutorService pool, final Task task, + long delay, final long period, final TimeUnit unit) { + TaskInfo taskInfo; + synchronized (TASK_TASKINFO_MAP) { + if (TASK_TASKINFO_MAP.get(task) != null) { + Log.e("PictureThreadUtils", "Task can only be executed once."); + return; + } + taskInfo = new TaskInfo(pool); + TASK_TASKINFO_MAP.put(task, taskInfo); + } + if (period == 0) { + if (delay == 0) { + pool.execute(task); + } else { + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + pool.execute(task); + } + }; + taskInfo.mTimerTask = timerTask; + TIMER.schedule(timerTask, unit.toMillis(delay)); + } + } else { + task.setSchedule(true); + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + pool.execute(task); + } + }; + taskInfo.mTimerTask = timerTask; + TIMER.scheduleAtFixedRate(timerTask, unit.toMillis(delay), unit.toMillis(period)); + } + } + + private static ExecutorService getPoolByTypeAndPriority(final int type) { + return getPoolByTypeAndPriority(type, Thread.NORM_PRIORITY); + } + + private static ExecutorService getPoolByTypeAndPriority(final int type, final int priority) { + synchronized (TYPE_PRIORITY_POOLS) { + ExecutorService pool; + Map priorityPools = TYPE_PRIORITY_POOLS.get(type); + if (priorityPools == null) { + priorityPools = new ConcurrentHashMap<>(); + pool = ThreadPoolExecutor4Util.createPool(type, priority); + priorityPools.put(priority, pool); + TYPE_PRIORITY_POOLS.put(type, priorityPools); + } else { + pool = priorityPools.get(priority); + if (pool == null) { + pool = ThreadPoolExecutor4Util.createPool(type, priority); + priorityPools.put(priority, pool); + } + } + return pool; + } + } + + static final class ThreadPoolExecutor4Util extends ThreadPoolExecutor { + + private static ExecutorService createPool(final int type, final int priority) { + switch (type) { + case TYPE_SINGLE: + return new ThreadPoolExecutor4Util(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue4Util(), + new UtilsThreadFactory("single", priority) + ); + case TYPE_CACHED: + return new ThreadPoolExecutor4Util(0, 128, + 60L, TimeUnit.SECONDS, + new LinkedBlockingQueue4Util(true), + new UtilsThreadFactory("cached", priority) + ); + case TYPE_IO: + return new ThreadPoolExecutor4Util(2 * CPU_COUNT + 1, 2 * CPU_COUNT + 1, + 30, TimeUnit.SECONDS, + new LinkedBlockingQueue4Util(), + new UtilsThreadFactory("io", priority) + ); + case TYPE_CPU: + return new ThreadPoolExecutor4Util(CPU_COUNT + 1, 2 * CPU_COUNT + 1, + 30, TimeUnit.SECONDS, + new LinkedBlockingQueue4Util(true), + new UtilsThreadFactory("cpu", priority) + ); + default: + return new ThreadPoolExecutor4Util(type, type, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue4Util(), + new UtilsThreadFactory("fixed(" + type + ")", priority) + ); + } + } + + private final AtomicInteger mSubmittedCount = new AtomicInteger(); + + private LinkedBlockingQueue4Util mWorkQueue; + + ThreadPoolExecutor4Util(int corePoolSize, int maximumPoolSize, + long keepAliveTime, TimeUnit unit, + LinkedBlockingQueue4Util workQueue, + ThreadFactory threadFactory) { + super(corePoolSize, maximumPoolSize, + keepAliveTime, unit, + workQueue, + threadFactory + ); + workQueue.mPool = this; + mWorkQueue = workQueue; + } + + private int getSubmittedCount() { + return mSubmittedCount.get(); + } + + @Override + protected void afterExecute(Runnable r, Throwable t) { + mSubmittedCount.decrementAndGet(); + super.afterExecute(r, t); + } + + @Override + public void execute(@NonNull Runnable command) { + if (this.isShutdown()) return; + mSubmittedCount.incrementAndGet(); + try { + super.execute(command); + } catch (RejectedExecutionException ignore) { + Log.e("PictureThreadUtils", "This will not happen!"); + mWorkQueue.offer(command); + } catch (Throwable t) { + mSubmittedCount.decrementAndGet(); + } + } + } + + private static final class LinkedBlockingQueue4Util extends LinkedBlockingQueue { + + private volatile ThreadPoolExecutor4Util mPool; + + private int mCapacity = Integer.MAX_VALUE; + + LinkedBlockingQueue4Util() { + super(); + } + + LinkedBlockingQueue4Util(boolean isAddSubThreadFirstThenAddQueue) { + super(); + if (isAddSubThreadFirstThenAddQueue) { + mCapacity = 0; + } + } + + LinkedBlockingQueue4Util(int capacity) { + super(); + mCapacity = capacity; + } + + @Override + public boolean offer(@NonNull Runnable runnable) { + if (mCapacity <= size() && + mPool != null && mPool.getPoolSize() < mPool.getMaximumPoolSize()) { + // create a non-core thread + return false; + } + return super.offer(runnable); + } + } + + private static final class UtilsThreadFactory extends AtomicLong + implements ThreadFactory { + private static final AtomicInteger POOL_NUMBER = new AtomicInteger(1); + private static final long serialVersionUID = -9209200509960368598L; + private final String namePrefix; + private final int priority; + private final boolean isDaemon; + + UtilsThreadFactory(String prefix, int priority) { + this(prefix, priority, false); + } + + UtilsThreadFactory(String prefix, int priority, boolean isDaemon) { + namePrefix = prefix + "-pool-" + + POOL_NUMBER.getAndIncrement() + + "-thread-"; + this.priority = priority; + this.isDaemon = isDaemon; + } + + @Override + public Thread newThread(@NonNull Runnable r) { + Thread t = new Thread(r, namePrefix + getAndIncrement()) { + @Override + public void run() { + try { + super.run(); + } catch (Throwable t) { + Log.e("PictureThreadUtils", "Request threw uncaught throwable", t); + } + } + }; + t.setDaemon(isDaemon); + t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() { + @Override + public void uncaughtException(Thread t, Throwable e) { + System.out.println(e); + } + }); + t.setPriority(priority); + return t; + } + } + + public abstract static class SimpleTask extends Task { + + @Override + public void onCancel() { + Log.e("PictureThreadUtils", "onCancel: " + Thread.currentThread()); + } + + @Override + public void onFail(Throwable t) { + Log.e("PictureThreadUtils", "onFail: ", t); + } + + } + + public abstract static class Task implements Runnable { + + private static final int NEW = 0; + private static final int RUNNING = 1; + private static final int EXCEPTIONAL = 2; + private static final int COMPLETING = 3; + private static final int CANCELLED = 4; + private static final int INTERRUPTED = 5; + private static final int TIMEOUT = 6; + + private final AtomicInteger state = new AtomicInteger(NEW); + + private volatile boolean isSchedule; + private volatile Thread runner; + + private Timer mTimer; + + private Executor deliver; + + public abstract T doInBackground() throws Throwable; + + public abstract void onSuccess(T result); + + public abstract void onCancel(); + + public abstract void onFail(Throwable t); + + @Override + public void run() { + if (isSchedule) { + if (runner == null) { + if (!state.compareAndSet(NEW, RUNNING)) return; + runner = Thread.currentThread(); + } else { + if (state.get() != RUNNING) return; + } + } else { + if (!state.compareAndSet(NEW, RUNNING)) return; + runner = Thread.currentThread(); + } + try { + final T result = doInBackground(); + if (isSchedule) { + if (state.get() != RUNNING) return; + getDeliver().execute(new Runnable() { + @Override + public void run() { + onSuccess(result); + } + }); + } else { + if (!state.compareAndSet(RUNNING, COMPLETING)) return; + getDeliver().execute(new Runnable() { + @Override + public void run() { + onSuccess(result); + onDone(); + } + }); + } + } catch (InterruptedException ignore) { + state.compareAndSet(CANCELLED, INTERRUPTED); + } catch (final Throwable throwable) { + if (!state.compareAndSet(RUNNING, EXCEPTIONAL)) return; + getDeliver().execute(new Runnable() { + @Override + public void run() { + onFail(throwable); + onDone(); + } + }); + } + } + + public void cancel() { + cancel(true); + } + + public void cancel(boolean mayInterruptIfRunning) { + synchronized (state) { + if (state.get() > RUNNING) return; + state.set(CANCELLED); + } + if (mayInterruptIfRunning) { + if (runner != null) { + runner.interrupt(); + } + } + + getDeliver().execute(new Runnable() { + @Override + public void run() { + onCancel(); + onDone(); + } + }); + } + + private void timeout() { + synchronized (state) { + if (state.get() > RUNNING) return; + state.set(TIMEOUT); + } + if (runner != null) { + runner.interrupt(); + } + onDone(); + } + + + public boolean isCanceled() { + return state.get() >= CANCELLED; + } + + public boolean isDone() { + return state.get() > RUNNING; + } + + public Task setDeliver(Executor deliver) { + this.deliver = deliver; + return this; + } + + public void setTimeout(final long timeoutMillis, final OnTimeoutListener listener) { + mTimer = new Timer(); + mTimer.schedule(new TimerTask() { + @Override + public void run() { + if (!isDone() && listener != null) { + timeout(); + listener.onTimeout(); + } + } + }, timeoutMillis); + } + + private void setSchedule(boolean isSchedule) { + this.isSchedule = isSchedule; + } + + private Executor getDeliver() { + if (deliver == null) { + return getGlobalDeliver(); + } + return deliver; + } + + @CallSuper + protected void onDone() { + TASK_TASKINFO_MAP.remove(this); + if (mTimer != null) { + mTimer.cancel(); + mTimer = null; + } + } + + public interface OnTimeoutListener { + void onTimeout(); + } + } + + private static Executor getGlobalDeliver() { + if (sDeliver == null) { + sDeliver = new Executor() { + private final Handler mHandler = new Handler(Looper.getMainLooper()); + + @Override + public void execute(@NonNull Runnable command) { + mHandler.post(command); + } + }; + } + return sDeliver; + } + + private static class TaskInfo { + private TimerTask mTimerTask; + private ExecutorService mService; + + private TaskInfo(ExecutorService service) { + mService = service; + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/AndroidQTransformUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/AndroidQTransformUtils.java new file mode 100644 index 0000000..2bee57a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/AndroidQTransformUtils.java @@ -0,0 +1,85 @@ +package com.luck.picture.lib.tools; + +import android.content.Context; +import android.net.Uri; +import android.text.TextUtils; + +import com.luck.picture.lib.config.PictureSelectionConfig; + +import java.io.File; +import java.io.OutputStream; +import java.util.Objects; + +import okio.BufferedSource; +import okio.Okio; + +/** + * @author:luck + * @date:2019-11-08 19:25 + * @describe:Android Q相关处理类 + */ +public class AndroidQTransformUtils { + + + /** + * 解析Android Q版本下图片 + * #耗时操作需要放在子线程中操作 + * + * @param ctx + * @param uri + * @param mineType + * @param customFileName + * @return + */ + public static String copyPathToAndroidQ(Context ctx, String url, int width, int height, String mineType, String customFileName) { + // 这里就是利用图片加载引擎的特性,因为图片加载器加载过了图片本地就有缓存,当然前提是用户设置了缓存策略 + if (PictureSelectionConfig.cacheResourcesEngine != null) { + String cachePath = PictureSelectionConfig.cacheResourcesEngine.onCachePath(ctx, url); + if (!TextUtils.isEmpty(cachePath)) { + return cachePath; + } + } + + // 走普通的文件复制流程,拷贝至应用沙盒内来 + BufferedSource inBuffer = null; + try { + Uri uri = Uri.parse(url); + String encryptionValue = StringUtils.getEncryptionValue(url, width, height); + String newPath = PictureFileUtils.createFilePath(ctx, encryptionValue, mineType, customFileName); + File outFile = new File(newPath); + if (outFile.exists()) { + return newPath; + } + inBuffer = Okio.buffer(Okio.source(Objects.requireNonNull(ctx.getContentResolver().openInputStream(uri)))); + boolean copyFileSuccess = PictureFileUtils.bufferCopy(inBuffer, outFile); + if (copyFileSuccess) { + return newPath; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (inBuffer != null && inBuffer.isOpen()) { + PictureFileUtils.close(inBuffer); + } + } + return null; + } + + /** + * 复制文件至AndroidQ手机相册目录 + * + * @param context + * @param inFile + * @param outUri + */ + public static boolean copyPathToDCIM(Context context, File inFile, Uri outUri) { + try { + OutputStream fileOutputStream = context.getContentResolver().openOutputStream(outUri); + return PictureFileUtils.bufferCopy(inFile, fileOutputStream); + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/AnimUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/AnimUtils.java new file mode 100644 index 0000000..96a50dd --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/AnimUtils.java @@ -0,0 +1,61 @@ +package com.luck.picture.lib.tools; + +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.view.View; +import android.view.animation.RotateAnimation; +import android.widget.ImageView; + +/** + * @author:luck + * @date:2019-11-21 19:20 + * @describe:动画相关 + */ +public class AnimUtils { + private final static int DURATION = 450; + + public static void zoom(View view, boolean isZoomAnim) { + if (isZoomAnim) { + AnimatorSet set = new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofFloat(view, "scaleX", 1f, 1.12f), + ObjectAnimator.ofFloat(view, "scaleY", 1f, 1.12f) + ); + set.setDuration(DURATION); + set.start(); + } + } + + public static void disZoom(View view, boolean isZoomAnim) { + if (isZoomAnim) { + AnimatorSet set = new AnimatorSet(); + set.playTogether( + ObjectAnimator.ofFloat(view, "scaleX", 1.12f, 1f), + ObjectAnimator.ofFloat(view, "scaleY", 1.12f, 1f) + ); + set.setDuration(DURATION); + set.start(); + } + } + + /** + * 箭头旋转动画 + * + * @param arrow + * @param flag + */ + public static void rotateArrow(ImageView arrow, boolean flag) { + float pivotX = arrow.getWidth() / 2f; + float pivotY = arrow.getHeight() / 2f; + // flag为true则向上 + float fromDegrees = flag ? 180f : 180f; + float toDegrees = flag ? 360f : 360f; + //旋转动画效果 参数值 旋转的开始角度 旋转的结束角度 pivotX x轴伸缩值 + RotateAnimation animation = new RotateAnimation(fromDegrees, toDegrees, + pivotX, pivotY); + //该方法用于设置动画的持续时间,以毫秒为单位 + animation.setDuration(350); + //启动动画 + arrow.startAnimation(animation); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/AttrsUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/AttrsUtils.java new file mode 100644 index 0000000..bacf084 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/AttrsUtils.java @@ -0,0 +1,78 @@ +package com.luck.picture.lib.tools; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.drawable.Drawable; +import android.util.TypedValue; + +/** + * @author:luck + * @data:2018/3/28 下午1:00 + * @描述: 动态获取attrs + */ + +public class AttrsUtils { + + /** + * get attrs color + * + * @param context + * @param attr + * @return + */ + public static int getTypeValueColor(Context context, int attr) { + try { + TypedValue typedValue = new TypedValue(); + int[] attribute = new int[]{attr}; + TypedArray array = context.obtainStyledAttributes(typedValue.resourceId, attribute); + int color = array.getColor(0, 0); + array.recycle(); + return color; + } catch (Exception e) { + e.printStackTrace(); + } + return 0; + } + + /** + * attrs status color or black + * + * @param context + * @param attr + * @return + */ + public static boolean getTypeValueBoolean(Context context, int attr) { + try { + TypedValue typedValue = new TypedValue(); + int[] attribute = new int[]{attr}; + TypedArray array = context.obtainStyledAttributes(typedValue.resourceId, attribute); + boolean statusFont = array.getBoolean(0, false); + array.recycle(); + return statusFont; + } catch (Exception e) { + e.printStackTrace(); + } + return false; + } + + /** + * attrs drawable + * + * @param context + * @param attr + * @return + */ + public static Drawable getTypeValueDrawable(Context context, int attr) { + try { + TypedValue typedValue = new TypedValue(); + int[] attribute = new int[]{attr}; + TypedArray array = context.obtainStyledAttributes(typedValue.resourceId, attribute); + Drawable drawable = array.getDrawable(0); + array.recycle(); + return drawable; + } catch (Exception e) { + e.printStackTrace(); + } + return null; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/BitmapUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/BitmapUtils.java new file mode 100644 index 0000000..b69097c --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/BitmapUtils.java @@ -0,0 +1,92 @@ +package com.luck.picture.lib.tools; + +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Matrix; + +import java.io.BufferedOutputStream; +import java.io.File; +import java.io.FileOutputStream; + +/** + * @author:luck + * @date:2020-01-15 18:22 + * @describe:BitmapUtils + */ +public class BitmapUtils { + /** + * 旋转Bitmap + * + * @param bitmap + * @param angle + * @return + */ + public static Bitmap rotatingImage(Bitmap bitmap, int angle) { + Matrix matrix = new Matrix(); + + matrix.postRotate(angle); + return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); + } + + /** + * 判断拍照 图片是否旋转 + * + * @param degree + * @param file + */ + public static void rotateImage(int degree, String path) { + if (degree > 0) { + try { + // 针对相片有旋转问题的处理方式 + BitmapFactory.Options opts = new BitmapFactory.Options(); + opts.inSampleSize = 2; + File file = new File(path); + Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), opts); + bitmap = rotatingImage(bitmap, degree); + if (bitmap != null) { + saveBitmapFile(bitmap, file); + bitmap.recycle(); + } + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + /** + * 保存Bitmap至本地 + * + * @param bitmap + * @param file + */ + public static void saveBitmapFile(Bitmap bitmap, File file) { + try { + BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(file)); + bitmap.compress(Bitmap.CompressFormat.JPEG, 100, bos); + bos.flush(); + bos.close(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + /** + * 获取旋转角度 + * + * @param orientation + * @return + */ + public static int getRotationAngle(int orientation) { + switch (orientation) { + case 1: + return 0; + case 3: + return 180; + case 6: + return 90; + case 8: + return 270; + } + return 0; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/DateUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/DateUtils.java new file mode 100644 index 0000000..8c1b6a6 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/DateUtils.java @@ -0,0 +1,79 @@ +package com.luck.picture.lib.tools; + +import java.text.SimpleDateFormat; +import java.util.Locale; +import java.util.concurrent.TimeUnit; + +/** + * @author:luck + * @date:2017-5-25 23:30 + * @describe:DateUtils + */ + +public class DateUtils { + private static SimpleDateFormat sf = new SimpleDateFormat("yyyyMMdd_HHmmssSS"); + + /** + * 判断两个时间戳相差多少秒 + * + * @param d + * @return + */ + public static int dateDiffer(long d) { + try { + long l1 = ValueOf.toLong(String.valueOf(System.currentTimeMillis()).substring(0, 10)); + long interval = l1 - d; + return (int) Math.abs(interval); + } catch (Exception e) { + e.printStackTrace(); + return -1; + } + } + + /** + * 时间戳转换成时间格式 + * + * @param duration + * @return + */ + public static String formatDurationTime(long duration) { + return String.format(Locale.getDefault(), "%02d:%02d", + TimeUnit.MILLISECONDS.toMinutes(duration), + TimeUnit.MILLISECONDS.toSeconds(duration) + - TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(duration))); + } + + + /** + * 根据时间戳创建文件名 + * + * @param prefix 前缀名 + * @return + */ + public static String getCreateFileName(String prefix) { + long millis = System.currentTimeMillis(); + return prefix + sf.format(millis); + } + + /** + * 根据时间戳创建文件名 + * + * @return + */ + public static String getCreateFileName() { + long millis = System.currentTimeMillis(); + return sf.format(millis); + } + + /** + * 计算两个时间间隔 + * + * @param sTime + * @param eTime + * @return + */ + public static String cdTime(long sTime, long eTime) { + long diff = eTime - sTime; + return diff > 1000 ? diff / 1000 + "秒" : diff + "毫秒"; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/DoubleUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/DoubleUtils.java new file mode 100644 index 0000000..358014f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/DoubleUtils.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.tools; + +/** + * author:luck + * project:PictureSelector + * package:com.luck.picture.lib.tool + * email:893855882@qq.com + * data:2017/5/25 + */ + +public class DoubleUtils { + /** + * Prevent continuous click, jump two pages + */ + private static long lastClickTime; + private final static long TIME = 800; + + public static boolean isFastDoubleClick() { + long time = System.currentTimeMillis(); + if (time - lastClickTime < TIME) { + return true; + } + lastClickTime = time; + return false; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/JumpUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/JumpUtils.java new file mode 100644 index 0000000..0caecb1 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/JumpUtils.java @@ -0,0 +1,59 @@ +package com.luck.picture.lib.tools; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; + +import com.luck.picture.lib.PicturePreviewActivity; +import com.luck.picture.lib.PictureSelectorPreviewWeChatStyleActivity; +import com.luck.picture.lib.PictureVideoPlayActivity; + +/** + * @author:luck + * @date:2019-11-23 18:57 + * @describe:Activity跳转 + */ +public class JumpUtils { + /** + * 启动视频播放页面 + * + * @param context + * @param bundle + */ + public static void startPictureVideoPlayActivity(Context context, Bundle bundle, int requestCode) { + if (!DoubleUtils.isFastDoubleClick()) { + Intent intent = new Intent(); + intent.setClass(context, PictureVideoPlayActivity.class); + intent.putExtras(bundle); + if (!(context instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + ((Activity) context).startActivityForResult(intent, requestCode); + } + } + } + + /** + * 启动预览界面 + * + * @param context + * @param isWeChatStyle + * @param bundle + * @param requestCode + */ + public static void startPicturePreviewActivity(Context context, boolean isWeChatStyle, Bundle bundle, int requestCode) { + if (!DoubleUtils.isFastDoubleClick()) { + Intent intent = new Intent(); + intent.setClass(context, isWeChatStyle ? PictureSelectorPreviewWeChatStyleActivity.class : PicturePreviewActivity.class); + intent.putExtras(bundle); + if (!(context instanceof Activity)) { + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } else { + ((Activity) context).startActivityForResult(intent, requestCode); + } + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/MediaUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/MediaUtils.java new file mode 100644 index 0000000..19e1d38 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/MediaUtils.java @@ -0,0 +1,583 @@ +package com.luck.picture.lib.tools; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.media.MediaMetadataRetriever; +import android.net.Uri; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.MediaStore; +import android.text.TextUtils; + +import androidx.annotation.Nullable; + +import com.luck.picture.lib.config.PictureMimeType; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.listener.OnCallbackListener; +import com.luck.picture.lib.thread.PictureThreadUtils; + +import java.io.InputStream; + + +/** + * @author:luck + * @date:2019-10-21 17:10 + * @describe:资源处理工具类 + */ +public class MediaUtils { + /** + * 创建一条图片地址uri,用于保存拍照后的照片 + * + * @param context + * @param suffixType + * @return 图片的uri + */ + @Nullable + public static Uri createImageUri(final Context context, String suffixType) { + final Uri[] imageFilePath = {null}; + String status = Environment.getExternalStorageState(); + String time = ValueOf.toString(System.currentTimeMillis()); + // ContentValues是我们希望这条记录被创建时包含的数据信息 + ContentValues values = new ContentValues(3); + values.put(MediaStore.Images.Media.DISPLAY_NAME, DateUtils.getCreateFileName("IMG_")); + values.put(MediaStore.Images.Media.DATE_TAKEN, time); + values.put(MediaStore.Images.Media.MIME_TYPE, TextUtils.isEmpty(suffixType) ? PictureMimeType.MIME_TYPE_IMAGE : suffixType); + // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储 + if (status.equals(Environment.MEDIA_MOUNTED)) { + values.put(MediaStore.Images.Media.RELATIVE_PATH, PictureMimeType.DCIM); + imageFilePath[0] = context.getContentResolver() + .insert(MediaStore.Images.Media.getContentUri("external"), values); + } else { + imageFilePath[0] = context.getContentResolver() + .insert(MediaStore.Images.Media.getContentUri("internal"), values); + } + return imageFilePath[0]; + } + + + /** + * 创建一条视频地址uri,用于保存录制的视频 + * + * @param context + * @param suffixType + * @return 视频的uri + */ + @Nullable + public static Uri createVideoUri(final Context context, String suffixType) { + final Uri[] imageFilePath = {null}; + String status = Environment.getExternalStorageState(); + String time = ValueOf.toString(System.currentTimeMillis()); + // ContentValues是我们希望这条记录被创建时包含的数据信息 + ContentValues values = new ContentValues(3); + values.put(MediaStore.Video.Media.DISPLAY_NAME, DateUtils.getCreateFileName("VID_")); + values.put(MediaStore.Video.Media.DATE_TAKEN, time); + values.put(MediaStore.Video.Media.MIME_TYPE, TextUtils.isEmpty(suffixType) ? PictureMimeType.MIME_TYPE_VIDEO : suffixType); + // 判断是否有SD卡,优先使用SD卡存储,当没有SD卡时使用手机存储 + if (status.equals(Environment.MEDIA_MOUNTED)) { + values.put(MediaStore.Video.Media.RELATIVE_PATH, Environment.DIRECTORY_MOVIES); + imageFilePath[0] = context.getContentResolver() + .insert(MediaStore.Video.Media.getContentUri("external"), values); + } else { + imageFilePath[0] = context.getContentResolver() + .insert(MediaStore.Video.Media.getContentUri("internal"), values); + } + return imageFilePath[0]; + } + + /** + * 获取视频时长 + * + * @param context + * @param isAndroidQ + * @param path + * @return + */ + public static long extractDuration(Context context, boolean isAndroidQ, String path) { + return isAndroidQ ? getLocalDuration(context, Uri.parse(path)) + : getLocalDuration(path); + } + + /** + * 是否是长图 + * + * @param media + * @return true 是 or false 不是 + */ + public static boolean isLongImg(LocalMedia media) { + if (null != media) { + int width = media.getWidth(); + int height = media.getHeight(); + int newHeight = width * 3; + return height > newHeight; + } + return false; + } + + /** + * 是否是长图 + * + * @param width 宽 + * @param height 高 + * @return true 是 or false 不是 + */ + public static boolean isLongImg(int width, int height) { + int newHeight = width * 3; + return height > newHeight; + } + + /** + * get Local video duration + * + * @return + */ + private static long getLocalDuration(Context context, Uri uri) { + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(context, uri); + return Long.parseLong(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_DURATION)); + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * get Local video duration + * + * @return + */ + private static long getLocalDuration(String path) { + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(path); + return Long.parseLong(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_DURATION)); + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + + /** + * get Local image width or height for api 29 + * + * @return + */ + public static int[] getImageSizeForUrlToAndroidQ(Context context, String url) { + int[] size = new int[2]; + Cursor query = null; + try { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { + query = context.getApplicationContext().getContentResolver() + .query(Uri.parse(url), + null, null, null); + if (query != null) { + query.moveToFirst(); + size[0] = query.getInt(query.getColumnIndexOrThrow(MediaStore.Images + .Media.WIDTH)); + size[1] = query.getInt(query.getColumnIndexOrThrow(MediaStore.Images + .Media.HEIGHT)); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (query != null) { + query.close(); + } + } + return size; + } + + /** + * get Local video width or height + * + * @return + */ + public static int[] getVideoSizeForUrl(String url) { + int[] size = new int[2]; + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(url); + size[0] = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); + size[1] = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); + } catch (Exception e) { + e.printStackTrace(); + } + return size; + } + + /** + * get Local video width or height + * + * @return + */ + public static int[] getVideoSizeForUri(Context context, Uri uri) { + int[] size = new int[2]; + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(context, uri); + size[0] = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)); + size[1] = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)); + } catch (Exception e) { + e.printStackTrace(); + } + return size; + } + + /** + * get Local image width or height + * + * @return + */ + public static int[] getImageSizeForUrl(String url) { + int[] size = new int[2]; + try { + ExifInterface exifInterface = new ExifInterface(url); + // 获取图片的宽度 + int width = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_WIDTH, ExifInterface.ORIENTATION_NORMAL); + // 获取图片的高度 + int height = exifInterface.getAttributeInt(ExifInterface.TAG_IMAGE_LENGTH, ExifInterface.ORIENTATION_NORMAL); + size[0] = width; + size[1] = height; + } catch (Exception e) { + e.printStackTrace(); + } + return size; + } + + + /** + * get Local image width or height + * + * @return + */ + public static int[] getImageSizeForUri(Context context, Uri uri) { + int[] size = new int[2]; + ParcelFileDescriptor fileDescriptor = null; + try { + fileDescriptor = context.getContentResolver().openFileDescriptor(uri, "r"); + if (fileDescriptor != null) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeFileDescriptor(fileDescriptor.getFileDescriptor(), null, options); + size[0] = options.outWidth; + size[1] = options.outHeight; + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + PictureFileUtils.close(fileDescriptor); + } + return size; + } + + + /** + * 删除部分手机 拍照在DCIM也生成一张的问题 + * + * @param id + */ + public static void removeMedia(Context context, int id) { + try { + ContentResolver cr = context.getApplicationContext().getContentResolver(); + Uri uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + String selection = MediaStore.Images.Media._ID + "=?"; + cr.delete(uri, selection, new String[]{Long.toString(id)}); + } catch (Exception e) { + e.printStackTrace(); + } + } + + + /** + * 获取DCIM文件下最新一条拍照记录 + * + * @return + */ + public static int getDCIMLastImageId(Context context) { + Cursor data = null; + try { + //selection: 指定查询条件 + String absolutePath = PictureFileUtils.getDCIMCameraPath(); + String orderBy = MediaStore.Files.FileColumns._ID + " DESC limit 1 offset 0"; + String selection = MediaStore.Images.Media.DATA + " like ?"; + //定义selectionArgs: + String[] selectionArgs = {absolutePath + "%"}; + data = context.getApplicationContext().getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, null, + selection, selectionArgs, orderBy); + if (data != null && data.getCount() > 0 && data.moveToFirst()) { + int id = data.getInt(data.getColumnIndex(MediaStore.Images.Media._ID)); + long date = data.getLong(data.getColumnIndex(MediaStore.Images.Media.DATE_ADDED)); + int duration = DateUtils.dateDiffer(date); + // DCIM文件下最近时间1s以内的图片,可以判定是最新生成的重复照片 + return duration <= 1 ? id : -1; + } else { + return -1; + } + } catch (Exception e) { + e.printStackTrace(); + return -1; + } finally { + if (data != null) { + data.close(); + } + } + } + + /** + * 获取Camera文件下最新一条拍照记录 + * + * @return + */ + public static long getCameraFirstBucketId(Context context) { + Cursor data = null; + try { + String absolutePath = PictureFileUtils.getDCIMCameraPath(); + //selection: 指定查询条件 + String selection = MediaStore.Files.FileColumns.DATA + " like ?"; + //定义selectionArgs: + String[] selectionArgs = {absolutePath + "%"}; + String orderBy = MediaStore.Files.FileColumns._ID + " DESC limit 1 offset 0"; + data = context.getApplicationContext().getContentResolver().query(MediaStore.Files.getContentUri("external"), null, + selection, selectionArgs, orderBy); + if (data != null && data.getCount() > 0 && data.moveToFirst()) { + return data.getLong(data.getColumnIndex("bucket_id")); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (data != null) { + data.close(); + } + } + return -1; + } + + + /** + * 获取刚录取的音频文件 + * + * @param uri + * @return + */ + @Nullable + public static String getAudioFilePathFromUri(Context context, Uri uri) { + String path = ""; + Cursor cursor = null; + try { + cursor = context.getApplicationContext().getContentResolver() + .query(uri, null, null, null, null); + if (cursor != null) { + cursor.moveToFirst(); + int index = cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA); + path = cursor.getString(index); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return path; + } + + + /** + * 获取旋转角度 + * + * @param path + * @return + */ + public static int getVideoOrientationForUrl(String path) { + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(path); + int rotation = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); + switch (rotation) { + case 90: + return ExifInterface.ORIENTATION_ROTATE_90; + case 270: + return ExifInterface.ORIENTATION_ROTATE_270; + default: + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * 获取旋转角度 + * + * @param uri + * @return + */ + public static int getVideoOrientationForUri(Context context, Uri uri) { + try { + MediaMetadataRetriever mmr = new MediaMetadataRetriever(); + mmr.setDataSource(context, uri); + int orientation = ValueOf.toInt(mmr.extractMetadata + (MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)); + switch (orientation) { + case 90: + return ExifInterface.ORIENTATION_ROTATE_90; + case 270: + return ExifInterface.ORIENTATION_ROTATE_270; + default: + return 0; + } + } catch (Exception e) { + e.printStackTrace(); + return 0; + } + } + + /** + * 获取旋转角度 + * + * @param context + * @param url + * @return + */ + public static int getImageOrientationForUrl(Context context, String url) { + ExifInterface exifInterface = null; + InputStream inputStream = null; + try { + if (SdkVersionUtils.checkedAndroid_Q() && PictureMimeType.isContent(url)) { + inputStream = context.getContentResolver().openInputStream(Uri.parse(url)); + if (inputStream != null) { + exifInterface = new ExifInterface(inputStream); + } + } else { + exifInterface = new ExifInterface(url); + } + return exifInterface != null ? exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL) : 0; + } catch (Exception e) { + e.printStackTrace(); + return 0; + } finally { + PictureFileUtils.close(inputStream); + } + } + + /** + * 设置LocalMedia旋转信息 + * + * @param context + * @param media + * @param isAndroidQChangeWH + * @param listener + * @return + */ + public static void setOrientationAsynchronous(Context context, LocalMedia media, + boolean isAndroidQChangeWH, + boolean isAndroidQChangeVideoWH, + OnCallbackListener listener) { + if (PictureMimeType.isHasImage(media.getMimeType())) { + if (!isAndroidQChangeWH) { + return; + } + } + + if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (!isAndroidQChangeVideoWH) { + return; + } + } + + if (media.getOrientation() != -1) { + if (listener != null) { + listener.onCall(media); + } + return; + } + PictureThreadUtils.executeByIo(new PictureThreadUtils.SimpleTask() { + + @Override + public Integer doInBackground() { + int orientation = 0; + if (PictureMimeType.isHasImage(media.getMimeType())) { + orientation = MediaUtils.getImageOrientationForUrl(context, media.getPath()); + } else if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (PictureMimeType.isContent(media.getPath())) { + orientation = MediaUtils.getVideoOrientationForUri(context, Uri.parse(media.getPath())); + } else { + orientation = MediaUtils.getVideoOrientationForUrl(media.getPath()); + } + } + return orientation; + } + + @Override + public void onSuccess(Integer orientation) { + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 + || orientation == ExifInterface.ORIENTATION_ROTATE_270) { + int width = media.getWidth(); + int height = media.getHeight(); + media.setWidth(height); + media.setHeight(width); + } + media.setOrientation(orientation); + if (listener != null) { + listener.onCall(media); + } + } + }); + } + + /** + * 设置LocalMedia旋转信息 + * + * @param context + * @param media + * @param isAndroidQChangeWH + * @return + */ + public static void setOrientationSynchronous(Context context, LocalMedia media, + boolean isAndroidQChangeWH, + boolean isAndroidQChangeVideoWH) { + if (PictureMimeType.isHasImage(media.getMimeType())) { + if (!isAndroidQChangeWH) { + return; + } + } + if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (!isAndroidQChangeVideoWH) { + return; + } + } + // 如果有旋转信息图片宽高则是相反 + int orientation = 0; + if (PictureMimeType.isHasImage(media.getMimeType())) { + orientation = MediaUtils.getImageOrientationForUrl(context, media.getPath()); + } else if (PictureMimeType.isHasVideo(media.getMimeType())) { + if (PictureMimeType.isContent(media.getPath())) { + orientation = MediaUtils.getVideoOrientationForUri(context, Uri.parse(media.getPath())); + } else { + orientation = MediaUtils.getVideoOrientationForUrl(media.getPath()); + } + } + if (orientation == ExifInterface.ORIENTATION_ROTATE_90 + || orientation == ExifInterface.ORIENTATION_ROTATE_270) { + int width = media.getWidth(); + int height = media.getHeight(); + media.setWidth(height); + media.setHeight(width); + } + media.setOrientation(orientation); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/PictureFileUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/PictureFileUtils.java new file mode 100644 index 0000000..a309d2f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/PictureFileUtils.java @@ -0,0 +1,663 @@ +package com.luck.picture.lib.tools; + +import android.annotation.SuppressLint; +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.graphics.BitmapFactory; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.Build; +import android.os.Environment; +import android.os.ParcelFileDescriptor; +import android.provider.DocumentsContract; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.FileProvider; + +import com.luck.picture.lib.config.PictureConfig; +import com.luck.picture.lib.config.PictureMimeType; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.channels.FileChannel; +import java.util.Locale; + +import okio.BufferedSink; +import okio.BufferedSource; +import okio.Okio; + +/** + * @author:luck + * @date:2017-5-30 19:30 + * @describe:PictureFileUtils + */ + +public class PictureFileUtils { + + public static final String POSTFIX = ".jpg"; + public static final String POST_VIDEO = ".mp4"; + public static final String POST_AUDIO = ".mp3"; + + + /** + * @param context + * @param type + * @param format + * @param outCameraDirectory + * @return + */ + @Nullable + public static File createCameraFile(Context context, int type, String fileName, String format, String outCameraDirectory) { + return createMediaFile(context, type, fileName, format, outCameraDirectory); + } + + /** + * 创建文件 + * + * @param context + * @param type + * @param fileName + * @param format + * @param outCameraDirectory + * @return + */ + @Nullable + private static File createMediaFile(Context context, int chooseMode, String fileName, String format, String outCameraDirectory) { + return createOutFile(context, chooseMode, fileName, format, outCameraDirectory); + } + + @Nullable + private static File createOutFile(Context context, int chooseMode, String fileName, String format, String outCameraDirectory) { + File folderDir = null; + if (TextUtils.isEmpty(outCameraDirectory)) { + // 外部没有自定义拍照存储路径使用默认 + String state = Environment.getExternalStorageState(); + File rootDir = state.equals(Environment.MEDIA_MOUNTED) ? Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM) + : getRootDirFile(context, chooseMode); + if (rootDir != null) { + if (!rootDir.exists()) { + rootDir.mkdirs(); + } + folderDir = new File(rootDir.getAbsolutePath() + File.separator + PictureMimeType.CAMERA + File.separator); + if (!folderDir.exists() && folderDir.mkdirs()) { + } + } + } else { + // 自定义存储路径 + folderDir = new File(outCameraDirectory); + if (!folderDir.exists()) { + folderDir.mkdirs(); + } + } + + if (folderDir == null) { + throw new NullPointerException("The media output path cannot be null"); + } + + boolean isOutFileNameEmpty = TextUtils.isEmpty(fileName); + switch (chooseMode) { + case PictureConfig.TYPE_VIDEO: + String newFileVideoName = isOutFileNameEmpty ? DateUtils.getCreateFileName("VID_") + POST_VIDEO : fileName; + return new File(folderDir, newFileVideoName); + case PictureConfig.TYPE_AUDIO: + String newFileAudioName = isOutFileNameEmpty ? DateUtils.getCreateFileName("AUD_") + POST_AUDIO : fileName; + return new File(folderDir, newFileAudioName); + default: + String suffix = TextUtils.isEmpty(format) ? POSTFIX : format; + String newFileImageName = isOutFileNameEmpty ? DateUtils.getCreateFileName("IMG_") + suffix : fileName; + return new File(folderDir, newFileImageName); + } + } + + /** + * 文件根目录 + * + * @param context + * @param type + * @return + */ + @Nullable + private static File getRootDirFile(Context context, int type) { + switch (type) { + case PictureConfig.TYPE_VIDEO: + return context.getExternalFilesDir(Environment.DIRECTORY_MOVIES); + case PictureConfig.TYPE_AUDIO: + return context.getExternalFilesDir(Environment.DIRECTORY_MUSIC); + default: + return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + } + } + + /** + * TAG for log messages. + */ + static final String TAG = "PictureFileUtils"; + + private PictureFileUtils() { + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + * @author paulburke + */ + public static boolean isExternalStorageDocument(Uri uri) { + return "com.android.externalstorage.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + * @author paulburke + */ + public static boolean isDownloadsDocument(Uri uri) { + return "com.android.providers.downloads.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + * @author paulburke + */ + public static boolean isMediaDocument(Uri uri) { + return "com.android.providers.media.documents".equals(uri.getAuthority()); + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + public static boolean isGooglePhotosUri(Uri uri) { + return "com.google.android.apps.photos.content".equals(uri.getAuthority()); + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + * @author paulburke + */ + public static String getDataColumn(Context context, Uri uri, String selection, + String[] selectionArgs) { + + Cursor cursor = null; + final String column = "_data"; + final String[] projection = { + column + }; + + try { + cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs, + null); + if (cursor != null && cursor.moveToFirst()) { + final int column_index = cursor.getColumnIndexOrThrow(column); + return cursor.getString(column_index); + } + } catch (IllegalArgumentException ex) { + Log.i(TAG, String.format(Locale.getDefault(), "getDataColumn: _data - [%s]", ex.getMessage())); + } finally { + if (cursor != null) { + cursor.close(); + } + } + return null; + } + + /** + * Get a file path from a Uri. This will get the the path for Storage Access + * Framework Documents, as well as the _data field for the MediaStore and + * other file-based ContentProviders.
+ *
+ * Callers should check whether the path is local before assuming it + * represents a local file. + * + * @param context The context. + * @param uri The Uri to query. + * @author paulburke + */ + @SuppressLint("NewApi") + public static String getPath(final Context ctx, final Uri uri) { + Context context = ctx.getApplicationContext(); + final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT; + + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { + if (isExternalStorageDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + if ("primary".equalsIgnoreCase(type)) { + if (SdkVersionUtils.checkedAndroid_Q()) { + return context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) + "/" + split[1]; + } else { + return Environment.getExternalStorageDirectory() + "/" + split[1]; + } + } + + // TODO handle non-primary volumes + } + // DownloadsProvider + else if (isDownloadsDocument(uri)) { + + final String id = DocumentsContract.getDocumentId(uri); + final Uri contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), Long.valueOf(id)); + + return getDataColumn(context, contentUri, null, null); + } + // MediaProvider + else if (isMediaDocument(uri)) { + final String docId = DocumentsContract.getDocumentId(uri); + final String[] split = docId.split(":"); + final String type = split[0]; + + Uri contentUri = null; + if ("image".equals(type)) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI; + } else if ("video".equals(type)) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI; + } else if ("audio".equals(type)) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; + } + + final String selection = "_id=?"; + final String[] selectionArgs = new String[]{ + split[1] + }; + + return getDataColumn(context, contentUri, selection, selectionArgs); + } + } + // MediaStore (and general) + else if ("content".equalsIgnoreCase(uri.getScheme())) { + + // Return the remote address + if (isGooglePhotosUri(uri)) { + return uri.getLastPathSegment(); + } + + return getDataColumn(context, uri, null, null); + } + // File + else if ("file".equalsIgnoreCase(uri.getScheme())) { + return uri.getPath(); + } + + return null; + } + + /** + * Copies one file into the other with the given paths. + * In the event that the paths are the same, trying to copy one file to the other + * will cause both files to become null. + * Simply skipping this step if the paths are identical. + */ + public static void copyFile(@NonNull String pathFrom, @NonNull String pathTo) throws IOException { + if (pathFrom.equalsIgnoreCase(pathTo)) { + return; + } + + FileChannel outputChannel = null; + FileChannel inputChannel = null; + try { + inputChannel = new FileInputStream(new File(pathFrom)).getChannel(); + outputChannel = new FileOutputStream(new File(pathTo)).getChannel(); + inputChannel.transferTo(0, inputChannel.size(), outputChannel); + inputChannel.close(); + } finally { + if (inputChannel != null) { + inputChannel.close(); + } + if (outputChannel != null) { + outputChannel.close(); + } + } + } + + /** + * 拷贝文件 + * + * @param outFile + * @return + */ + public static boolean bufferCopy(BufferedSource inBuffer, File outFile) { + BufferedSink outBuffer = null; + try { + outBuffer = Okio.buffer(Okio.sink(outFile)); + outBuffer.writeAll(inBuffer); + outBuffer.flush(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + close(inBuffer); + close(outBuffer); + } + return false; + } + + /** + * 拷贝文件 + * + * @param outputStream + * @return + */ + public static boolean bufferCopy(BufferedSource inBuffer, OutputStream outputStream) { + BufferedSink outBuffer = null; + try { + outBuffer = Okio.buffer(Okio.sink(outputStream)); + outBuffer.writeAll(inBuffer); + outBuffer.flush(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + close(inBuffer); + close(outBuffer); + } + return false; + } + + + /** + * 拷贝文件 + * + * @param inFile + * @param outPutStream + * @return + */ + public static boolean bufferCopy(File inFile, OutputStream outPutStream) { + BufferedSource inBuffer = null; + BufferedSink outBuffer = null; + try { + inBuffer = Okio.buffer(Okio.source(inFile)); + outBuffer = Okio.buffer(Okio.sink(outPutStream)); + outBuffer.writeAll(inBuffer); + outBuffer.flush(); + return true; + } catch (Exception e) { + e.printStackTrace(); + } finally { + close(inBuffer); + close(outPutStream); + close(outBuffer); + } + return false; + } + + /** + * 读取图片属性:旋转的角度 + * + * @param path 图片绝对路径 + * @return degree旋转的角度 + */ + public static int readPictureDegree(Context context, String path) { + int degree = 0; + try { + ExifInterface exifInterface; + if (SdkVersionUtils.checkedAndroid_Q()) { + ParcelFileDescriptor parcelFileDescriptor = + context.getContentResolver() + .openFileDescriptor(Uri.parse(path), "r"); + exifInterface = new ExifInterface(parcelFileDescriptor.getFileDescriptor()); + } else { + exifInterface = new ExifInterface(path); + } + int orientation = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + switch (orientation) { + case ExifInterface.ORIENTATION_ROTATE_90: + degree = 90; + break; + case ExifInterface.ORIENTATION_ROTATE_180: + degree = 180; + break; + case ExifInterface.ORIENTATION_ROTATE_270: + degree = 270; + break; + } + } catch (Exception e) { + e.printStackTrace(); + } + return degree; + } + + /** + * getDCIMCameraPath + * + * @return + */ + public static String getDCIMCameraPath() { + String absolutePath; + try { + absolutePath = "%" + Environment.getExternalStoragePublicDirectory + (Environment.DIRECTORY_DCIM).getAbsolutePath() + "/Camera"; + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + return absolutePath; + } + + /** + * set empty PictureSelector Cache + * + * @param mContext + * @param type image or video ... + */ + public static void deleteCacheDirFile(Context mContext, int type) { + File cutDir = mContext.getExternalFilesDir(type == PictureMimeType.ofImage() + ? Environment.DIRECTORY_PICTURES : Environment.DIRECTORY_MOVIES); + if (cutDir != null) { + File[] files = cutDir.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + } + + /** + * set empty PictureSelector Cache + * + * @param context + * @param type image、video、audio ... + */ + public static void deleteAllCacheDirFile(Context context) { + + File dirPictures = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (dirPictures != null) { + File[] files = dirPictures.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + + File dirMovies = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES); + if (dirMovies != null) { + File[] files = dirMovies.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + + File dirMusic = context.getExternalFilesDir(Environment.DIRECTORY_MUSIC); + if (dirMusic != null) { + File[] files = dirMusic.listFiles(); + if (files != null) { + for (File file : files) { + if (file.isFile()) { + file.delete(); + } + } + } + } + } + + /** + * @param ctx + * @return + */ + public static String getDiskCacheDir(Context ctx) { + File filesDir = ctx.getExternalFilesDir(Environment.DIRECTORY_PICTURES); + if (filesDir == null) { + return ""; + } + return filesDir.getPath(); + } + + /** + * @param ctx + * @return + */ + public static String getVideoDiskCacheDir(Context ctx) { + File filesDir = ctx.getExternalFilesDir(Environment.DIRECTORY_MOVIES); + if (filesDir == null) { + return ""; + } + return filesDir.getPath(); + } + + /** + * @param ctx + * @return + */ + public static String getAudioDiskCacheDir(Context ctx) { + File filesDir = ctx.getExternalFilesDir(Environment.DIRECTORY_MUSIC); + if (filesDir == null) { + return ""; + } + return filesDir.getPath(); + } + + /** + * 生成uri + * + * @param context + * @param cameraFile + * @return + */ + public static Uri parUri(Context context, File cameraFile) { + Uri imageUri; + String authority = context.getPackageName() + ".provider"; + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.M) { + //通过FileProvider创建一个content类型的Uri + imageUri = FileProvider.getUriForFile(context, authority, cameraFile); + } else { + imageUri = Uri.fromFile(cameraFile); + } + return imageUri; + } + + /** + * 获取图片后缀 + * + * @param input + * @return + */ + public static String extSuffix(InputStream input) { + try { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inJustDecodeBounds = true; + BitmapFactory.decodeStream(input, null, options); + return options.outMimeType.replace("image/", "."); + } catch (Exception e) { + return PictureMimeType.JPEG; + } + } + + /** + * 根据类型创建文件名 + * + * @param context + * @param md5 + * @param mineType + * @param customFileName + * @return + */ + public static String createFilePath(Context context, String md5, String mineType, String customFileName) { + String suffix = PictureMimeType.getLastImgSuffix(mineType); + if (PictureMimeType.isHasVideo(mineType)) { + // 视频 + String filesDir = PictureFileUtils.getVideoDiskCacheDir(context) + File.separator; + if (!TextUtils.isEmpty(md5)) { + String fileName = TextUtils.isEmpty(customFileName) ? "VID_" + md5.toUpperCase() + suffix : customFileName; + return filesDir + fileName; + } else { + String fileName = TextUtils.isEmpty(customFileName) ? DateUtils.getCreateFileName("VID_") + suffix : customFileName; + return filesDir + fileName; + } + } else if (PictureMimeType.isHasAudio(mineType)) { + // 音频 + String filesDir = PictureFileUtils.getAudioDiskCacheDir(context) + File.separator; + if (!TextUtils.isEmpty(md5)) { + String fileName = TextUtils.isEmpty(customFileName) ? "AUD_" + md5.toUpperCase() + suffix : customFileName; + return filesDir + fileName; + } else { + String fileName = TextUtils.isEmpty(customFileName) ? DateUtils.getCreateFileName("AUD_") + suffix : customFileName; + return filesDir + fileName; + } + } else { + // 图片 + String filesDir = PictureFileUtils.getDiskCacheDir(context) + File.separator; + if (!TextUtils.isEmpty(md5)) { + String fileName = TextUtils.isEmpty(customFileName) ? "IMG_" + md5.toUpperCase() + suffix : customFileName; + return filesDir + fileName; + } else { + String fileName = TextUtils.isEmpty(customFileName) ? DateUtils.getCreateFileName("IMG_") + suffix : customFileName; + return filesDir + fileName; + } + } + } + + /** + * 判断文件是否存在 + * + * @param path + * @return + */ + public static boolean isFileExists(String path) { + if (!TextUtils.isEmpty(path) && !new File(path).exists()) { + return false; + } + return true; + } + + @SuppressWarnings("ConstantConditions") + public static void close(@Nullable Closeable c) { + // java.lang.IncompatibleClassChangeError: interface not implemented + if (c != null && c instanceof Closeable) { + try { + c.close(); + } catch (Exception e) { + // silence + } + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/SPUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/SPUtils.java new file mode 100644 index 0000000..ba445b8 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/SPUtils.java @@ -0,0 +1,470 @@ +package com.luck.picture.lib.tools; + +import android.annotation.SuppressLint; +import android.app.Application; +import android.content.Context; +import android.content.SharedPreferences; + +import androidx.annotation.NonNull; + +import java.lang.reflect.InvocationTargetException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author:luck + * @date:2019-11-25 21:30 + * @describe:SharedPreferences + */ +public class SPUtils { + private static final Map SP_UTILS_MAP = new HashMap<>(); + private SharedPreferences sp; + + public static SPUtils getPictureSpUtils() { + return SPUtils.getInstance("PictureSpUtils"); + } + + /** + * Return the single {@link SPUtils} instance + * + * @return the single {@link SPUtils} instance + */ + public static SPUtils getInstance() { + return getInstance("", Context.MODE_PRIVATE); + } + + /** + * Return the single {@link SPUtils} instance + * + * @param mode Operating mode. + * @return the single {@link SPUtils} instance + */ + public static SPUtils getInstance(final int mode) { + return getInstance("", mode); + } + + /** + * Return the single {@link SPUtils} instance + * + * @param spName The name of sp. + * @return the single {@link SPUtils} instance + */ + public static SPUtils getInstance(String spName) { + return getInstance(spName, Context.MODE_PRIVATE); + } + + /** + * Return the single {@link SPUtils} instance + * + * @param spName The name of sp. + * @param mode Operating mode. + * @return the single {@link SPUtils} instance + */ + public static SPUtils getInstance(String spName, final int mode) { + if (isSpace(spName)) spName = "spUtils"; + SPUtils spUtils = SP_UTILS_MAP.get(spName); + if (spUtils == null) { + synchronized (SPUtils.class) { + spUtils = SP_UTILS_MAP.get(spName); + if (spUtils == null) { + spUtils = new SPUtils(spName, mode); + SP_UTILS_MAP.put(spName, spUtils); + } + } + } + return spUtils; + } + + private SPUtils(final String spName) { + sp = getApplicationByReflect().getSharedPreferences(spName, Context.MODE_PRIVATE); + } + + private SPUtils(final String spName, final int mode) { + sp = getApplicationByReflect().getSharedPreferences(spName, mode); + } + + /** + * Put the string value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final String value) { + put(key, value, false); + } + + /** + * Put the string value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, final String value, final boolean isCommit) { + if (isCommit) { + sp.edit().putString(key, value).commit(); + } else { + sp.edit().putString(key, value).apply(); + } + } + + /** + * Return the string value in sp. + * + * @param key The key of sp. + * @return the string value if sp exists or {@code ""} otherwise + */ + public String getString(@NonNull final String key) { + return getString(key, ""); + } + + /** + * Return the string value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the string value if sp exists or {@code defaultValue} otherwise + */ + public String getString(@NonNull final String key, final String defaultValue) { + return sp.getString(key, defaultValue); + } + + /** + * Put the int value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final int value) { + put(key, value, false); + } + + /** + * Put the int value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, final int value, final boolean isCommit) { + if (isCommit) { + sp.edit().putInt(key, value).commit(); + } else { + sp.edit().putInt(key, value).apply(); + } + } + + /** + * Return the int value in sp. + * + * @param key The key of sp. + * @return the int value if sp exists or {@code -1} otherwise + */ + public int getInt(@NonNull final String key) { + return getInt(key, -1); + } + + /** + * Return the int value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the int value if sp exists or {@code defaultValue} otherwise + */ + public int getInt(@NonNull final String key, final int defaultValue) { + return sp.getInt(key, defaultValue); + } + + /** + * Put the long value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final long value) { + put(key, value, false); + } + + /** + * Put the long value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, final long value, final boolean isCommit) { + if (isCommit) { + sp.edit().putLong(key, value).commit(); + } else { + sp.edit().putLong(key, value).apply(); + } + } + + /** + * Return the long value in sp. + * + * @param key The key of sp. + * @return the long value if sp exists or {@code -1} otherwise + */ + public long getLong(@NonNull final String key) { + return getLong(key, -1L); + } + + /** + * Return the long value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the long value if sp exists or {@code defaultValue} otherwise + */ + public long getLong(@NonNull final String key, final long defaultValue) { + return sp.getLong(key, defaultValue); + } + + /** + * Put the float value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final float value) { + put(key, value, false); + } + + /** + * Put the float value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, final float value, final boolean isCommit) { + if (isCommit) { + sp.edit().putFloat(key, value).commit(); + } else { + sp.edit().putFloat(key, value).apply(); + } + } + + /** + * Return the float value in sp. + * + * @param key The key of sp. + * @return the float value if sp exists or {@code -1f} otherwise + */ + public float getFloat(@NonNull final String key) { + return getFloat(key, -1f); + } + + /** + * Return the float value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the float value if sp exists or {@code defaultValue} otherwise + */ + public float getFloat(@NonNull final String key, final float defaultValue) { + return sp.getFloat(key, defaultValue); + } + + /** + * Put the boolean value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final boolean value) { + put(key, value, false); + } + + /** + * Put the boolean value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, final boolean value, final boolean isCommit) { + if (isCommit) { + sp.edit().putBoolean(key, value).commit(); + } else { + sp.edit().putBoolean(key, value).apply(); + } + } + + /** + * Return the boolean value in sp. + * + * @param key The key of sp. + * @return the boolean value if sp exists or {@code false} otherwise + */ + public boolean getBoolean(@NonNull final String key) { + return getBoolean(key, false); + } + + /** + * Return the boolean value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the boolean value if sp exists or {@code defaultValue} otherwise + */ + public boolean getBoolean(@NonNull final String key, final boolean defaultValue) { + return sp.getBoolean(key, defaultValue); + } + + /** + * Put the set of string value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + */ + public void put(@NonNull final String key, final Set value) { + put(key, value, false); + } + + /** + * Put the set of string value in sp. + * + * @param key The key of sp. + * @param value The value of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void put(@NonNull final String key, + final Set value, + final boolean isCommit) { + if (isCommit) { + sp.edit().putStringSet(key, value).commit(); + } else { + sp.edit().putStringSet(key, value).apply(); + } + } + + /** + * Return the set of string value in sp. + * + * @param key The key of sp. + * @return the set of string value if sp exists + * or {@code Collections.emptySet()} otherwise + */ + public Set getStringSet(@NonNull final String key) { + return getStringSet(key, Collections.emptySet()); + } + + /** + * Return the set of string value in sp. + * + * @param key The key of sp. + * @param defaultValue The default value if the sp doesn't exist. + * @return the set of string value if sp exists or {@code defaultValue} otherwise + */ + public Set getStringSet(@NonNull final String key, + final Set defaultValue) { + return sp.getStringSet(key, defaultValue); + } + + /** + * Return all values in sp. + * + * @return all values in sp + */ + public Map getAll() { + return sp.getAll(); + } + + /** + * Return whether the sp contains the preference. + * + * @param key The key of sp. + * @return {@code true}: yes
{@code false}: no + */ + public boolean contains(@NonNull final String key) { + return sp.contains(key); + } + + /** + * Remove the preference in sp. + * + * @param key The key of sp. + */ + public void remove(@NonNull final String key) { + remove(key, false); + } + + /** + * Remove the preference in sp. + * + * @param key The key of sp. + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void remove(@NonNull final String key, final boolean isCommit) { + if (isCommit) { + sp.edit().remove(key).commit(); + } else { + sp.edit().remove(key).apply(); + } + } + + /** + * Remove all preferences in sp. + */ + public void clear() { + clear(false); + } + + /** + * Remove all preferences in sp. + * + * @param isCommit True to use {@link SharedPreferences.Editor#commit()}, + * false to use {@link SharedPreferences.Editor#apply()} + */ + public void clear(final boolean isCommit) { + if (isCommit) { + sp.edit().clear().commit(); + } else { + sp.edit().clear().apply(); + } + } + + 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 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"); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/ScreenUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/ScreenUtils.java new file mode 100644 index 0000000..6d08005 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/ScreenUtils.java @@ -0,0 +1,48 @@ +package com.luck.picture.lib.tools; + +import android.app.Activity; +import android.content.Context; +import android.util.DisplayMetrics; + +import java.lang.reflect.Field; + +/** + * @author:luck + * @date:2017-5-30 19:30 + * @describe:ScreenUtils + */ +public class ScreenUtils { + /** + * dp2px + */ + public static int dip2px(Context context, float dpValue) { + final float scale = context.getApplicationContext().getResources().getDisplayMetrics().density; + return (int) (dpValue * scale + 0.5f); + } + + public static int getScreenWidth(Context context) { + DisplayMetrics localDisplayMetrics = new DisplayMetrics(); + ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(localDisplayMetrics); + return localDisplayMetrics.widthPixels; + } + + public static int getScreenHeight(Context context) { + DisplayMetrics localDisplayMetrics = new DisplayMetrics(); + ((Activity) context).getWindowManager().getDefaultDisplay().getMetrics(localDisplayMetrics); + return localDisplayMetrics.heightPixels - getStatusBarHeight(context); + } + + public static int getStatusBarHeight(Context context) { + int statusBarHeight = 0; + try { + Class c = Class.forName("com.android.internal.R$dimen"); + Object o = c.newInstance(); + Field field = c.getField("status_bar_height"); + int x = (Integer) field.get(o); + statusBarHeight = context.getApplicationContext().getResources().getDimensionPixelSize(x); + } catch (Exception e) { + e.printStackTrace(); + } + return statusBarHeight == 0 ? dip2px(context, 25) : statusBarHeight; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/SdkVersionUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/SdkVersionUtils.java new file mode 100644 index 0000000..0061e74 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/SdkVersionUtils.java @@ -0,0 +1,19 @@ +package com.luck.picture.lib.tools; + +import android.os.Build; + +/** + * @author:luck + * @date:2019-07-17 15:12 + * @describe:Android Sdk版本判断 + */ +public class SdkVersionUtils { + /** + * 判断是否是Android Q版本 + * + * @return + */ + public static boolean checkedAndroid_Q() { + return Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/StringUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/StringUtils.java new file mode 100644 index 0000000..c3edef8 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/StringUtils.java @@ -0,0 +1,111 @@ +package com.luck.picture.lib.tools; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.RelativeSizeSpan; +import android.widget.TextView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.config.PictureMimeType; + +import java.util.regex.Pattern; + +/** + * @author:luck + * @data:2017/5/25 19:12 + * @描述: String Utils + */ +public class StringUtils { + + public static void tempTextFont(TextView tv, int mimeType) { + String text = tv.getText().toString().trim(); + String str = mimeType == PictureMimeType.ofAudio() ? + tv.getContext().getString(R.string.picture_empty_audio_title) + : tv.getContext().getString(R.string.picture_empty_title); + String sumText = str + text; + Spannable placeSpan = new SpannableString(sumText); + placeSpan.setSpan(new RelativeSizeSpan(0.8f), str.length(), sumText.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + tv.setText(placeSpan); + } + + /** + * 匹配数值 + * + * @param str + * @return + */ + public static int stringToInt(String str) { + Pattern pattern = Pattern.compile("^[-\\+]?[\\d]+$"); + return pattern.matcher(str).matches() ? Integer.valueOf(str) : 0; + } + + /** + * 根据类型获取相应的Toast文案 + * + * @param context + * @param mimeType + * @param maxSelectNum + * @return + */ + @SuppressLint("StringFormatMatches") + public static String getMsg(Context context, String mimeType, int maxSelectNum) { + if (PictureMimeType.isHasVideo(mimeType)) { + return context.getString(R.string.picture_message_video_max_num, maxSelectNum); + } else if (PictureMimeType.isHasAudio(mimeType)) { + return context.getString(R.string.picture_message_audio_max_num, maxSelectNum); + } else { + return context.getString(R.string.picture_message_max_num, maxSelectNum); + } + } + + /** + * 重命名相册拍照 + * + * @param fileName + * @return + */ + public static String rename(String fileName) { + String temp = fileName.substring(0, fileName.lastIndexOf(".")); + String suffix = fileName.substring(fileName.lastIndexOf(".")); + return temp + "_" + DateUtils.getCreateFileName() + suffix; + } + + /** + * 重命名后缀 + * + * @param fileName + * @return + */ + public static String renameSuffix(String fileName, String suffix) { + String temp = fileName.substring(0, fileName.lastIndexOf(".")); + return temp + suffix; + } + + /** + * getEncryptionValue + * + * @param url + * @param width + * @param height + * @return + */ + public static String getEncryptionValue(String url, int width, int height) { + StringBuilder stringBuilder = new StringBuilder(); + stringBuilder.append(url).append("_").append(width).append("x").append(height); + return ValueOf.toString(Math.abs(hash(stringBuilder.hashCode()))); + } + + /** + * hash + * + * @param key + * @return + */ + public static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/ToastUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/ToastUtils.java new file mode 100644 index 0000000..9705818 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/ToastUtils.java @@ -0,0 +1,34 @@ +package com.luck.picture.lib.tools; + +import android.content.Context; +import android.widget.Toast; + +/** + * @author:luck + * @data:2018/3/28 下午4:10 + * @描述: Toast工具类 + */ + +public final class ToastUtils { + public static void s(Context context, String s) { + if (!isShowToast()) { + Toast.makeText(context.getApplicationContext(), s, Toast.LENGTH_SHORT) + .show(); + } + } + + /** + * Prevent continuous click, jump two pages + */ + private static long lastToastTime; + private final static long TIME = 1500; + + public static boolean isShowToast() { + long time = System.currentTimeMillis(); + if (time - lastToastTime < TIME) { + return true; + } + lastToastTime = time; + return false; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/ValueOf.java b/picture_library/src/main/java/com/luck/picture/lib/tools/ValueOf.java new file mode 100644 index 0000000..da18653 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/ValueOf.java @@ -0,0 +1,142 @@ +package com.luck.picture.lib.tools; + +/** + * @author:luck + * @date:2019-11-12 14:27 + * @describe:类型转换工具类 + */ +public class ValueOf { + public static String toString(Object o) { + String value = ""; + try { + value = o.toString(); + } catch (Exception e) { + } + + return value; + } + + + public static double toDouble(Object o) { + + return toDouble(o, 0); + } + + public static double toDouble(Object o, int defaultValue) { + if (o == null) { + return defaultValue; + } + + double value; + try { + value = Double.valueOf(o.toString().trim()); + } catch (Exception e) { + value = defaultValue; + } + + return value; + } + + public static long toLong(Object o, long defaultValue) { + if (o == null) { + return defaultValue; + } + long value = 0; + try { + String s = o.toString().trim(); + if (s.contains(".")) { + value = Long.valueOf(s.substring(0, s.lastIndexOf("."))); + } else { + value = Long.valueOf(s); + } + } catch (Exception e) { + value = defaultValue; + } + + + return value; + } + + public static long toLong(Object o) { + return toLong(o, 0); + } + + + public static float toFloat(Object o, long defaultValue) { + if (o == null) { + return defaultValue; + } + float value = 0; + try { + String s = o.toString().trim(); + value = Float.valueOf(s); + } catch (Exception e) { + value = defaultValue; + } + + + return value; + } + + public static float toFloat(Object o) { + return toFloat(o, 0); + } + + + public static int toInt(Object o, int defaultValue) { + if (o == null) { + return defaultValue; + } + int value; + try { + String s = o.toString().trim(); + if (s.contains(".")) { + value = Integer.valueOf(s.substring(0, s.lastIndexOf("."))); + } else { + value = Integer.valueOf(s); + } + } catch (Exception e) { + value = defaultValue; + } + + return value; + } + + public static int toInt(Object o) { + return toInt(o, 0); + } + + public static boolean toBoolean(Object o) { + return toBoolean(o, false); + + } + + + public static boolean toBoolean(Object o, boolean defaultValue) { + if (o == null) { + return false; + } + boolean value; + try { + String s = o.toString().trim(); + if ("false".equals(s.trim())) { + value = false; + } else { + value = true; + } + } catch (Exception e) { + value = defaultValue; + } + + return value; + } + + + public static T to(Object o, T defaultValue) { + if (o == null) { + return defaultValue; + } + T value = (T)o; + return (T) value; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/tools/VoiceUtils.java b/picture_library/src/main/java/com/luck/picture/lib/tools/VoiceUtils.java new file mode 100644 index 0000000..5577bce --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/tools/VoiceUtils.java @@ -0,0 +1,72 @@ +package com.luck.picture.lib.tools; + +import android.content.Context; +import android.media.AudioManager; +import android.media.SoundPool; + +import com.luck.picture.lib.R; + +/** + * @author:luck + * @data:2017/5/25 19:12 + * @描述: voice utils + */ +public class VoiceUtils { + + private static VoiceUtils instance; + + public static VoiceUtils getInstance() { + if (instance == null) { + synchronized (VoiceUtils.class) { + if (instance == null) { + instance = new VoiceUtils(); + } + } + } + return instance; + } + + public VoiceUtils() { + } + + private SoundPool soundPool; + /** + * 创建某个声音对应的音频ID + */ + private int soundID; + + public void init(Context context) { + initPool(context); + } + + private void initPool(Context context) { + if (soundPool == null) { + soundPool = new SoundPool(1, AudioManager.STREAM_ALARM, 0); + soundID = soundPool.load(context.getApplicationContext(), R.raw.picture_music, 1); + } + } + + /** + * 播放音频 + */ + public void play() { + if (soundPool != null) { + soundPool.play(soundID, 0.1f, 0.5f, 0, 1, 1); + } + } + + /** + * 释放资源 + */ + public void releaseSoundPool() { + try { + if (soundPool != null) { + soundPool.release(); + soundPool = null; + } + instance = null; + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/FolderPopWindow.java b/picture_library/src/main/java/com/luck/picture/lib/widget/FolderPopWindow.java new file mode 100644 index 0000000..5876953 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/FolderPopWindow.java @@ -0,0 +1,197 @@ +package com.luck.picture.lib.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.os.Build; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.PopupWindow; +import android.widget.RelativeLayout; + +import androidx.core.content.ContextCompat; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.R; +import com.luck.picture.lib.adapter.PictureAlbumDirectoryAdapter; +import com.luck.picture.lib.config.PictureSelectionConfig; +import com.luck.picture.lib.entity.LocalMedia; +import com.luck.picture.lib.entity.LocalMediaFolder; +import com.luck.picture.lib.listener.OnAlbumItemClickListener; +import com.luck.picture.lib.tools.AnimUtils; +import com.luck.picture.lib.tools.AttrsUtils; +import com.luck.picture.lib.tools.ScreenUtils; + +import java.util.List; + +/** + * @author:luck + * @date:2017-5-25 17:02 + * @describe:文件目录PopupWindow + */ + +public class FolderPopWindow extends PopupWindow { + private Context context; + private View window; + private View rootView; + private RecyclerView mRecyclerView; + private PictureAlbumDirectoryAdapter adapter; + private boolean isDismiss = false; + private ImageView ivArrowView; + private Drawable drawableUp, drawableDown; + private int chooseMode; + private PictureSelectionConfig config; + private int maxHeight; + private View rootViewBg; + + public FolderPopWindow(Context context, PictureSelectionConfig config) { + this.context = context; + this.config = config; + this.chooseMode = config.chooseMode; + this.window = LayoutInflater.from(context).inflate(R.layout.picture_window_folder, null); + this.setContentView(window); + this.setWidth(RelativeLayout.LayoutParams.MATCH_PARENT); + this.setHeight(RelativeLayout.LayoutParams.WRAP_CONTENT); + this.setAnimationStyle(R.style.PictureThemeWindowStyle); + this.setFocusable(true); + this.setOutsideTouchable(true); + this.update(); + if (config.style != null) { + if (config.style.pictureTitleUpResId != 0) { + this.drawableUp = ContextCompat.getDrawable(context, config.style.pictureTitleUpResId); + } + if (config.style.pictureTitleDownResId != 0) { + this.drawableDown = ContextCompat.getDrawable(context, config.style.pictureTitleDownResId); + } + } else { + if (config.isWeChatStyle) { + this.drawableUp = ContextCompat.getDrawable(context, R.drawable.picture_icon_wechat_up); + this.drawableDown = ContextCompat.getDrawable(context, R.drawable.picture_icon_wechat_down); + } else { + if (config.upResId != 0) { + this.drawableUp = ContextCompat.getDrawable(context, config.upResId); + } else { + // 兼容老的Theme方式 + this.drawableUp = AttrsUtils.getTypeValueDrawable(context, R.attr.picture_arrow_up_icon); + } + if (config.downResId != 0) { + this.drawableDown = ContextCompat.getDrawable(context, config.downResId); + } else { + // 兼容老的Theme方式 picture.arrow_down.icon + this.drawableDown = AttrsUtils.getTypeValueDrawable(context, R.attr.picture_arrow_down_icon); + } + } + } + this.maxHeight = (int) (ScreenUtils.getScreenHeight(context) * 0.6); + initView(); + } + + public void initView() { + rootViewBg = window.findViewById(R.id.rootViewBg); + adapter = new PictureAlbumDirectoryAdapter(config); + mRecyclerView = window.findViewById(R.id.folder_list); + mRecyclerView.setLayoutManager(new LinearLayoutManager(context)); + mRecyclerView.setAdapter(adapter); + rootView = window.findViewById(R.id.rootView); + rootViewBg.setOnClickListener(v -> dismiss()); + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) { + rootView.setOnClickListener(v -> dismiss()); + } + } + + public void bindFolder(List folders) { + adapter.setChooseMode(chooseMode); + adapter.bindFolderData(folders); + ViewGroup.LayoutParams lp = mRecyclerView.getLayoutParams(); + lp.height = folders != null && folders.size() > 8 ? maxHeight + : ViewGroup.LayoutParams.WRAP_CONTENT; + } + + public List getFolderData() { + return adapter.getFolderData(); + } + + public boolean isEmpty() { + return adapter.getFolderData().size() == 0; + } + + public LocalMediaFolder getFolder(int position) { + return adapter.getFolderData().size() > 0 + && position < adapter.getFolderData().size() ? adapter.getFolderData().get(position) : null; + } + + public void setArrowImageView(ImageView ivArrowView) { + this.ivArrowView = ivArrowView; + } + + @Override + public void showAsDropDown(View anchor) { + try { + if (Build.VERSION.SDK_INT == Build.VERSION_CODES.N) { + int[] location = new int[2]; + anchor.getLocationInWindow(location); + showAtLocation(anchor, Gravity.NO_GRAVITY, 0, location[1] + anchor.getHeight()); + } else { + super.showAsDropDown(anchor); + } + isDismiss = false; + ivArrowView.setImageDrawable(drawableUp); + AnimUtils.rotateArrow(ivArrowView, true); + rootViewBg.animate() + .alpha(1) + .setDuration(250) + .setStartDelay(250).start(); + } catch (Exception e) { + e.printStackTrace(); + } + } + + public void setOnAlbumItemClickListener(OnAlbumItemClickListener listener) { + adapter.setOnAlbumItemClickListener(listener); + } + + @Override + public void dismiss() { + if (isDismiss) { + return; + } + rootViewBg.animate() + .alpha(0) + .setDuration(50) + .start(); + ivArrowView.setImageDrawable(drawableDown); + AnimUtils.rotateArrow(ivArrowView, false); + isDismiss = true; + FolderPopWindow.super.dismiss(); + isDismiss = false; + } + + /** + * 设置选中状态 + */ + public void updateFolderCheckStatus(List result) { + try { + List folders = adapter.getFolderData(); + int size = folders.size(); + int resultSize = result.size(); + for (int i = 0; i < size; i++) { + LocalMediaFolder folder = folders.get(i); + folder.setCheckedNum(0); + for (int j = 0; j < resultSize; j++) { + LocalMedia media = result.get(j); + if (folder.getName().equals(media.getParentFolderName()) + || folder.getBucketId() == -1) { + folder.setCheckedNum(1); + break; + } + } + } + adapter.bindFolderData(folders); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/MScroller.java b/picture_library/src/main/java/com/luck/picture/lib/widget/MScroller.java new file mode 100644 index 0000000..a6846b9 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/MScroller.java @@ -0,0 +1,43 @@ +package com.luck.picture.lib.widget; + +import android.content.Context; +import android.widget.Scroller; +import android.view.animation.Interpolator; + +/** + * @author:luck + * @date:2020-04-11 14:41 + * @describe:MScroller + */ +public class MScroller extends Scroller { + + public MScroller(Context context) { + this(context, sInterpolator); + } + + + public MScroller(Context context, Interpolator interpolator) { + super(context, interpolator); + } + + private static final Interpolator sInterpolator = t -> { + t -= 1.0f; + return t * t * t * t * t + 1.0f; + }; + + public boolean noDuration; + + public void setNoDuration(boolean noDuration) { + this.noDuration = noDuration; + } + + @Override + public void startScroll(int startX, int startY, int dx, int dy, int duration) { + if (noDuration) { + //界面滑动不需要时间间隔 + super.startScroll(startX, startY, dx, dy, 0); + } else { + super.startScroll(startX, startY, dx, dy, duration); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/MyViewPageHelper.java b/picture_library/src/main/java/com/luck/picture/lib/widget/MyViewPageHelper.java new file mode 100644 index 0000000..5002014 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/MyViewPageHelper.java @@ -0,0 +1,56 @@ +package com.luck.picture.lib.widget; + +import androidx.viewpager.widget.ViewPager; + +import java.lang.reflect.Field; + +/** + * @author:luck + * @date:2020-04-11 14:43 + * @describe:MyViewPageHelper + */ +public class MyViewPageHelper { + ViewPager viewPager; + + MScroller scroller; + + public MyViewPageHelper(ViewPager viewPager) { + this.viewPager = viewPager; + init(); + } + + public void setCurrentItem(int item) { + setCurrentItem(item, true); + } + + public MScroller getScroller() { + return scroller; + } + + + public void setCurrentItem(int item, boolean smooth) { + int current = viewPager.getCurrentItem(); + //如果页面相隔大于1,就设置页面切换的动画的时间为0 + if (Math.abs(current - item) > 1) { + scroller.setNoDuration(true); + viewPager.setCurrentItem(item, smooth); + scroller.setNoDuration(false); + } else { + scroller.setNoDuration(false); + viewPager.setCurrentItem(item, smooth); + } + } + + private void init() { + scroller = new MScroller(viewPager.getContext()); + Class cl = ViewPager.class; + try { + Field field = cl.getDeclaredField("mScroller"); + field.setAccessible(true); + //利用反射设置mScroller域为自己定义的MScroller + field.set(viewPager, scroller); + } catch (Exception e) { + e.printStackTrace(); + } + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/PreviewViewPager.java b/picture_library/src/main/java/com/luck/picture/lib/widget/PreviewViewPager.java new file mode 100644 index 0000000..984c698 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/PreviewViewPager.java @@ -0,0 +1,73 @@ +package com.luck.picture.lib.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.MotionEvent; + +import androidx.viewpager.widget.ViewPager; + +/** + * @author:luck + * @date:2016-12-31 22:12 + * @describe:PreviewViewPager + */ + +public class PreviewViewPager extends ViewPager { + private MyViewPageHelper helper; + + public PreviewViewPager(Context context) { + super(context); + } + + public PreviewViewPager(Context context, AttributeSet attrs) { + super(context, attrs); + helper = new MyViewPageHelper(this); + } + + @Override + public void setCurrentItem(int item) { + setCurrentItem(item, true); + } + + @Override + public void setCurrentItem(int item, boolean smoothScroll) { + MScroller scroller = helper.getScroller(); + if (Math.abs(getCurrentItem() - item) > 1) { + scroller.setNoDuration(true); + super.setCurrentItem(item, smoothScroll); + scroller.setNoDuration(false); + } else { + scroller.setNoDuration(false); + super.setCurrentItem(item, smoothScroll); + } + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + try { + return super.onTouchEvent(ev); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + try { + return super.onInterceptTouchEvent(ev); + } catch (IllegalArgumentException ex) { + ex.printStackTrace(); + } + return false; + } + + @Override + public boolean dispatchTouchEvent(MotionEvent ev) { + try { + return super.dispatchTouchEvent(ev); + } catch (IllegalArgumentException | ArrayIndexOutOfBoundsException ignored) { + } + return false; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/RecyclerPreloadView.java b/picture_library/src/main/java/com/luck/picture/lib/widget/RecyclerPreloadView.java new file mode 100644 index 0000000..a7af54b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/RecyclerPreloadView.java @@ -0,0 +1,144 @@ +package com.luck.picture.lib.widget; + +import android.content.Context; +import android.util.AttributeSet; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.luck.picture.lib.listener.OnRecyclerViewPreloadMoreListener; + +/** + * @author:luck + * @date:2020-04-14 18:43 + * @describe:RecyclerPreloadView + */ +public class RecyclerPreloadView extends RecyclerView { + private static final String TAG = RecyclerPreloadView.class.getSimpleName(); + private static final int BOTTOM_DEFAULT = 1; + public static final int BOTTOM_PRELOAD = 2; + public boolean isInTheBottom = false; + public boolean isEnabledLoadMore = false; + private int mFirstVisiblePosition, mLastVisiblePosition; + /** + * reachBottomRow = 1;(default) + * mean : when the lastVisibleRow is lastRow , call the onReachBottom(); + * reachBottomRow = 2; + * mean : when the lastVisibleRow is Penultimate Row , call the onReachBottom(); + * And so on + */ + private int reachBottomRow = BOTTOM_DEFAULT; + + public RecyclerPreloadView(@NonNull Context context) { + super(context); + } + + public RecyclerPreloadView(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + public RecyclerPreloadView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + + public void setReachBottomRow(int reachBottomRow) { + if (reachBottomRow < 1) + reachBottomRow = 1; + this.reachBottomRow = reachBottomRow; + } + + /** + * Whether to load more + * + * @param isEnabledLoadMore + */ + public void setEnabledLoadMore(boolean isEnabledLoadMore) { + this.isEnabledLoadMore = isEnabledLoadMore; + } + + /** + * Whether to load more + */ + public boolean isEnabledLoadMore() { + return isEnabledLoadMore; + } + + @Override + public void onScrollStateChanged(int newState) { + super.onScrollStateChanged(newState); + if (newState == SCROLL_STATE_IDLE || newState == SCROLL_STATE_DRAGGING) { + LayoutManager layoutManager = getLayoutManager(); + if (layoutManager instanceof GridLayoutManager) { + GridLayoutManager linearManager = (GridLayoutManager) layoutManager; + mFirstVisiblePosition = linearManager.findFirstVisibleItemPosition(); + mLastVisiblePosition = linearManager.findLastVisibleItemPosition(); + } + } + } + + /** + * Gets the first visible position index + * + * @return + */ + public int getFirstVisiblePosition() { + return mFirstVisiblePosition; + } + + /** + * Gets the last visible position index + * + * @return + */ + public int getLastVisiblePosition() { + return mLastVisiblePosition; + } + + @Override + public void onScrolled(int dx, int dy) { + super.onScrolled(dx, dy); + if (onRecyclerViewPreloadListener != null) { + if (isEnabledLoadMore) { + LayoutManager layoutManager = getLayoutManager(); + if (layoutManager == null) { + throw new RuntimeException("LayoutManager is null,Please check it!"); + } + Adapter adapter = getAdapter(); + if (adapter == null) { + throw new RuntimeException("Adapter is null,Please check it!"); + } + boolean isReachBottom = false; + if (layoutManager instanceof GridLayoutManager) { + GridLayoutManager gridLayoutManager = (GridLayoutManager) layoutManager; + int rowCount = adapter.getItemCount() / gridLayoutManager.getSpanCount(); + int lastVisibleRowPosition = gridLayoutManager.findLastVisibleItemPosition() / gridLayoutManager.getSpanCount(); + isReachBottom = (lastVisibleRowPosition >= rowCount - reachBottomRow); + } + + if (!isReachBottom) { + isInTheBottom = false; + } else if (!isInTheBottom) { + onRecyclerViewPreloadListener.onRecyclerViewPreloadMore(); + if (dy > 0) { + isInTheBottom = true; + } + } else { + // 属于首次进入屏幕未滑动且内容未超过一屏,用于确保分页数设置过小导致内容不足二次上拉加载... + if (dy == 0) { + isInTheBottom = false; + } + } + } + } + } + + + private OnRecyclerViewPreloadMoreListener onRecyclerViewPreloadListener; + + public void setOnRecyclerViewPreloadListener(OnRecyclerViewPreloadMoreListener onRecyclerViewPreloadListener) { + this.onRecyclerViewPreloadListener = onRecyclerViewPreloadListener; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/SquareRelativeLayout.java b/picture_library/src/main/java/com/luck/picture/lib/widget/SquareRelativeLayout.java new file mode 100644 index 0000000..b9c1561 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/SquareRelativeLayout.java @@ -0,0 +1,39 @@ +package com.luck.picture.lib.widget; + +import android.annotation.TargetApi; +import android.content.Context; +import android.os.Build; +import android.util.AttributeSet; +import android.widget.RelativeLayout; + +/** + * @author:luck + * @date:2016-12-31 22:02 + * @describe:SquareRelativeLayout + */ +public class SquareRelativeLayout extends RelativeLayout { + + public SquareRelativeLayout(Context context) { + super(context); + } + + public SquareRelativeLayout(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + } + + @TargetApi(Build.VERSION_CODES.LOLLIPOP) + public SquareRelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { + super(context, attrs, defStyleAttr, defStyleRes); + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + // Set a square layout. + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/CompatDecoderFactory.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/CompatDecoderFactory.java new file mode 100644 index 0000000..f848d6b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/CompatDecoderFactory.java @@ -0,0 +1,21 @@ +package com.luck.picture.lib.widget.longimage; + + +import androidx.annotation.NonNull; + +/** + * Compatibility factory to instantiate decoders with empty public constructors. + * @param The base type of the decoder this factory will produce. + */ +public class CompatDecoderFactory implements DecoderFactory { + private Class clazz; + + public CompatDecoderFactory(@NonNull Class clazz) { + this.clazz = clazz; + } + + @Override + public T make() throws IllegalAccessException, InstantiationException { + return clazz.newInstance(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/DecoderFactory.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/DecoderFactory.java new file mode 100644 index 0000000..679619f --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/DecoderFactory.java @@ -0,0 +1,13 @@ +package com.luck.picture.lib.widget.longimage; + +/** + * Interface for decoder (and region decoder) factories. + * @param the class of decoder that will be produced. + */ +public interface DecoderFactory { + /** + * Produce a new instance of a decoder with type {@link T}. + * @return a new instance of your decoder. + */ + T make() throws IllegalAccessException, InstantiationException; +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageDecoder.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageDecoder.java new file mode 100644 index 0000000..55b46bb --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageDecoder.java @@ -0,0 +1,26 @@ +package com.luck.picture.lib.widget.longimage; + +import android.content.Context; +import android.graphics.Bitmap; +import android.net.Uri; + +/** + * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder} + * based on the Skia library to be replaced with a custom class. + */ +public interface ImageDecoder { + + /** + * Decode an image. When possible, initial setup work once in this method. This method + * must return the dimensions of the image. The URI can be in one of the following formats: + * File: file:///scard/picture.jpg + * Asset: file:///android_asset/picture.png + * Resource: android.resource://com.example.app/drawable/picture + * @param context Application context. A reference may be held, but must be cleared on recycle. + * @param uri URI of the image. + * @return Dimensions of the image. + * @throws Exception if initialisation fails. + */ + Bitmap decode(Context context, Uri uri) throws Exception; + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageRegionDecoder.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageRegionDecoder.java new file mode 100644 index 0000000..aa58d5b --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageRegionDecoder.java @@ -0,0 +1,50 @@ +package com.luck.picture.lib.widget.longimage; + +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.Point; +import android.graphics.Rect; +import android.net.Uri; + +/** + * Interface for image decoding classes, allowing the default {@link android.graphics.BitmapRegionDecoder} + * based on the Skia library to be replaced with a custom class. + */ +public interface ImageRegionDecoder { + + /** + * Initialise the decoder. When possible, initial setup work once in this method. This method + * must return the dimensions of the image. The URI can be in one of the following formats: + * File: file:///scard/picture.jpg + * Asset: file:///android_asset/picture.png + * Resource: android.resource://com.example.app/drawable/picture + * @param context Application context. A reference may be held, but must be cleared on recycle. + * @param uri URI of the image. + * @return Dimensions of the image. + * @throws Exception if initialisation fails. + */ + Point init(Context context, Uri uri) throws Exception; + + /** + * Decode a region of the image with the given sample size. This method is called off the UI thread so it can safely + * load the image on the current thread. It is called from an {@link android.os.AsyncTask} running in a single + * threaded executor, and while a synchronization lock is held on this object, so will never be called concurrently + * even if the decoder implementation supports it. + * @param sRect Source image rectangle to decode. + * @param sampleSize Sample size. + * @return The decoded region. It is safe to return null if decoding fails. + */ + Bitmap decodeRegion(Rect sRect, int sampleSize); + + /** + * Status check. Should return false before initialisation and after recycle. + * @return true if the decoder is ready to be used. + */ + boolean isReady(); + + /** + * This method will be called when the decoder is no longer required. It should clean up any resources still in use. + */ + void recycle(); + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageSource.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageSource.java new file mode 100644 index 0000000..25d326a --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageSource.java @@ -0,0 +1,233 @@ +package com.luck.picture.lib.widget.longimage; + +import android.graphics.Bitmap; +import android.graphics.Rect; +import android.net.Uri; + +import java.io.File; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +/** + * Helper class used to set the source and additional attributes from a variety of sources. Supports + * use of a bitmap, asset, resource, external file or any other URI. + * + * When you are using a preview image, you must set the dimensions of the full size image on the + * ImageSource object for the full size image using the {@link #dimensions(int, int)} method. + */ +public final class ImageSource { + + static final String FILE_SCHEME = "file:///"; + static final String ASSET_SCHEME = "file:///android_asset/"; + + private final Uri uri; + private final Bitmap bitmap; + private final Integer resource; + private boolean tile; + private int sWidth; + private int sHeight; + private Rect sRegion; + private boolean cached; + + private ImageSource(Bitmap bitmap, boolean cached) { + this.bitmap = bitmap; + this.uri = null; + this.resource = null; + this.tile = false; + this.sWidth = bitmap.getWidth(); + this.sHeight = bitmap.getHeight(); + this.cached = cached; + } + + private ImageSource(Uri uri) { + // #114 If file doesn't exist, attempt to url decode the URI and try again + String uriString = uri.toString(); + if (uriString.startsWith(FILE_SCHEME)) { + File uriFile = new File(uriString.substring(FILE_SCHEME.length() - 1)); + if (!uriFile.exists()) { + try { + uri = Uri.parse(URLDecoder.decode(uriString, "UTF-8")); + } catch (UnsupportedEncodingException e) { + // Fallback to encoded URI. This exception is not expected. + } + } + } + this.bitmap = null; + this.uri = uri; + this.resource = null; + this.tile = true; + } + + private ImageSource(int resource) { + this.bitmap = null; + this.uri = null; + this.resource = resource; + this.tile = true; + } + + /** + * Create an instance from a resource. The correct resource for the device screen resolution will be used. + * @param resId resource ID. + */ + public static ImageSource resource(int resId) { + return new ImageSource(resId); + } + + /** + * Create an instance from an asset name. + * @param assetName asset name. + */ + public static ImageSource asset(String assetName) { + if (assetName == null) { + throw new NullPointerException("Asset name must not be null"); + } + return uri(ASSET_SCHEME + assetName); + } + + /** + * Create an instance from a URI. If the URI does not start with a scheme, it's assumed to be the URI + * of a file. + * @param uri image URI. + */ + public static ImageSource uri(String uri) { + if (uri == null) { + throw new NullPointerException("Uri must not be null"); + } + if (!uri.contains("://")) { + if (uri.startsWith("/")) { + uri = uri.substring(1); + } + uri = FILE_SCHEME + uri; + } + return new ImageSource(Uri.parse(uri)); + } + + /** + * Create an instance from a URI. + * @param uri image URI. + */ + public static ImageSource uri(Uri uri) { + if (uri == null) { + throw new NullPointerException("Uri must not be null"); + } + return new ImageSource(uri); + } + + /** + * Provide a loaded bitmap for display. + * @param bitmap bitmap to be displayed. + */ + public static ImageSource bitmap(Bitmap bitmap) { + if (bitmap == null) { + throw new NullPointerException("Bitmap must not be null"); + } + return new ImageSource(bitmap, false); + } + + /** + * Provide a loaded and cached bitmap for display. This bitmap will not be recycled when it is no + * longer needed. Use this method if you loaded the bitmap with an image loader such as Picasso + * or Volley. + * @param bitmap bitmap to be displayed. + */ + public static ImageSource cachedBitmap(Bitmap bitmap) { + if (bitmap == null) { + throw new NullPointerException("Bitmap must not be null"); + } + return new ImageSource(bitmap, true); + } + + /** + * Enable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap., + * and tiling cannot be disabled when displaying a region of the source image. + * @return this instance for chaining. + */ + public ImageSource tilingEnabled() { + return tiling(true); + } + + /** + * Disable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap, + * and tiling cannot be disabled when displaying a region of the source image. + * @return this instance for chaining. + */ + public ImageSource tilingDisabled() { + return tiling(false); + } + + /** + * Enable or disable tiling of the image. This does not apply to preview images which are always loaded as a single bitmap, + * and tiling cannot be disabled when displaying a region of the source image. + * @return this instance for chaining. + */ + public ImageSource tiling(boolean tile) { + this.tile = tile; + return this; + } + + /** + * Use a region of the source image. Region must be set independently for the full size image and the preview if + * you are using one. + * @return this instance for chaining. + */ + public ImageSource region(Rect sRegion) { + this.sRegion = sRegion; + setInvariants(); + return this; + } + + /** + * Declare the dimensions of the image. This is only required for a full size image, when you are specifying a URI + * and also a preview image. When displaying a bitmap object, or not using a preview, you do not need to declare + * the image dimensions. Note if the declared dimensions are found to be incorrect, the view will reset. + * @return this instance for chaining. + */ + public ImageSource dimensions(int sWidth, int sHeight) { + if (bitmap == null) { + this.sWidth = sWidth; + this.sHeight = sHeight; + } + setInvariants(); + return this; + } + + private void setInvariants() { + if (this.sRegion != null) { + this.tile = true; + this.sWidth = this.sRegion.width(); + this.sHeight = this.sRegion.height(); + } + } + + protected final Uri getUri() { + return uri; + } + + protected final Bitmap getBitmap() { + return bitmap; + } + + protected final Integer getResource() { + return resource; + } + + protected final boolean getTile() { + return tile; + } + + protected final int getSWidth() { + return sWidth; + } + + protected final int getSHeight() { + return sHeight; + } + + protected final Rect getSRegion() { + return sRegion; + } + + protected final boolean isCached() { + return cached; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageViewState.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageViewState.java new file mode 100644 index 0000000..a022000 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/ImageViewState.java @@ -0,0 +1,55 @@ +/* +Copyright 2014 David Morrissey + +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.luck.picture.lib.widget.longimage; + +import android.graphics.PointF; + +import java.io.Serializable; + +/** + * Wraps the scale, center and orientation of a displayed image for easy restoration on screen rotate. + */ +public class ImageViewState implements Serializable { + + private float scale; + + private float centerX; + + private float centerY; + + private int orientation; + + public ImageViewState(float scale, PointF center, int orientation) { + this.scale = scale; + this.centerX = center.x; + this.centerY = center.y; + this.orientation = orientation; + } + + public float getScale() { + return scale; + } + + public PointF getCenter() { + return new PointF(centerX, centerY); + } + + public int getOrientation() { + return orientation; + } + +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageDecoder.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageDecoder.java new file mode 100644 index 0000000..aece7a3 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageDecoder.java @@ -0,0 +1,79 @@ +package com.luck.picture.lib.widget.longimage; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.net.Uri; +import android.text.TextUtils; + +import java.io.InputStream; +import java.util.List; + +/** + * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageDecoder} + * using Android's {@link BitmapFactory}, based on the Skia library. This + * works well in most circumstances and has reasonable performance, however it has some problems + * with grayscale, indexed and CMYK images. + */ +public class SkiaImageDecoder implements ImageDecoder { + + private static final String FILE_PREFIX = "file://"; + private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; + private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; + + @Override + public Bitmap decode(Context context, Uri uri) throws Exception { + String uriString = uri.toString(); + BitmapFactory.Options options = new BitmapFactory.Options(); + Bitmap bitmap; + options.inPreferredConfig = Bitmap.Config.RGB_565; + if (uriString.startsWith(RESOURCE_PREFIX)) { + Resources res; + String packageName = uri.getAuthority(); + if (context.getPackageName().equals(packageName)) { + res = context.getResources(); + } else { + PackageManager pm = context.getPackageManager(); + res = pm.getResourcesForApplication(packageName); + } + + int id = 0; + List segments = uri.getPathSegments(); + int size = segments.size(); + if (size == 2 && segments.get(0).equals("drawable")) { + String resName = segments.get(1); + id = res.getIdentifier(resName, "drawable", packageName); + } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) { + try { + id = Integer.parseInt(segments.get(0)); + } catch (NumberFormatException ignored) { + } + } + + bitmap = BitmapFactory.decodeResource(context.getResources(), id, options); + } else if (uriString.startsWith(ASSET_PREFIX)) { + String assetName = uriString.substring(ASSET_PREFIX.length()); + bitmap = BitmapFactory.decodeStream(context.getAssets().open(assetName), null, options); + } else if (uriString.startsWith(FILE_PREFIX)) { + bitmap = BitmapFactory.decodeFile(uriString.substring(FILE_PREFIX.length()), options); + } else { + InputStream inputStream = null; + try { + ContentResolver contentResolver = context.getContentResolver(); + inputStream = contentResolver.openInputStream(uri); + bitmap = BitmapFactory.decodeStream(inputStream, null, options); + } finally { + if (inputStream != null) { + try { inputStream.close(); } catch (Exception e) { } + } + } + } + if (bitmap == null) { + throw new RuntimeException("Skia image region decoder returned null bitmap - image format may not be supported"); + } + return bitmap; + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageRegionDecoder.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageRegionDecoder.java new file mode 100644 index 0000000..cf934e4 --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SkiaImageRegionDecoder.java @@ -0,0 +1,105 @@ +package com.luck.picture.lib.widget.longimage; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.pm.PackageManager; +import android.content.res.AssetManager; +import android.content.res.Resources; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.BitmapFactory; +import android.graphics.BitmapRegionDecoder; +import android.graphics.Point; +import android.graphics.Rect; +import android.net.Uri; +import android.text.TextUtils; + +import java.io.InputStream; +import java.util.List; + +/** + * Default implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder} + * using Android's {@link BitmapRegionDecoder}, based on the Skia library. This + * works well in most circumstances and has reasonable performance due to the cached decoder instance, + * however it has some problems with grayscale, indexed and CMYK images. + */ +public class SkiaImageRegionDecoder implements ImageRegionDecoder { + + private BitmapRegionDecoder decoder; + private final Object decoderLock = new Object(); + + private static final String FILE_PREFIX = "file://"; + private static final String ASSET_PREFIX = FILE_PREFIX + "/android_asset/"; + private static final String RESOURCE_PREFIX = ContentResolver.SCHEME_ANDROID_RESOURCE + "://"; + + @Override + public Point init(Context context, Uri uri) throws Exception { + String uriString = uri.toString(); + if (uriString.startsWith(RESOURCE_PREFIX)) { + Resources res; + String packageName = uri.getAuthority(); + if (context.getPackageName().equals(packageName)) { + res = context.getResources(); + } else { + PackageManager pm = context.getPackageManager(); + res = pm.getResourcesForApplication(packageName); + } + + int id = 0; + List segments = uri.getPathSegments(); + int size = segments.size(); + if (size == 2 && segments.get(0).equals("drawable")) { + String resName = segments.get(1); + id = res.getIdentifier(resName, "drawable", packageName); + } else if (size == 1 && TextUtils.isDigitsOnly(segments.get(0))) { + try { + id = Integer.parseInt(segments.get(0)); + } catch (NumberFormatException ignored) { + } + } + + decoder = BitmapRegionDecoder.newInstance(context.getResources().openRawResource(id), false); + } else if (uriString.startsWith(ASSET_PREFIX)) { + String assetName = uriString.substring(ASSET_PREFIX.length()); + decoder = BitmapRegionDecoder.newInstance(context.getAssets().open(assetName, AssetManager.ACCESS_RANDOM), false); + } else if (uriString.startsWith(FILE_PREFIX)) { + decoder = BitmapRegionDecoder.newInstance(uriString.substring(FILE_PREFIX.length()), false); + } else { + InputStream inputStream = null; + try { + ContentResolver contentResolver = context.getContentResolver(); + inputStream = contentResolver.openInputStream(uri); + decoder = BitmapRegionDecoder.newInstance(inputStream, false); + } finally { + if (inputStream != null) { + try { inputStream.close(); } catch (Exception e) { } + } + } + } + return new Point(decoder.getWidth(), decoder.getHeight()); + } + + @Override + public Bitmap decodeRegion(Rect sRect, int sampleSize) { + synchronized (decoderLock) { + BitmapFactory.Options options = new BitmapFactory.Options(); + options.inSampleSize = sampleSize; + options.inPreferredConfig = Config.RGB_565; + Bitmap bitmap = decoder.decodeRegion(sRect, options); + if (bitmap == null) { + throw new RuntimeException("Skia image decoder returned null bitmap - image format may not be supported"); + } + return bitmap; + } + } + + @Override + public boolean isReady() { + return decoder != null && !decoder.isRecycled(); + } + + @Override + public void recycle() { + decoder.recycle(); + } +} diff --git a/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SubsamplingScaleImageView.java b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SubsamplingScaleImageView.java new file mode 100644 index 0000000..5399d5c --- /dev/null +++ b/picture_library/src/main/java/com/luck/picture/lib/widget/longimage/SubsamplingScaleImageView.java @@ -0,0 +1,3138 @@ +/* +Copyright 2013-2015 David Morrissey + +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.luck.picture.lib.widget.longimage; + +import android.content.ContentResolver; +import android.content.Context; +import android.content.res.TypedArray; +import android.database.Cursor; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Matrix; +import android.graphics.Paint; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.RectF; +import android.media.ExifInterface; +import android.net.Uri; +import android.os.AsyncTask; +import android.os.Build.VERSION; +import android.os.Handler; +import android.os.Message; +import android.provider.MediaStore; +import android.util.AttributeSet; +import android.util.DisplayMetrics; +import android.util.Log; +import android.util.TypedValue; +import android.view.GestureDetector; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewParent; + +import androidx.annotation.AnyThread; +import androidx.annotation.NonNull; + +import com.luck.picture.lib.R; + +import java.lang.ref.WeakReference; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * Displays an image subsampled as necessary to avoid loading too much image data into memory. After a pinch to zoom in, + * a set of image tiles subsampled at higher resolution are loaded and displayed over the base layer. During pinch and + * zoom, tiles off screen or higher/lower resolution than required are discarded from memory. + *

+ * Tiles are no larger than the max supported bitmap size, so with large images tiling may be used even when zoomed out. + *

+ * v prefixes - coordinates, translations and distances measured in screen (view) pixels + * s prefixes - coordinates, translations and distances measured in source image pixels (scaled) + */ +@SuppressWarnings("unused") +public class SubsamplingScaleImageView extends View { + + private static final String TAG = SubsamplingScaleImageView.class.getSimpleName(); + + /** + * Attempt to use EXIF information on the image to rotate it. Works for external files only. + */ + public static final int ORIENTATION_USE_EXIF = -1; + /** + * Display the image file in its native orientation. + */ + public static final int ORIENTATION_0 = 0; + /** + * Rotate the image 90 degrees clockwise. + */ + public static final int ORIENTATION_90 = 90; + /** + * Rotate the image 180 degrees. + */ + public static final int ORIENTATION_180 = 180; + /** + * Rotate the image 270 degrees clockwise. + */ + public static final int ORIENTATION_270 = 270; + + private static final List VALID_ORIENTATIONS = Arrays.asList(ORIENTATION_0, ORIENTATION_90, ORIENTATION_180, ORIENTATION_270, ORIENTATION_USE_EXIF); + + /** + * During zoom animation, keep the point of the image that was tapped in the same place, and scale the image around it. + */ + public static final int ZOOM_FOCUS_FIXED = 1; + /** + * During zoom animation, move the point of the image that was tapped to the center of the screen. + */ + public static final int ZOOM_FOCUS_CENTER = 2; + /** + * Zoom in to and center the tapped point immediately without animating. + */ + public static final int ZOOM_FOCUS_CENTER_IMMEDIATE = 3; + + private static final List VALID_ZOOM_STYLES = Arrays.asList(ZOOM_FOCUS_FIXED, ZOOM_FOCUS_CENTER, ZOOM_FOCUS_CENTER_IMMEDIATE); + + /** + * Quadratic ease out. Not recommended for scale animation, but good for panning. + */ + public static final int EASE_OUT_QUAD = 1; + /** + * Quadratic ease in and out. + */ + public static final int EASE_IN_OUT_QUAD = 2; + + private static final List VALID_EASING_STYLES = Arrays.asList(EASE_IN_OUT_QUAD, EASE_OUT_QUAD); + + /** + * Don't allow the image to be panned off screen. As much of the image as possible is always displayed, centered in the view when it is smaller. This is the best option for galleries. + */ + public static final int PAN_LIMIT_INSIDE = 1; + /** + * Allows the image to be panned until it is just off screen, but no further. The edge of the image will stop when it is flush with the screen edge. + */ + public static final int PAN_LIMIT_OUTSIDE = 2; + /** + * Allows the image to be panned until a corner reaches the center of the screen but no further. Useful when you want to pan any spot on the image to the exact center of the screen. + */ + public static final int PAN_LIMIT_CENTER = 3; + + private static final List VALID_PAN_LIMITS = Arrays.asList(PAN_LIMIT_INSIDE, PAN_LIMIT_OUTSIDE, PAN_LIMIT_CENTER); + + /** + * Scale the image so that both dimensions of the image will be equal to or less than the corresponding dimension of the view. The image is then centered in the view. This is the default behaviour and best for galleries. + */ + public static final int SCALE_TYPE_CENTER_INSIDE = 1; + /** + * Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view. + */ + public static final int SCALE_TYPE_CENTER_CROP = 2; + /** + * Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view. + */ + public static final int SCALE_TYPE_CUSTOM = 3; + + private static final List VALID_SCALE_TYPES = Arrays.asList(SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE, SCALE_TYPE_CUSTOM); + + /** + * State change originated from animation. + */ + public static final int ORIGIN_ANIM = 1; + /** + * State change originated from touch gesture. + */ + public static final int ORIGIN_TOUCH = 2; + /** + * State change originated from a fling momentum anim. + */ + public static final int ORIGIN_FLING = 3; + /** + * State change originated from a double tap zoom anim. + */ + public static final int ORIGIN_DOUBLE_TAP_ZOOM = 4; + + // Bitmap (preview or full image) + private Bitmap bitmap; + + // Whether the bitmap is a preview image + private boolean bitmapIsPreview; + + // Specifies if a cache handler is also referencing the bitmap. Do not recycle if so. + private boolean bitmapIsCached; + + // Uri of full size image + private Uri uri; + + // Sample size used to display the whole image when fully zoomed out + private int fullImageSampleSize; + + // Map of zoom level to tile grid + private Map> tileMap; + + // Overlay tile boundaries and other info + private boolean debug; + + // Image orientation setting + private int orientation = ORIENTATION_0; + + // Max scale allowed (prevent infinite zoom) + private float maxScale = 2F; + + // Min scale allowed (prevent infinite zoom) + private float minScale = minScale(); + + // Density to reach before loading higher resolution tiles + private int minimumTileDpi = -1; + + // Pan limiting style + private int panLimit = PAN_LIMIT_INSIDE; + + // Minimum scale type + private int minimumScaleType = SCALE_TYPE_CENTER_INSIDE; + + // overrides for the dimensions of the generated tiles + public static int TILE_SIZE_AUTO = Integer.MAX_VALUE; + private int maxTileWidth = TILE_SIZE_AUTO; + private int maxTileHeight = TILE_SIZE_AUTO; + + // Whether to use the thread pool executor to load tiles + private boolean parallelLoadingEnabled; + + // Gesture detection settings + private boolean panEnabled = true; + private boolean zoomEnabled = true; + private boolean quickScaleEnabled = true; + + // Double tap zoom behaviour + private float doubleTapZoomScale = 1F; + private int doubleTapZoomStyle = ZOOM_FOCUS_FIXED; + private int doubleTapZoomDuration = 500; + + // Current scale and scale at start of zoom + private float scale; + private float scaleStart; + + // Screen coordinate of top-left corner of source image + private PointF vTranslate; + private PointF vTranslateStart; + private PointF vTranslateBefore; + + // Source coordinate to center on, used when new position is set externally before view is ready + private Float pendingScale; + private PointF sPendingCenter; + private PointF sRequestedCenter; + + // Source image dimensions and orientation - dimensions relate to the unrotated image + private int sWidth; + private int sHeight; + private int sOrientation; + private Rect sRegion; + private Rect pRegion; + + // Is two-finger zooming in progress + private boolean isZooming; + // Is one-finger panning in progress + private boolean isPanning; + // Is quick-scale gesture in progress + private boolean isQuickScaling; + // Max touches used in current gesture + private int maxTouchCount; + + // Fling detector + private GestureDetector detector; + + // Tile and image decoding + private ImageRegionDecoder decoder; + private final Object decoderLock = new Object(); + private DecoderFactory bitmapDecoderFactory = new CompatDecoderFactory(SkiaImageDecoder.class); + private DecoderFactory regionDecoderFactory = new CompatDecoderFactory(SkiaImageRegionDecoder.class); + + // Debug values + private PointF vCenterStart; + private float vDistStart; + + // Current quickscale state + private final float quickScaleThreshold; + private float quickScaleLastDistance; + private boolean quickScaleMoved; + private PointF quickScaleVLastPoint; + private PointF quickScaleSCenter; + private PointF quickScaleVStart; + + // Scale and center animation tracking + private Anim anim; + + // Whether a ready notification has been sent to subclasses + private boolean readySent; + // Whether a base layer loaded notification has been sent to subclasses + private boolean imageLoadedSent; + + // Event listener + private OnImageEventListener onImageEventListener; + + // Scale and center listener + private OnStateChangedListener onStateChangedListener; + + // Long click listener + private OnLongClickListener onLongClickListener; + + // Long click handler + private Handler handler; + private static final int MESSAGE_LONG_CLICK = 1; + + // Paint objects created once and reused for efficiency + private Paint bitmapPaint; + private Paint debugPaint; + private Paint tileBgPaint; + + // Volatile fields used to reduce object creation + private ScaleAndTranslate satTemp; + private Matrix matrix; + private RectF sRect; + private float[] srcArray = new float[8]; + private float[] dstArray = new float[8]; + + //The logical density of the display + private float density; + + + public SubsamplingScaleImageView(Context context, AttributeSet attr) { + super(context, attr); + density = getResources().getDisplayMetrics().density; + setMinimumDpi(160); + setDoubleTapZoomDpi(160); + setGestureDetector(context); + this.handler = new Handler(new Handler.Callback() { + public boolean handleMessage(Message message) { + if (message.what == MESSAGE_LONG_CLICK && onLongClickListener != null) { + maxTouchCount = 0; + SubsamplingScaleImageView.super.setOnLongClickListener(onLongClickListener); + performLongClick(); + SubsamplingScaleImageView.super.setOnLongClickListener(null); + } + return true; + } + }); + // Handle XML attributes + if (attr != null) { + TypedArray typedAttr = getContext().obtainStyledAttributes(attr, R.styleable.PictureLongScaleImageView); + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_assetName)) { + String assetName = typedAttr.getString(R.styleable.PictureLongScaleImageView_assetName); + if (assetName != null && assetName.length() > 0) { + setImage(ImageSource.asset(assetName).tilingEnabled()); + } + } + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_src)) { + int resId = typedAttr.getResourceId(R.styleable.PictureLongScaleImageView_src, 0); + if (resId > 0) { + setImage(ImageSource.resource(resId).tilingEnabled()); + } + } + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_panEnabled)) { + setPanEnabled(typedAttr.getBoolean(R.styleable.PictureLongScaleImageView_panEnabled, true)); + } + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_zoomEnabled)) { + setZoomEnabled(typedAttr.getBoolean(R.styleable.PictureLongScaleImageView_zoomEnabled, true)); + } + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_quickScaleEnabled)) { + setQuickScaleEnabled(typedAttr.getBoolean(R.styleable.PictureLongScaleImageView_quickScaleEnabled, true)); + } + if (typedAttr.hasValue(R.styleable.PictureLongScaleImageView_tileBackgroundColor)) { + setTileBackgroundColor(typedAttr.getColor(R.styleable.PictureLongScaleImageView_tileBackgroundColor, Color.argb(0, 0, 0, 0))); + } + typedAttr.recycle(); + } + + quickScaleThreshold = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 20, context.getResources().getDisplayMetrics()); + } + + public SubsamplingScaleImageView(Context context) { + this(context, null); + } + + /** + * Sets the image orientation. It's best to call this before setting the image file or asset, because it may waste + * loading of tiles. However, this can be freely called at any time. + */ + public final void setOrientation(int orientation) { + if (!VALID_ORIENTATIONS.contains(orientation)) { + throw new IllegalArgumentException("Invalid orientation: " + orientation); + } + this.orientation = orientation; + reset(false); + invalidate(); + requestLayout(); + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI. + * + * @param imageSource Image source. + */ + public final void setImage(ImageSource imageSource) { + setImage(imageSource, null, null); + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI, starting with a given orientation + * setting, scale and center. This is the best method to use when you want scale and center to be restored + * after screen orientation change; it avoids any redundant loading of tiles in the wrong orientation. + * + * @param imageSource Image source. + * @param state State to be restored. Nullable. + */ + public final void setImage(ImageSource imageSource, ImageViewState state) { + setImage(imageSource, null, state); + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be + * displayed until the full size image is loaded. + *

+ * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)} + * on the imageSource object. The preview source will be ignored if you don't provide dimensions, + * and if you provide a bitmap for the full size image. + * + * @param imageSource Image source. Dimensions must be declared. + * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads. + */ + public final void setImage(ImageSource imageSource, ImageSource previewSource) { + setImage(imageSource, previewSource, null); + } + + /** + * Set the image source from a bitmap, resource, asset, file or other URI, providing a preview image to be + * displayed until the full size image is loaded, starting with a given orientation setting, scale and center. + * This is the best method to use when you want scale and center to be restored after screen orientation change; + * it avoids any redundant loading of tiles in the wrong orientation. + *

+ * You must declare the dimensions of the full size image by calling {@link ImageSource#dimensions(int, int)} + * on the imageSource object. The preview source will be ignored if you don't provide dimensions, + * and if you provide a bitmap for the full size image. + * + * @param imageSource Image source. Dimensions must be declared. + * @param previewSource Optional source for a preview image to be displayed and allow interaction while the full size image loads. + * @param state State to be restored. Nullable. + */ + public final void setImage(ImageSource imageSource, ImageSource previewSource, ImageViewState state) { + if (imageSource == null) { + throw new NullPointerException("imageSource must not be null"); + } + + reset(true); + if (state != null) { + restoreState(state); + } + + if (previewSource != null) { + if (imageSource.getBitmap() != null) { + throw new IllegalArgumentException("Preview image cannot be used when a bitmap is provided for the main image"); + } + if (imageSource.getSWidth() <= 0 || imageSource.getSHeight() <= 0) { + throw new IllegalArgumentException("Preview image cannot be used unless dimensions are provided for the main image"); + } + this.sWidth = imageSource.getSWidth(); + this.sHeight = imageSource.getSHeight(); + this.pRegion = previewSource.getSRegion(); + if (previewSource.getBitmap() != null) { + this.bitmapIsCached = previewSource.isCached(); + onPreviewLoaded(previewSource.getBitmap()); + } else { + Uri uri = previewSource.getUri(); + if (uri == null && previewSource.getResource() != null) { + uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + previewSource.getResource()); + } + BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, true); + execute(task); + } + } + + if (imageSource.getBitmap() != null && imageSource.getSRegion() != null) { + onImageLoaded(Bitmap.createBitmap(imageSource.getBitmap(), imageSource.getSRegion().left, imageSource.getSRegion().top, imageSource.getSRegion().width(), imageSource.getSRegion().height()), ORIENTATION_0, false); + } else if (imageSource.getBitmap() != null) { + onImageLoaded(imageSource.getBitmap(), ORIENTATION_0, imageSource.isCached()); + } else { + sRegion = imageSource.getSRegion(); + uri = imageSource.getUri(); + if (uri == null && imageSource.getResource() != null) { + uri = Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + getContext().getPackageName() + "/" + imageSource.getResource()); + } + if (imageSource.getTile() || sRegion != null) { + // Load the bitmap using tile decoding. + TilesInitTask task = new TilesInitTask(this, getContext(), regionDecoderFactory, uri); + execute(task); + } else { + // Load the bitmap as a single image. + BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false); + execute(task); + } + } + } + + /** + * Reset all state before setting/changing image or setting new rotation. + */ + private void reset(boolean newImage) { + debug("reset newImage=" + newImage); + scale = 0f; + scaleStart = 0f; + vTranslate = null; + vTranslateStart = null; + vTranslateBefore = null; + pendingScale = 0f; + sPendingCenter = null; + sRequestedCenter = null; + isZooming = false; + isPanning = false; + isQuickScaling = false; + maxTouchCount = 0; + fullImageSampleSize = 0; + vCenterStart = null; + vDistStart = 0; + quickScaleLastDistance = 0f; + quickScaleMoved = false; + quickScaleSCenter = null; + quickScaleVLastPoint = null; + quickScaleVStart = null; + anim = null; + satTemp = null; + matrix = null; + sRect = null; + if (newImage) { + uri = null; + if (decoder != null) { + synchronized (decoderLock) { + decoder.recycle(); + decoder = null; + } + } + if (bitmap != null && !bitmapIsCached) { + bitmap.recycle(); + } + if (bitmap != null && bitmapIsCached && onImageEventListener != null) { + onImageEventListener.onPreviewReleased(); + } + sWidth = 0; + sHeight = 0; + sOrientation = 0; + sRegion = null; + pRegion = null; + readySent = false; + imageLoadedSent = false; + bitmap = null; + bitmapIsPreview = false; + bitmapIsCached = false; + } + if (tileMap != null) { + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + for (Tile tile : tileMapEntry.getValue()) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + } + tileMap = null; + } + setGestureDetector(getContext()); + } + + private void setGestureDetector(final Context context) { + this.detector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener() { + + @Override + public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { + if (panEnabled && readySent && vTranslate != null && e1 != null && e2 != null && (Math.abs(e1.getX() - e2.getX()) > 50 || Math.abs(e1.getY() - e2.getY()) > 50) && (Math.abs(velocityX) > 500 || Math.abs(velocityY) > 500) && !isZooming) { + PointF vTranslateEnd = new PointF(vTranslate.x + (velocityX * 0.25f), vTranslate.y + (velocityY * 0.25f)); + float sCenterXEnd = ((getWidth() / 2) - vTranslateEnd.x) / scale; + float sCenterYEnd = ((getHeight() / 2) - vTranslateEnd.y) / scale; + new AnimationBuilder(new PointF(sCenterXEnd, sCenterYEnd)).withEasing(EASE_OUT_QUAD).withPanLimited(false).withOrigin(ORIGIN_FLING).start(); + return true; + } + return super.onFling(e1, e2, velocityX, velocityY); + } + + @Override + public boolean onSingleTapConfirmed(MotionEvent e) { + performClick(); + return true; + } + + @Override + public boolean onDoubleTap(MotionEvent e) { + if (zoomEnabled && readySent && vTranslate != null) { + // Hacky solution for #15 - after a double tap the GestureDetector gets in a state + // where the next fling is ignored, so here we replace it with a new one. + setGestureDetector(context); + if (quickScaleEnabled) { + // Store quick scale params. This will become either a double tap zoom or a + // quick scale depending on whether the user swipes. + vCenterStart = new PointF(e.getX(), e.getY()); + vTranslateStart = new PointF(vTranslate.x, vTranslate.y); + scaleStart = scale; + isQuickScaling = true; + isZooming = true; + quickScaleLastDistance = -1F; + quickScaleSCenter = viewToSourceCoord(vCenterStart); + quickScaleVStart = new PointF(e.getX(), e.getY()); + quickScaleVLastPoint = new PointF(quickScaleSCenter.x, quickScaleSCenter.y); + quickScaleMoved = false; + // We need to get events in onTouchEvent after this. + return false; + } else { + // Start double tap zoom animation. + doubleTapZoom(viewToSourceCoord(new PointF(e.getX(), e.getY())), new PointF(e.getX(), e.getY())); + return true; + } + } + return super.onDoubleTapEvent(e); + } + }); + } + + /** + * On resize, preserve center and scale. Various behaviours are possible, override this method to use another. + */ + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + debug("onSizeChanged %dx%d -> %dx%d", oldw, oldh, w, h); + PointF sCenter = getCenter(); + if (readySent && sCenter != null) { + this.anim = null; + this.pendingScale = scale; + this.sPendingCenter = sCenter; + } + } + + /** + * Measures the width and height of the view, preserving the aspect ratio of the image displayed if wrap_content is + * used. The image will scale within this box, not resizing the view as it is zoomed. + */ + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec); + int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec); + int parentWidth = MeasureSpec.getSize(widthMeasureSpec); + int parentHeight = MeasureSpec.getSize(heightMeasureSpec); + boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY; + boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY; + int width = parentWidth; + int height = parentHeight; + if (sWidth > 0 && sHeight > 0) { + if (resizeWidth && resizeHeight) { + width = sWidth(); + height = sHeight(); + } else if (resizeHeight) { + height = (int) ((((double) sHeight() / (double) sWidth()) * width)); + } else if (resizeWidth) { + width = (int) ((((double) sWidth() / (double) sHeight()) * height)); + } + } + width = Math.max(width, getSuggestedMinimumWidth()); + height = Math.max(height, getSuggestedMinimumHeight()); + setMeasuredDimension(width, height); + } + + /** + * Handle touch events. One finger pans, and two finger pinch and zoom plus panning. + */ + @Override + public boolean onTouchEvent(@NonNull MotionEvent event) { + // During non-interruptible anims, ignore all touch events + if (anim != null && !anim.interruptible) { + requestDisallowInterceptTouchEvent(true); + return true; + } else { + if (anim != null && anim.listener != null) { + try { + anim.listener.onInterruptedByUser(); + } catch (Exception e) { + Log.w(TAG, "Error thrown by animation listener", e); + } + } + anim = null; + } + + // Abort if not ready + if (vTranslate == null) { + return true; + } + // Detect flings, taps and double taps + if (!isQuickScaling && (detector == null || detector.onTouchEvent(event))) { + isZooming = false; + isPanning = false; + maxTouchCount = 0; + return true; + } + + if (vTranslateStart == null) { + vTranslateStart = new PointF(0, 0); + } + if (vTranslateBefore == null) { + vTranslateBefore = new PointF(0, 0); + } + if (vCenterStart == null) { + vCenterStart = new PointF(0, 0); + } + + // Store current values so we can send an event if they change + float scaleBefore = scale; + vTranslateBefore.set(vTranslate); + + boolean handled = onTouchEventInternal(event); + sendStateChanged(scaleBefore, vTranslateBefore, ORIGIN_TOUCH); + return handled || super.onTouchEvent(event); + } + + @SuppressWarnings("deprecation") + private boolean onTouchEventInternal(@NonNull MotionEvent event) { + int touchCount = event.getPointerCount(); + switch (event.getAction()) { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_1_DOWN: + case MotionEvent.ACTION_POINTER_2_DOWN: + anim = null; + requestDisallowInterceptTouchEvent(true); + maxTouchCount = Math.max(maxTouchCount, touchCount); + if (touchCount >= 2) { + if (zoomEnabled) { + // Start pinch to zoom. Calculate distance between touch points and center point of the pinch. + float distance = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); + scaleStart = scale; + vDistStart = distance; + vTranslateStart.set(vTranslate.x, vTranslate.y); + vCenterStart.set((event.getX(0) + event.getX(1)) / 2, (event.getY(0) + event.getY(1)) / 2); + } else { + // Abort all gestures on second touch + maxTouchCount = 0; + } + // Cancel long click timer + handler.removeMessages(MESSAGE_LONG_CLICK); + } else if (!isQuickScaling) { + // Start one-finger pan + vTranslateStart.set(vTranslate.x, vTranslate.y); + vCenterStart.set(event.getX(), event.getY()); + + // Start long click timer + handler.sendEmptyMessageDelayed(MESSAGE_LONG_CLICK, 600); + } + return true; + case MotionEvent.ACTION_MOVE: + boolean consumed = false; + if (maxTouchCount > 0) { + if (touchCount >= 2) { + // Calculate new distance between touch points, to scale and pan relative to start values. + float vDistEnd = distance(event.getX(0), event.getX(1), event.getY(0), event.getY(1)); + float vCenterEndX = (event.getX(0) + event.getX(1)) / 2; + float vCenterEndY = (event.getY(0) + event.getY(1)) / 2; + + if (zoomEnabled && (distance(vCenterStart.x, vCenterEndX, vCenterStart.y, vCenterEndY) > 5 || Math.abs(vDistEnd - vDistStart) > 5 || isPanning)) { + isZooming = true; + isPanning = true; + consumed = true; + + double previousScale = scale; + scale = Math.min(maxScale, (vDistEnd / vDistStart) * scaleStart); + + if (scale <= minScale()) { + // Minimum scale reached so don't pan. Adjust start settings so any expand will zoom in. + vDistStart = vDistEnd; + scaleStart = minScale(); + vCenterStart.set(vCenterEndX, vCenterEndY); + vTranslateStart.set(vTranslate); + } else if (panEnabled) { + // Translate to place the source image coordinate that was at the center of the pinch at the start + // at the center of the pinch now, to give simultaneous pan + zoom. + float vLeftStart = vCenterStart.x - vTranslateStart.x; + float vTopStart = vCenterStart.y - vTranslateStart.y; + float vLeftNow = vLeftStart * (scale / scaleStart); + float vTopNow = vTopStart * (scale / scaleStart); + vTranslate.x = vCenterEndX - vLeftNow; + vTranslate.y = vCenterEndY - vTopNow; + if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) { + fitToBounds(true); + vCenterStart.set(vCenterEndX, vCenterEndY); + vTranslateStart.set(vTranslate); + scaleStart = scale; + vDistStart = vDistEnd; + } + } else if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + vTranslate.x = (getWidth() / 2) - (scale * sRequestedCenter.x); + vTranslate.y = (getHeight() / 2) - (scale * sRequestedCenter.y); + } else { + // With no requested center, scale around the image center. + vTranslate.x = (getWidth() / 2) - (scale * (sWidth() / 2)); + vTranslate.y = (getHeight() / 2) - (scale * (sHeight() / 2)); + } + + fitToBounds(true); + refreshRequiredTiles(false); + } + } else if (isQuickScaling) { + // One finger zoom + // Stole Google's Magical Formula™ to make sure it feels the exact same + float dist = Math.abs(quickScaleVStart.y - event.getY()) * 2 + quickScaleThreshold; + + if (quickScaleLastDistance == -1f) { + quickScaleLastDistance = dist; + } + boolean isUpwards = event.getY() > quickScaleVLastPoint.y; + quickScaleVLastPoint.set(0, event.getY()); + + float spanDiff = Math.abs(1 - (dist / quickScaleLastDistance)) * 0.5f; + + if (spanDiff > 0.03f || quickScaleMoved) { + quickScaleMoved = true; + + float multiplier = 1; + if (quickScaleLastDistance > 0) { + multiplier = isUpwards ? (1 + spanDiff) : (1 - spanDiff); + } + + double previousScale = scale; + scale = Math.max(minScale(), Math.min(maxScale, scale * multiplier)); + + if (panEnabled) { + float vLeftStart = vCenterStart.x - vTranslateStart.x; + float vTopStart = vCenterStart.y - vTranslateStart.y; + float vLeftNow = vLeftStart * (scale / scaleStart); + float vTopNow = vTopStart * (scale / scaleStart); + vTranslate.x = vCenterStart.x - vLeftNow; + vTranslate.y = vCenterStart.y - vTopNow; + if ((previousScale * sHeight() < getHeight() && scale * sHeight() >= getHeight()) || (previousScale * sWidth() < getWidth() && scale * sWidth() >= getWidth())) { + fitToBounds(true); + vCenterStart.set(sourceToViewCoord(quickScaleSCenter)); + vTranslateStart.set(vTranslate); + scaleStart = scale; + dist = 0; + } + } else if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + vTranslate.x = (getWidth() / 2) - (scale * sRequestedCenter.x); + vTranslate.y = (getHeight() / 2) - (scale * sRequestedCenter.y); + } else { + // With no requested center, scale around the image center. + vTranslate.x = (getWidth() / 2) - (scale * (sWidth() / 2)); + vTranslate.y = (getHeight() / 2) - (scale * (sHeight() / 2)); + } + } + + quickScaleLastDistance = dist; + + fitToBounds(true); + refreshRequiredTiles(false); + + consumed = true; + } else if (!isZooming) { + // One finger pan - translate the image. We do this calculation even with pan disabled so click + // and long click behaviour is preserved. + float dx = Math.abs(event.getX() - vCenterStart.x); + float dy = Math.abs(event.getY() - vCenterStart.y); + + //On the Samsung S6 long click event does not work, because the dx > 5 usually true + float offset = density * 5; + if (dx > offset || dy > offset || isPanning) { + consumed = true; + vTranslate.x = vTranslateStart.x + (event.getX() - vCenterStart.x); + vTranslate.y = vTranslateStart.y + (event.getY() - vCenterStart.y); + + float lastX = vTranslate.x; + float lastY = vTranslate.y; + fitToBounds(true); + boolean atXEdge = lastX != vTranslate.x; + boolean atYEdge = lastY != vTranslate.y; + boolean edgeXSwipe = atXEdge && dx > dy && !isPanning; + boolean edgeYSwipe = atYEdge && dy > dx && !isPanning; + boolean yPan = lastY == vTranslate.y && dy > offset * 3; + if (!edgeXSwipe && !edgeYSwipe && (!atXEdge || !atYEdge || yPan || isPanning)) { + isPanning = true; + } else if (dx > offset || dy > offset) { + // Haven't panned the image, and we're at the left or right edge. Switch to page swipe. + maxTouchCount = 0; + handler.removeMessages(MESSAGE_LONG_CLICK); + requestDisallowInterceptTouchEvent(false); + } + if (!panEnabled) { + vTranslate.x = vTranslateStart.x; + vTranslate.y = vTranslateStart.y; + requestDisallowInterceptTouchEvent(false); + } + + refreshRequiredTiles(false); + } + } + } + if (consumed) { + handler.removeMessages(MESSAGE_LONG_CLICK); + invalidate(); + return true; + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + case MotionEvent.ACTION_POINTER_2_UP: + handler.removeMessages(MESSAGE_LONG_CLICK); + if (isQuickScaling) { + isQuickScaling = false; + if (!quickScaleMoved) { + doubleTapZoom(quickScaleSCenter, vCenterStart); + } + } + if (maxTouchCount > 0 && (isZooming || isPanning)) { + if (isZooming && touchCount == 2) { + // Convert from zoom to pan with remaining touch + isPanning = true; + vTranslateStart.set(vTranslate.x, vTranslate.y); + if (event.getActionIndex() == 1) { + vCenterStart.set(event.getX(0), event.getY(0)); + } else { + vCenterStart.set(event.getX(1), event.getY(1)); + } + } + if (touchCount < 3) { + // End zooming when only one touch point + isZooming = false; + } + if (touchCount < 2) { + // End panning when no touch points + isPanning = false; + maxTouchCount = 0; + } + // Trigger load of tiles now required + refreshRequiredTiles(true); + return true; + } + if (touchCount == 1) { + isZooming = false; + isPanning = false; + maxTouchCount = 0; + } + return true; + } + return false; + } + + private void requestDisallowInterceptTouchEvent(boolean disallowIntercept) { + ViewParent parent = getParent(); + if (parent != null) { + parent.requestDisallowInterceptTouchEvent(disallowIntercept); + } + } + + /** + * Double tap zoom handler triggered from gesture detector or on touch, depending on whether + * quick scale is enabled. + */ + private void doubleTapZoom(PointF sCenter, PointF vFocus) { + if (!panEnabled) { + if (sRequestedCenter != null) { + // With a center specified from code, zoom around that point. + sCenter.x = sRequestedCenter.x; + sCenter.y = sRequestedCenter.y; + } else { + // With no requested center, scale around the image center. + sCenter.x = sWidth() / 2; + sCenter.y = sHeight() / 2; + } + } + float doubleTapZoomScale = Math.min(maxScale, SubsamplingScaleImageView.this.doubleTapZoomScale); + boolean zoomIn = scale <= doubleTapZoomScale * 0.9; + float targetScale = zoomIn ? doubleTapZoomScale : minScale(); + if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER_IMMEDIATE) { + setScaleAndCenter(targetScale, sCenter); + } else if (doubleTapZoomStyle == ZOOM_FOCUS_CENTER || !zoomIn || !panEnabled) { + new AnimationBuilder(targetScale, sCenter).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start(); + } else if (doubleTapZoomStyle == ZOOM_FOCUS_FIXED) { + new AnimationBuilder(targetScale, sCenter, vFocus).withInterruptible(false).withDuration(doubleTapZoomDuration).withOrigin(ORIGIN_DOUBLE_TAP_ZOOM).start(); + } + invalidate(); + } + + /** + * Draw method should not be called until the view has dimensions so the first calls are used as triggers to calculate + * the scaling and tiling required. Once the view is setup, tiles are displayed as they are loaded. + */ + @Override + protected void onDraw(Canvas canvas) { + super.onDraw(canvas); + createPaints(); + + // If image or view dimensions are not known yet, abort. + if (sWidth == 0 || sHeight == 0 || getWidth() == 0 || getHeight() == 0) { + return; + } + + // When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading. + if (tileMap == null && decoder != null) { + initialiseBaseLayer(getMaxBitmapDimensions(canvas)); + } + + // If image has been loaded or supplied as a bitmap, onDraw may be the first time the view has + // dimensions and therefore the first opportunity to set scale and translate. If this call returns + // false there is nothing to be drawn so return immediately. + if (!checkReady()) { + return; + } + + // Set scale and translate before draw. + preDraw(); + + // If animating scale, calculate current scale and center with easing equations + if (anim != null) { + // Store current values so we can send an event if they change + float scaleBefore = scale; + if (vTranslateBefore == null) { + vTranslateBefore = new PointF(0, 0); + } + vTranslateBefore.set(vTranslate); + + long scaleElapsed = System.currentTimeMillis() - anim.time; + boolean finished = scaleElapsed > anim.duration; + scaleElapsed = Math.min(scaleElapsed, anim.duration); + scale = ease(anim.easing, scaleElapsed, anim.scaleStart, anim.scaleEnd - anim.scaleStart, anim.duration); + + // Apply required animation to the focal point + float vFocusNowX = ease(anim.easing, scaleElapsed, anim.vFocusStart.x, anim.vFocusEnd.x - anim.vFocusStart.x, anim.duration); + float vFocusNowY = ease(anim.easing, scaleElapsed, anim.vFocusStart.y, anim.vFocusEnd.y - anim.vFocusStart.y, anim.duration); + // Find out where the focal point is at this scale and adjust its position to follow the animation path + vTranslate.x -= sourceToViewX(anim.sCenterEnd.x) - vFocusNowX; + vTranslate.y -= sourceToViewY(anim.sCenterEnd.y) - vFocusNowY; + + // For translate anims, showing the image non-centered is never allowed, for scaling anims it is during the animation. + fitToBounds(finished || (anim.scaleStart == anim.scaleEnd)); + sendStateChanged(scaleBefore, vTranslateBefore, anim.origin); + refreshRequiredTiles(finished); + if (finished) { + if (anim.listener != null) { + try { + anim.listener.onComplete(); + } catch (Exception e) { + Log.w(TAG, "Error thrown by animation listener", e); + } + } + anim = null; + } + invalidate(); + } + + if (tileMap != null && isBaseLayerReady()) { + + // Optimum sample size for current scale + int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale)); + + // First check for missing tiles - if there are any we need the base layer underneath to avoid gaps + boolean hasMissingTiles = false; + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + if (tileMapEntry.getKey() == sampleSize) { + for (Tile tile : tileMapEntry.getValue()) { + if (tile.visible && (tile.loading || tile.bitmap == null)) { + hasMissingTiles = true; + } + } + } + } + + // Render all loaded tiles. LinkedHashMap used for bottom up rendering - lower res tiles underneath. + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + if (tileMapEntry.getKey() == sampleSize || hasMissingTiles) { + for (Tile tile : tileMapEntry.getValue()) { + sourceToViewRect(tile.sRect, tile.vRect); + if (!tile.loading && tile.bitmap != null) { + if (tileBgPaint != null) { + canvas.drawRect(tile.vRect, tileBgPaint); + } + if (matrix == null) { + matrix = new Matrix(); + } + matrix.reset(); + setMatrixArray(srcArray, 0, 0, tile.bitmap.getWidth(), 0, tile.bitmap.getWidth(), tile.bitmap.getHeight(), 0, tile.bitmap.getHeight()); + if (getRequiredRotation() == ORIENTATION_0) { + setMatrixArray(dstArray, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom); + } else if (getRequiredRotation() == ORIENTATION_90) { + setMatrixArray(dstArray, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top); + } else if (getRequiredRotation() == ORIENTATION_180) { + setMatrixArray(dstArray, tile.vRect.right, tile.vRect.bottom, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top); + } else if (getRequiredRotation() == ORIENTATION_270) { + setMatrixArray(dstArray, tile.vRect.left, tile.vRect.bottom, tile.vRect.left, tile.vRect.top, tile.vRect.right, tile.vRect.top, tile.vRect.right, tile.vRect.bottom); + } + matrix.setPolyToPoly(srcArray, 0, dstArray, 0, 4); + canvas.drawBitmap(tile.bitmap, matrix, bitmapPaint); + if (debug) { + canvas.drawRect(tile.vRect, debugPaint); + } + } else if (tile.loading && debug) { + canvas.drawText("LOADING", tile.vRect.left + 5, tile.vRect.top + 35, debugPaint); + } + if (tile.visible && debug) { + canvas.drawText("ISS " + tile.sampleSize + " RECT " + tile.sRect.top + "," + tile.sRect.left + "," + tile.sRect.bottom + "," + tile.sRect.right, tile.vRect.left + 5, tile.vRect.top + 15, debugPaint); + } + } + } + } + + } else if (bitmap != null) { + + float xScale = scale, yScale = scale; + if (bitmapIsPreview) { + xScale = scale * ((float) sWidth / bitmap.getWidth()); + yScale = scale * ((float) sHeight / bitmap.getHeight()); + } + + if (matrix == null) { + matrix = new Matrix(); + } + matrix.reset(); + matrix.postScale(xScale, yScale); + matrix.postRotate(getRequiredRotation()); + matrix.postTranslate(vTranslate.x, vTranslate.y); + + if (getRequiredRotation() == ORIENTATION_180) { + matrix.postTranslate(scale * sWidth, scale * sHeight); + } else if (getRequiredRotation() == ORIENTATION_90) { + matrix.postTranslate(scale * sHeight, 0); + } else if (getRequiredRotation() == ORIENTATION_270) { + matrix.postTranslate(0, scale * sWidth); + } + + if (tileBgPaint != null) { + if (sRect == null) { + sRect = new RectF(); + } + sRect.set(0f, 0f, bitmapIsPreview ? bitmap.getWidth() : sWidth, bitmapIsPreview ? bitmap.getHeight() : sHeight); + matrix.mapRect(sRect); + canvas.drawRect(sRect, tileBgPaint); + } + canvas.drawBitmap(bitmap, matrix, bitmapPaint); + + } + + if (debug) { + canvas.drawText("Scale: " + String.format(Locale.ENGLISH, "%.2f", scale), 5, 15, debugPaint); + canvas.drawText("Translate: " + String.format(Locale.ENGLISH, "%.2f", vTranslate.x) + ":" + String.format(Locale.ENGLISH, "%.2f", vTranslate.y), 5, 35, debugPaint); + PointF center = getCenter(); + canvas.drawText("Source center: " + String.format(Locale.ENGLISH, "%.2f", center.x) + ":" + String.format(Locale.ENGLISH, "%.2f", center.y), 5, 55, debugPaint); + debugPaint.setStrokeWidth(2f); + if (anim != null) { + PointF vCenterStart = sourceToViewCoord(anim.sCenterStart); + PointF vCenterEndRequested = sourceToViewCoord(anim.sCenterEndRequested); + PointF vCenterEnd = sourceToViewCoord(anim.sCenterEnd); + canvas.drawCircle(vCenterStart.x, vCenterStart.y, 10, debugPaint); + debugPaint.setColor(Color.RED); + canvas.drawCircle(vCenterEndRequested.x, vCenterEndRequested.y, 20, debugPaint); + debugPaint.setColor(Color.BLUE); + canvas.drawCircle(vCenterEnd.x, vCenterEnd.y, 25, debugPaint); + debugPaint.setColor(Color.CYAN); + canvas.drawCircle(getWidth() / 2, getHeight() / 2, 30, debugPaint); + } + if (vCenterStart != null) { + debugPaint.setColor(Color.RED); + canvas.drawCircle(vCenterStart.x, vCenterStart.y, 20, debugPaint); + } + if (quickScaleSCenter != null) { + debugPaint.setColor(Color.BLUE); + canvas.drawCircle(sourceToViewX(quickScaleSCenter.x), sourceToViewY(quickScaleSCenter.y), 35, debugPaint); + } + if (quickScaleVStart != null) { + debugPaint.setColor(Color.CYAN); + canvas.drawCircle(quickScaleVStart.x, quickScaleVStart.y, 30, debugPaint); + } + debugPaint.setColor(Color.MAGENTA); + debugPaint.setStrokeWidth(1f); + } + } + + /** + * Helper method for setting the values of a tile matrix array. + */ + private void setMatrixArray(float[] array, float f0, float f1, float f2, float f3, float f4, float f5, float f6, float f7) { + array[0] = f0; + array[1] = f1; + array[2] = f2; + array[3] = f3; + array[4] = f4; + array[5] = f5; + array[6] = f6; + array[7] = f7; + } + + /** + * Checks whether the base layer of tiles or full size bitmap is ready. + */ + private boolean isBaseLayerReady() { + if (bitmap != null && !bitmapIsPreview) { + return true; + } else if (tileMap != null) { + boolean baseLayerReady = true; + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + if (tileMapEntry.getKey() == fullImageSampleSize) { + for (Tile tile : tileMapEntry.getValue()) { + if (tile.loading || tile.bitmap == null) { + baseLayerReady = false; + } + } + } + } + return baseLayerReady; + } + return false; + } + + /** + * Check whether view and image dimensions are known and either a preview, full size image or + * base layer tiles are loaded. First time, send ready event to listener. The next draw will + * display an image. + */ + private boolean checkReady() { + boolean ready = getWidth() > 0 && getHeight() > 0 && sWidth > 0 && sHeight > 0 && (bitmap != null || isBaseLayerReady()); + if (!readySent && ready) { + preDraw(); + readySent = true; + onReady(); + if (onImageEventListener != null) { + onImageEventListener.onReady(); + } + } + return ready; + } + + /** + * Check whether either the full size bitmap or base layer tiles are loaded. First time, send image + * loaded event to listener. + */ + private boolean checkImageLoaded() { + boolean imageLoaded = isBaseLayerReady(); + if (!imageLoadedSent && imageLoaded) { + preDraw(); + imageLoadedSent = true; + onImageLoaded(); + if (onImageEventListener != null) { + onImageEventListener.onImageLoaded(); + } + } + return imageLoaded; + } + + /** + * Creates Paint objects once when first needed. + */ + private void createPaints() { + if (bitmapPaint == null) { + bitmapPaint = new Paint(); + bitmapPaint.setAntiAlias(true); + bitmapPaint.setFilterBitmap(true); + bitmapPaint.setDither(true); + } + if (debugPaint == null && debug) { + debugPaint = new Paint(); + debugPaint.setTextSize(18); + debugPaint.setColor(Color.MAGENTA); + debugPaint.setStyle(Style.STROKE); + } + } + + /** + * Called on first draw when the view has dimensions. Calculates the initial sample size and starts async loading of + * the base layer image - the whole source subsampled as necessary. + */ + private synchronized void initialiseBaseLayer(Point maxTileDimensions) { + debug("initialiseBaseLayer maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y); + + satTemp = new ScaleAndTranslate(0f, new PointF(0, 0)); + fitToBounds(true, satTemp); + + // Load double resolution - next level will be split into four tiles and at the center all four are required, + // so don't bother with tiling until the next level 16 tiles are needed. + fullImageSampleSize = calculateInSampleSize(satTemp.scale); + if (fullImageSampleSize > 1) { + fullImageSampleSize /= 2; + } + + if (fullImageSampleSize == 1 && sRegion == null && sWidth() < maxTileDimensions.x && sHeight() < maxTileDimensions.y) { + + // Whole image is required at native resolution, and is smaller than the canvas max bitmap size. + // Use BitmapDecoder for better image support. + decoder.recycle(); + decoder = null; + BitmapLoadTask task = new BitmapLoadTask(this, getContext(), bitmapDecoderFactory, uri, false); + execute(task); + + } else { + + initialiseTileMap(maxTileDimensions); + + List baseGrid = tileMap.get(fullImageSampleSize); + for (Tile baseTile : baseGrid) { + TileLoadTask task = new TileLoadTask(this, decoder, baseTile); + execute(task); + } + refreshRequiredTiles(true); + + } + + } + + /** + * Loads the optimum tiles for display at the current scale and translate, so the screen can be filled with tiles + * that are at least as high resolution as the screen. Frees up bitmaps that are now off the screen. + * + * @param load Whether to load the new tiles needed. Use false while scrolling/panning for performance. + */ + private void refreshRequiredTiles(boolean load) { + if (decoder == null || tileMap == null) { + return; + } + + int sampleSize = Math.min(fullImageSampleSize, calculateInSampleSize(scale)); + + // Load tiles of the correct sample size that are on screen. Discard tiles off screen, and those that are higher + // resolution than required, or lower res than required but not the base layer, so the base layer is always present. + for (Map.Entry> tileMapEntry : tileMap.entrySet()) { + for (Tile tile : tileMapEntry.getValue()) { + if (tile.sampleSize < sampleSize || (tile.sampleSize > sampleSize && tile.sampleSize != fullImageSampleSize)) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + if (tile.sampleSize == sampleSize) { + if (tileVisible(tile)) { + tile.visible = true; + if (!tile.loading && tile.bitmap == null && load) { + TileLoadTask task = new TileLoadTask(this, decoder, tile); + execute(task); + } + } else if (tile.sampleSize != fullImageSampleSize) { + tile.visible = false; + if (tile.bitmap != null) { + tile.bitmap.recycle(); + tile.bitmap = null; + } + } + } else if (tile.sampleSize == fullImageSampleSize) { + tile.visible = true; + } + } + } + + } + + /** + * Determine whether tile is visible. + */ + private boolean tileVisible(Tile tile) { + float sVisLeft = viewToSourceX(0), + sVisRight = viewToSourceX(getWidth()), + sVisTop = viewToSourceY(0), + sVisBottom = viewToSourceY(getHeight()); + return !(sVisLeft > tile.sRect.right || tile.sRect.left > sVisRight || sVisTop > tile.sRect.bottom || tile.sRect.top > sVisBottom); + } + + /** + * Sets scale and translate ready for the next draw. + */ + private void preDraw() { + if (getWidth() == 0 || getHeight() == 0 || sWidth <= 0 || sHeight <= 0) { + return; + } + + // If waiting to translate to new center position, set translate now + if (sPendingCenter != null && pendingScale != null) { + scale = pendingScale; + if (vTranslate == null) { + vTranslate = new PointF(); + } + vTranslate.x = (getWidth() / 2) - (scale * sPendingCenter.x); + vTranslate.y = (getHeight() / 2) - (scale * sPendingCenter.y); + sPendingCenter = null; + pendingScale = null; + fitToBounds(true); + refreshRequiredTiles(true); + } + + // On first display of base image set up position, and in other cases make sure scale is correct. + fitToBounds(false); + } + + /** + * Calculates sample size to fit the source image in given bounds. + */ + private int calculateInSampleSize(float scale) { + if (minimumTileDpi > 0) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi) / 2; + scale = (minimumTileDpi / averageDpi) * scale; + } + + int reqWidth = (int) (sWidth() * scale); + int reqHeight = (int) (sHeight() * scale); + + // Raw height and width of image + int inSampleSize = 1; + if (reqWidth == 0 || reqHeight == 0) { + return 32; + } + + if (sHeight() > reqHeight || sWidth() > reqWidth) { + + // Calculate ratios of height and width to requested height and width + final int heightRatio = Math.round((float) sHeight() / (float) reqHeight); + final int widthRatio = Math.round((float) sWidth() / (float) reqWidth); + + // Choose the smallest ratio as inSampleSize value, this will guarantee + // a final image with both dimensions larger than or equal to the + // requested height and width. + inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio; + } + + // We want the actual sample size that will be used, so round down to nearest power of 2. + int power = 1; + while (power * 2 < inSampleSize) { + power = power * 2; + } + + return power; + } + + /** + * Adjusts hypothetical future scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. Used to calculate what the target of an + * animation should be. + * + * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + * @param sat The scale we want and the translation we're aiming for. The values are adjusted to be valid. + */ + private void fitToBounds(boolean center, ScaleAndTranslate sat) { + if (panLimit == PAN_LIMIT_OUTSIDE && isReady()) { + center = false; + } + + PointF vTranslate = sat.vTranslate; + float scale = limitedScale(sat.scale); + float scaleWidth = scale * sWidth(); + float scaleHeight = scale * sHeight(); + + if (panLimit == PAN_LIMIT_CENTER && isReady()) { + vTranslate.x = Math.max(vTranslate.x, getWidth() / 2 - scaleWidth); + vTranslate.y = Math.max(vTranslate.y, getHeight() / 2 - scaleHeight); + } else if (center) { + vTranslate.x = Math.max(vTranslate.x, getWidth() - scaleWidth); + vTranslate.y = Math.max(vTranslate.y, getHeight() - scaleHeight); + } else { + vTranslate.x = Math.max(vTranslate.x, -scaleWidth); + vTranslate.y = Math.max(vTranslate.y, -scaleHeight); + } + + // Asymmetric padding adjustments + float xPaddingRatio = getPaddingLeft() > 0 || getPaddingRight() > 0 ? getPaddingLeft() / (float) (getPaddingLeft() + getPaddingRight()) : 0.5f; + float yPaddingRatio = getPaddingTop() > 0 || getPaddingBottom() > 0 ? getPaddingTop() / (float) (getPaddingTop() + getPaddingBottom()) : 0.5f; + + float maxTx; + float maxTy; + if (panLimit == PAN_LIMIT_CENTER && isReady()) { + maxTx = Math.max(0, getWidth() / 2); + maxTy = Math.max(0, getHeight() / 2); + } else if (center) { + maxTx = Math.max(0, (getWidth() - scaleWidth) * xPaddingRatio); + maxTy = Math.max(0, (getHeight() - scaleHeight) * yPaddingRatio); + } else { + maxTx = Math.max(0, getWidth()); + maxTy = Math.max(0, getHeight()); + } + + vTranslate.x = Math.min(vTranslate.x, maxTx); + vTranslate.y = Math.min(vTranslate.y, maxTy); + + sat.scale = scale; + } + + /** + * Adjusts current scale and translate values to keep scale within the allowed range and the image on screen. Minimum scale + * is set so one dimension fills the view and the image is centered on the other dimension. + * + * @param center Whether the image should be centered in the dimension it's too small to fill. While animating this can be false to avoid changes in direction as bounds are reached. + */ + private void fitToBounds(boolean center) { + boolean init = false; + if (vTranslate == null) { + init = true; + vTranslate = new PointF(0, 0); + } + if (satTemp == null) { + satTemp = new ScaleAndTranslate(0, new PointF(0, 0)); + } + satTemp.scale = scale; + satTemp.vTranslate.set(vTranslate); + fitToBounds(center, satTemp); + scale = satTemp.scale; + vTranslate.set(satTemp.vTranslate); + if (init) { + vTranslate.set(vTranslateForSCenter(sWidth() / 2, sHeight() / 2, scale)); + } + } + + /** + * Once source image and view dimensions are known, creates a map of sample size to tile grid. + */ + private void initialiseTileMap(Point maxTileDimensions) { + debug("initialiseTileMap maxTileDimensions=%dx%d", maxTileDimensions.x, maxTileDimensions.y); + this.tileMap = new LinkedHashMap<>(); + int sampleSize = fullImageSampleSize; + int xTiles = 1; + int yTiles = 1; + while (true) { + int sTileWidth = sWidth() / xTiles; + int sTileHeight = sHeight() / yTiles; + int subTileWidth = sTileWidth / sampleSize; + int subTileHeight = sTileHeight / sampleSize; + while (subTileWidth + xTiles + 1 > maxTileDimensions.x || (subTileWidth > getWidth() * 1.25 && sampleSize < fullImageSampleSize)) { + xTiles += 1; + sTileWidth = sWidth() / xTiles; + subTileWidth = sTileWidth / sampleSize; + } + while (subTileHeight + yTiles + 1 > maxTileDimensions.y || (subTileHeight > getHeight() * 1.25 && sampleSize < fullImageSampleSize)) { + yTiles += 1; + sTileHeight = sHeight() / yTiles; + subTileHeight = sTileHeight / sampleSize; + } + List tileGrid = new ArrayList<>(xTiles * yTiles); + for (int x = 0; x < xTiles; x++) { + for (int y = 0; y < yTiles; y++) { + Tile tile = new Tile(); + tile.sampleSize = sampleSize; + tile.visible = sampleSize == fullImageSampleSize; + tile.sRect = new Rect( + x * sTileWidth, + y * sTileHeight, + x == xTiles - 1 ? sWidth() : (x + 1) * sTileWidth, + y == yTiles - 1 ? sHeight() : (y + 1) * sTileHeight + ); + tile.vRect = new Rect(0, 0, 0, 0); + tile.fileSRect = new Rect(tile.sRect); + tileGrid.add(tile); + } + } + tileMap.put(sampleSize, tileGrid); + if (sampleSize == 1) { + break; + } else { + sampleSize /= 2; + } + } + } + + /** + * Async task used to get image details without blocking the UI thread. + */ + private static class TilesInitTask extends AsyncTask { + private final WeakReference viewRef; + private final WeakReference contextRef; + private final WeakReference> decoderFactoryRef; + private final Uri source; + private ImageRegionDecoder decoder; + private Exception exception; + + TilesInitTask(SubsamplingScaleImageView view, Context context, DecoderFactory decoderFactory, Uri source) { + this.viewRef = new WeakReference<>(view); + this.contextRef = new WeakReference<>(context); + this.decoderFactoryRef = new WeakReference>(decoderFactory); + this.source = source; + } + + @Override + protected int[] doInBackground(Void... params) { + try { + String sourceUri = source.toString(); + Context context = contextRef.get(); + DecoderFactory decoderFactory = decoderFactoryRef.get(); + SubsamplingScaleImageView view = viewRef.get(); + if (context != null && decoderFactory != null && view != null) { + view.debug("TilesInitTask.doInBackground"); + decoder = decoderFactory.make(); + Point dimensions = decoder.init(context, source); + int sWidth = dimensions.x; + int sHeight = dimensions.y; + int exifOrientation = view.getExifOrientation(context, sourceUri); + if (view.sRegion != null) { + sWidth = view.sRegion.width(); + sHeight = view.sRegion.height(); + } + return new int[]{sWidth, sHeight, exifOrientation}; + } + } catch (Exception e) { + Log.e(TAG, "Failed to initialise bitmap decoder", e); + this.exception = e; + } + return null; + } + + @Override + protected void onPostExecute(int[] xyo) { + final SubsamplingScaleImageView view = viewRef.get(); + if (view != null) { + if (decoder != null && xyo != null && xyo.length == 3) { + view.onTilesInited(decoder, xyo[0], xyo[1], xyo[2]); + } else if (exception != null && view.onImageEventListener != null) { + view.onImageEventListener.onImageLoadError(exception); + } + } + } + } + + /** + * Called by worker task when decoder is ready and image size and EXIF orientation is known. + */ + private synchronized void onTilesInited(ImageRegionDecoder decoder, int sWidth, int sHeight, int sOrientation) { + debug("onTilesInited sWidth=%d, sHeight=%d, sOrientation=%d", sWidth, sHeight, orientation); + // If actual dimensions don't match the declared size, reset everything. + if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != sWidth || this.sHeight != sHeight)) { + reset(false); + if (bitmap != null) { + if (!bitmapIsCached) { + bitmap.recycle(); + } + bitmap = null; + if (onImageEventListener != null && bitmapIsCached) { + onImageEventListener.onPreviewReleased(); + } + bitmapIsPreview = false; + bitmapIsCached = false; + } + } + this.decoder = decoder; + this.sWidth = sWidth; + this.sHeight = sHeight; + this.sOrientation = sOrientation; + checkReady(); + if (!checkImageLoaded() && maxTileWidth > 0 && maxTileWidth != TILE_SIZE_AUTO && maxTileHeight > 0 && maxTileHeight != TILE_SIZE_AUTO && getWidth() > 0 && getHeight() > 0) { + initialiseBaseLayer(new Point(maxTileWidth, maxTileHeight)); + } + invalidate(); + requestLayout(); + } + + /** + * Async task used to load images without blocking the UI thread. + */ + private static class TileLoadTask extends AsyncTask { + private final WeakReference viewRef; + private final WeakReference decoderRef; + private final WeakReference tileRef; + private Exception exception; + + TileLoadTask(SubsamplingScaleImageView view, ImageRegionDecoder decoder, Tile tile) { + this.viewRef = new WeakReference<>(view); + this.decoderRef = new WeakReference<>(decoder); + this.tileRef = new WeakReference<>(tile); + tile.loading = true; + } + + @Override + protected Bitmap doInBackground(Void... params) { + try { + SubsamplingScaleImageView view = viewRef.get(); + ImageRegionDecoder decoder = decoderRef.get(); + Tile tile = tileRef.get(); + if (decoder != null && tile != null && view != null && decoder.isReady() && tile.visible) { + view.debug("TileLoadTask.doInBackground, tile.sRect=%s, tile.sampleSize=%d", tile.sRect, tile.sampleSize); + synchronized (view.decoderLock) { + // Update tile's file sRect according to rotation + view.fileSRect(tile.sRect, tile.fileSRect); + if (view.sRegion != null) { + tile.fileSRect.offset(view.sRegion.left, view.sRegion.top); + } + return decoder.decodeRegion(tile.fileSRect, tile.sampleSize); + } + } else if (tile != null) { + tile.loading = false; + } + } catch (Exception e) { + Log.e(TAG, "Failed to decode tile", e); + this.exception = e; + } catch (OutOfMemoryError e) { + Log.e(TAG, "Failed to decode tile - OutOfMemoryError", e); + this.exception = new RuntimeException(e); + } + return null; + } + + @Override + protected void onPostExecute(Bitmap bitmap) { + final SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); + final Tile tile = tileRef.get(); + if (subsamplingScaleImageView != null && tile != null) { + if (bitmap != null) { + tile.bitmap = bitmap; + tile.loading = false; + subsamplingScaleImageView.onTileLoaded(); + } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) { + subsamplingScaleImageView.onImageEventListener.onTileLoadError(exception); + } + } + } + } + + /** + * Called by worker task when a tile has loaded. Redraws the view. + */ + private synchronized void onTileLoaded() { + debug("onTileLoaded"); + checkReady(); + checkImageLoaded(); + if (isBaseLayerReady() && bitmap != null) { + if (!bitmapIsCached) { + bitmap.recycle(); + } + bitmap = null; + if (onImageEventListener != null && bitmapIsCached) { + onImageEventListener.onPreviewReleased(); + } + bitmapIsPreview = false; + bitmapIsCached = false; + } + invalidate(); + } + + /** + * Async task used to load bitmap without blocking the UI thread. + */ + private static class BitmapLoadTask extends AsyncTask { + private final WeakReference viewRef; + private final WeakReference contextRef; + private final WeakReference> decoderFactoryRef; + private final Uri source; + private final boolean preview; + private Bitmap bitmap; + private Exception exception; + + BitmapLoadTask(SubsamplingScaleImageView view, Context context, DecoderFactory decoderFactory, Uri source, boolean preview) { + this.viewRef = new WeakReference<>(view); + this.contextRef = new WeakReference<>(context); + this.decoderFactoryRef = new WeakReference>(decoderFactory); + this.source = source; + this.preview = preview; + } + + @Override + protected Integer doInBackground(Void... params) { + try { + String sourceUri = source.toString(); + Context context = contextRef.get(); + DecoderFactory decoderFactory = decoderFactoryRef.get(); + SubsamplingScaleImageView view = viewRef.get(); + if (context != null && decoderFactory != null && view != null) { + view.debug("BitmapLoadTask.doInBackground"); + bitmap = decoderFactory.make().decode(context, source); + return view.getExifOrientation(context, sourceUri); + } + } catch (Exception e) { + Log.e(TAG, "Failed to load bitmap", e); + this.exception = e; + } catch (OutOfMemoryError e) { + Log.e(TAG, "Failed to load bitmap - OutOfMemoryError", e); + this.exception = new RuntimeException(e); + } + return null; + } + + @Override + protected void onPostExecute(Integer orientation) { + SubsamplingScaleImageView subsamplingScaleImageView = viewRef.get(); + if (subsamplingScaleImageView != null) { + if (bitmap != null && orientation != null) { + if (preview) { + subsamplingScaleImageView.onPreviewLoaded(bitmap); + } else { + subsamplingScaleImageView.onImageLoaded(bitmap, orientation, false); + } + } else if (exception != null && subsamplingScaleImageView.onImageEventListener != null) { + if (preview) { + subsamplingScaleImageView.onImageEventListener.onPreviewLoadError(exception); + } else { + subsamplingScaleImageView.onImageEventListener.onImageLoadError(exception); + } + } + } + } + } + + /** + * Called by worker task when preview image is loaded. + */ + private synchronized void onPreviewLoaded(Bitmap previewBitmap) { + debug("onPreviewLoaded"); + if (bitmap != null || imageLoadedSent) { + previewBitmap.recycle(); + return; + } + if (pRegion != null) { + bitmap = Bitmap.createBitmap(previewBitmap, pRegion.left, pRegion.top, pRegion.width(), pRegion.height()); + } else { + bitmap = previewBitmap; + } + bitmapIsPreview = true; + if (checkReady()) { + invalidate(); + requestLayout(); + } + } + + /** + * Called by worker task when full size image bitmap is ready (tiling is disabled). + */ + private synchronized void onImageLoaded(Bitmap bitmap, int sOrientation, boolean bitmapIsCached) { + debug("onImageLoaded"); + // If actual dimensions don't match the declared size, reset everything. + if (this.sWidth > 0 && this.sHeight > 0 && (this.sWidth != bitmap.getWidth() || this.sHeight != bitmap.getHeight())) { + reset(false); + } + if (this.bitmap != null && !this.bitmapIsCached) { + this.bitmap.recycle(); + } + + if (this.bitmap != null && this.bitmapIsCached && onImageEventListener != null) { + onImageEventListener.onPreviewReleased(); + } + + this.bitmapIsPreview = false; + this.bitmapIsCached = bitmapIsCached; + this.bitmap = bitmap; + this.sWidth = bitmap.getWidth(); + this.sHeight = bitmap.getHeight(); + this.sOrientation = sOrientation; + boolean ready = checkReady(); + boolean imageLoaded = checkImageLoaded(); + if (ready || imageLoaded) { + invalidate(); + requestLayout(); + } + } + + /** + * Helper method for load tasks. Examines the EXIF info on the image file to determine the orientation. + * This will only work for external files, not assets, resources or other URIs. + */ + @AnyThread + private int getExifOrientation(Context context, String sourceUri) { + int exifOrientation = ORIENTATION_0; + if (sourceUri.startsWith(ContentResolver.SCHEME_CONTENT)) { + Cursor cursor = null; + try { + String[] columns = {MediaStore.Images.Media.ORIENTATION}; + cursor = context.getContentResolver().query(Uri.parse(sourceUri), columns, null, null, null); + if (cursor != null) { + if (cursor.moveToFirst()) { + int orientation = cursor.getInt(0); + if (VALID_ORIENTATIONS.contains(orientation) && orientation != ORIENTATION_USE_EXIF) { + exifOrientation = orientation; + } else { + Log.w(TAG, "Unsupported orientation: " + orientation); + } + } + } + } catch (Exception e) { + Log.w(TAG, "Could not get orientation of image from media store"); + } finally { + if (cursor != null) { + cursor.close(); + } + } + } else if (sourceUri.startsWith(ImageSource.FILE_SCHEME) && !sourceUri.startsWith(ImageSource.ASSET_SCHEME)) { + try { + ExifInterface exifInterface = new ExifInterface(sourceUri.substring(ImageSource.FILE_SCHEME.length() - 1)); + int orientationAttr = exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL); + if (orientationAttr == ExifInterface.ORIENTATION_NORMAL || orientationAttr == ExifInterface.ORIENTATION_UNDEFINED) { + exifOrientation = ORIENTATION_0; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_90) { + exifOrientation = ORIENTATION_90; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_180) { + exifOrientation = ORIENTATION_180; + } else if (orientationAttr == ExifInterface.ORIENTATION_ROTATE_270) { + exifOrientation = ORIENTATION_270; + } else { + Log.w(TAG, "Unsupported EXIF orientation: " + orientationAttr); + } + } catch (Exception e) { + Log.w(TAG, "Could not get EXIF orientation of image"); + } + } + return exifOrientation; + } + + private void execute(AsyncTask asyncTask) { + if (parallelLoadingEnabled) { + try { + Field executorField = AsyncTask.class.getField("THREAD_POOL_EXECUTOR"); + Executor executor = (Executor) executorField.get(null); + Method executeMethod = AsyncTask.class.getMethod("executeOnExecutor", Executor.class, Object[].class); + executeMethod.invoke(asyncTask, executor, null); + return; + } catch (Exception e) { + Log.i(TAG, "Failed to execute AsyncTask on thread pool executor, falling back to single threaded executor", e); + } + } + asyncTask.execute(); + } + + private static class Tile { + + private Rect sRect; + private int sampleSize; + private Bitmap bitmap; + private boolean loading; + private boolean visible; + + // Volatile fields instantiated once then updated before use to reduce GC. + private Rect vRect; + private Rect fileSRect; + + } + + private static class Anim { + + private float scaleStart; // Scale at start of anim + private float scaleEnd; // Scale at end of anim (target) + private PointF sCenterStart; // Source center point at start + private PointF sCenterEnd; // Source center point at end, adjusted for pan limits + private PointF sCenterEndRequested; // Source center point that was requested, without adjustment + private PointF vFocusStart; // View point that was double tapped + private PointF vFocusEnd; // Where the view focal point should be moved to during the anim + private long duration = 500; // How long the anim takes + private boolean interruptible = true; // Whether the anim can be interrupted by a touch + private int easing = EASE_IN_OUT_QUAD; // Easing style + private int origin = ORIGIN_ANIM; // Animation origin (API, double tap or fling) + private long time = System.currentTimeMillis(); // Start time + private OnAnimationEventListener listener; // Event listener + + } + + private static class ScaleAndTranslate { + private ScaleAndTranslate(float scale, PointF vTranslate) { + this.scale = scale; + this.vTranslate = vTranslate; + } + + private float scale; + private PointF vTranslate; + } + + /** + * Set scale, center and orientation from saved state. + */ + private void restoreState(ImageViewState state) { + if (state != null && state.getCenter() != null && VALID_ORIENTATIONS.contains(state.getOrientation())) { + this.orientation = state.getOrientation(); + this.pendingScale = state.getScale(); + this.sPendingCenter = state.getCenter(); + invalidate(); + } + } + + /** + * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour. + * + * @param maxPixels Maximum tile size X and Y in pixels. + */ + public void setMaxTileSize(int maxPixels) { + this.maxTileWidth = maxPixels; + this.maxTileHeight = maxPixels; + } + + /** + * By default the View automatically calculates the optimal tile size. Set this to override this, and force an upper limit to the dimensions of the generated tiles. Passing {@link #TILE_SIZE_AUTO} will re-enable the default behaviour. + * + * @param maxPixelsX Maximum tile width. + * @param maxPixelsY Maximum tile height. + */ + public void setMaxTileSize(int maxPixelsX, int maxPixelsY) { + this.maxTileWidth = maxPixelsX; + this.maxTileHeight = maxPixelsY; + } + + /** + * In SDK 14 and above, use canvas max bitmap width and height instead of the default 2048, to avoid redundant tiling. + */ + private Point getMaxBitmapDimensions(Canvas canvas) { + int maxWidth = 2048; + int maxHeight = 2048; + if (VERSION.SDK_INT >= 14) { + try { + maxWidth = (Integer) Canvas.class.getMethod("getMaximumBitmapWidth").invoke(canvas); + maxHeight = (Integer) Canvas.class.getMethod("getMaximumBitmapHeight").invoke(canvas); + } catch (Exception e) { + // Return default + } + } + return new Point(Math.min(maxWidth, maxTileWidth), Math.min(maxHeight, maxTileHeight)); + } + + /** + * Get source width taking rotation into account. + */ + @SuppressWarnings("SuspiciousNameCombination") + private int sWidth() { + int rotation = getRequiredRotation(); + if (rotation == 90 || rotation == 270) { + return sHeight; + } else { + return sWidth; + } + } + + /** + * Get source height taking rotation into account. + */ + @SuppressWarnings("SuspiciousNameCombination") + private int sHeight() { + int rotation = getRequiredRotation(); + if (rotation == 90 || rotation == 270) { + return sWidth; + } else { + return sHeight; + } + } + + /** + * Converts source rectangle from tile, which treats the image file as if it were in the correct orientation already, + * to the rectangle of the image that needs to be loaded. + */ + @SuppressWarnings("SuspiciousNameCombination") + @AnyThread + private void fileSRect(Rect sRect, Rect target) { + if (getRequiredRotation() == 0) { + target.set(sRect); + } else if (getRequiredRotation() == 90) { + target.set(sRect.top, sHeight - sRect.right, sRect.bottom, sHeight - sRect.left); + } else if (getRequiredRotation() == 180) { + target.set(sWidth - sRect.right, sHeight - sRect.bottom, sWidth - sRect.left, sHeight - sRect.top); + } else { + target.set(sWidth - sRect.bottom, sRect.left, sWidth - sRect.top, sRect.right); + } + } + + /** + * Determines the rotation to be applied to tiles, based on EXIF orientation or chosen setting. + */ + @AnyThread + private int getRequiredRotation() { + if (orientation == ORIENTATION_USE_EXIF) { + return sOrientation; + } else { + return orientation; + } + } + + /** + * Pythagoras distance between two points. + */ + private float distance(float x0, float x1, float y0, float y1) { + float x = x0 - x1; + float y = y0 - y1; + return (float) Math.sqrt(x * x + y * y); + } + + /** + * Releases all resources the view is using and resets the state, nulling any fields that use significant memory. + * After you have called this method, the view can be re-used by setting a new image. Settings are remembered + * but state (scale and center) is forgotten. You can restore these yourself if required. + */ + public void recycle() { + reset(true); + bitmapPaint = null; + debugPaint = null; + tileBgPaint = null; + } + + /** + * Convert screen to source x coordinate. + */ + private float viewToSourceX(float vx) { + if (vTranslate == null) { + return Float.NaN; + } + return (vx - vTranslate.x) / scale; + } + + /** + * Convert screen to source y coordinate. + */ + private float viewToSourceY(float vy) { + if (vTranslate == null) { + return Float.NaN; + } + return (vy - vTranslate.y) / scale; + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(PointF vxy) { + return viewToSourceCoord(vxy.x, vxy.y, new PointF()); + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(float vx, float vy) { + return viewToSourceCoord(vx, vy, new PointF()); + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(PointF vxy, PointF sTarget) { + return viewToSourceCoord(vxy.x, vxy.y, sTarget); + } + + /** + * Convert screen coordinate to source coordinate. + */ + public final PointF viewToSourceCoord(float vx, float vy, PointF sTarget) { + if (vTranslate == null) { + return null; + } + sTarget.set(viewToSourceX(vx), viewToSourceY(vy)); + return sTarget; + } + + /** + * Convert source to screen x coordinate. + */ + private float sourceToViewX(float sx) { + if (vTranslate == null) { + return Float.NaN; + } + return (sx * scale) + vTranslate.x; + } + + /** + * Convert source to screen y coordinate. + */ + private float sourceToViewY(float sy) { + if (vTranslate == null) { + return Float.NaN; + } + return (sy * scale) + vTranslate.y; + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(PointF sxy) { + return sourceToViewCoord(sxy.x, sxy.y, new PointF()); + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(float sx, float sy) { + return sourceToViewCoord(sx, sy, new PointF()); + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(PointF sxy, PointF vTarget) { + return sourceToViewCoord(sxy.x, sxy.y, vTarget); + } + + /** + * Convert source coordinate to screen coordinate. + */ + public final PointF sourceToViewCoord(float sx, float sy, PointF vTarget) { + if (vTranslate == null) { + return null; + } + vTarget.set(sourceToViewX(sx), sourceToViewY(sy)); + return vTarget; + } + + /** + * Convert source rect to screen rect, integer values. + */ + private Rect sourceToViewRect(Rect sRect, Rect vTarget) { + vTarget.set( + (int) sourceToViewX(sRect.left), + (int) sourceToViewY(sRect.top), + (int) sourceToViewX(sRect.right), + (int) sourceToViewY(sRect.bottom) + ); + return vTarget; + } + + /** + * Get the translation required to place a given source coordinate at the center of the screen, with the center + * adjusted for asymmetric padding. Accepts the desired scale as an argument, so this is independent of current + * translate and scale. The result is fitted to bounds, putting the image point as near to the screen center as permitted. + */ + private PointF vTranslateForSCenter(float sCenterX, float sCenterY, float scale) { + int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft()) / 2; + int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop()) / 2; + if (satTemp == null) { + satTemp = new ScaleAndTranslate(0, new PointF(0, 0)); + } + satTemp.scale = scale; + satTemp.vTranslate.set(vxCenter - (sCenterX * scale), vyCenter - (sCenterY * scale)); + fitToBounds(true, satTemp); + return satTemp.vTranslate; + } + + /** + * Given a requested source center and scale, calculate what the actual center will have to be to keep the image in + * pan limits, keeping the requested center as near to the middle of the screen as allowed. + */ + private PointF limitedSCenter(float sCenterX, float sCenterY, float scale, PointF sTarget) { + PointF vTranslate = vTranslateForSCenter(sCenterX, sCenterY, scale); + int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft()) / 2; + int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop()) / 2; + float sx = (vxCenter - vTranslate.x) / scale; + float sy = (vyCenter - vTranslate.y) / scale; + sTarget.set(sx, sy); + return sTarget; + } + + /** + * Returns the minimum allowed scale. + */ + private float minScale() { + int vPadding = getPaddingBottom() + getPaddingTop(); + int hPadding = getPaddingLeft() + getPaddingRight(); + if (minimumScaleType == SCALE_TYPE_CENTER_CROP) { + return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight()); + } else if (minimumScaleType == SCALE_TYPE_CUSTOM && minScale > 0) { + return minScale; + } else { + return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight()); + } + } + + /** + * Adjust a requested scale to be within the allowed limits. + */ + private float limitedScale(float targetScale) { + targetScale = Math.max(minScale(), targetScale); + targetScale = Math.min(maxScale, targetScale); + return targetScale; + } + + /** + * Apply a selected type of easing. + * + * @param type Easing type, from static fields + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float ease(int type, long time, float from, float change, long duration) { + switch (type) { + case EASE_IN_OUT_QUAD: + return easeInOutQuad(time, from, change, duration); + case EASE_OUT_QUAD: + return easeOutQuad(time, from, change, duration); + default: + throw new IllegalStateException("Unexpected easing type: " + type); + } + } + + /** + * Quadratic easing for fling. With thanks to Robert Penner - http://gizma.com/easing/ + * + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float easeOutQuad(long time, float from, float change, long duration) { + float progress = (float) time / (float) duration; + return -change * progress * (progress - 2) + from; + } + + /** + * Quadratic easing for scale and center animations. With thanks to Robert Penner - http://gizma.com/easing/ + * + * @param time Elapsed time + * @param from Start value + * @param change Target value + * @param duration Anm duration + * @return Current value + */ + private float easeInOutQuad(long time, float from, float change, long duration) { + float timeF = time / (duration / 2f); + if (timeF < 1) { + return (change / 2f * timeF * timeF) + from; + } else { + timeF--; + return (-change / 2f) * (timeF * (timeF - 2) - 1) + from; + } + } + + /** + * Debug logger + */ + @AnyThread + private void debug(String message, Object... args) { + if (debug) { + Log.d(TAG, String.format(message, args)); + } + } + + /** + * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or + * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a + * public default constructor. + * + * @param regionDecoderClass The {@link ImageRegionDecoder} implementation to use. + */ + public final void setRegionDecoderClass(Class regionDecoderClass) { + if (regionDecoderClass == null) { + throw new IllegalArgumentException("Decoder class cannot be set to null"); + } + this.regionDecoderFactory = new CompatDecoderFactory<>(regionDecoderClass); + } + + /** + * Swap the default region decoder implementation for one of your own. You must do this before setting the image file or + * asset, and you cannot use a custom decoder when using layout XML to set an asset name. + * + * @param regionDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageRegionDecoder} + * instances. + */ + public final void setRegionDecoderFactory(DecoderFactory regionDecoderFactory) { + if (regionDecoderFactory == null) { + throw new IllegalArgumentException("Decoder factory cannot be set to null"); + } + this.regionDecoderFactory = regionDecoderFactory; + } + + /** + * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or + * asset, and you cannot use a custom decoder when using layout XML to set an asset name. Your class must have a + * public default constructor. + * + * @param bitmapDecoderClass The {@link ImageDecoder} implementation to use. + */ + public final void setBitmapDecoderClass(Class bitmapDecoderClass) { + if (bitmapDecoderClass == null) { + throw new IllegalArgumentException("Decoder class cannot be set to null"); + } + this.bitmapDecoderFactory = new CompatDecoderFactory<>(bitmapDecoderClass); + } + + /** + * Swap the default bitmap decoder implementation for one of your own. You must do this before setting the image file or + * asset, and you cannot use a custom decoder when using layout XML to set an asset name. + * + * @param bitmapDecoderFactory The {@link DecoderFactory} implementation that produces {@link ImageDecoder} instances. + */ + public final void setBitmapDecoderFactory(DecoderFactory bitmapDecoderFactory) { + if (bitmapDecoderFactory == null) { + throw new IllegalArgumentException("Decoder factory cannot be set to null"); + } + this.bitmapDecoderFactory = bitmapDecoderFactory; + } + + /** + * Set the pan limiting style. See static fields. Normally {@link #PAN_LIMIT_INSIDE} is best, for image galleries. + */ + public final void setPanLimit(int panLimit) { + if (!VALID_PAN_LIMITS.contains(panLimit)) { + throw new IllegalArgumentException("Invalid pan limit: " + panLimit); + } + this.panLimit = panLimit; + if (isReady()) { + fitToBounds(true); + invalidate(); + } + } + + /** + * Set the minimum scale type. See static fields. Normally {@link #SCALE_TYPE_CENTER_INSIDE} is best, for image galleries. + */ + public final void setMinimumScaleType(int scaleType) { + if (!VALID_SCALE_TYPES.contains(scaleType)) { + throw new IllegalArgumentException("Invalid scale type: " + scaleType); + } + this.minimumScaleType = scaleType; + if (isReady()) { + fitToBounds(true); + invalidate(); + } + } + + /** + * Set the maximum scale allowed. A value of 1 means 1:1 pixels at maximum scale. You may wish to set this according + * to screen density - on a retina screen, 1:1 may still be too small. Consider using {@link #setMinimumDpi(int)}, + * which is density aware. + */ + public final void setMaxScale(float maxScale) { + this.maxScale = maxScale; + } + + /** + * Set the minimum scale allowed. A value of 1 means 1:1 pixels at minimum scale. You may wish to set this according + * to screen density. Consider using {@link #setMaximumDpi(int)}, which is density aware. + */ + public final void setMinScale(float minScale) { + this.minScale = minScale; + } + + /** + * This is a screen density aware alternative to {@link #setMaxScale(float)}; it allows you to express the maximum + * allowed scale in terms of the minimum pixel density. This avoids the problem of 1:1 scale still being + * too small on a high density screen. A sensible starting point is 160 - the default used by this view. + * + * @param dpi Source image pixel density at maximum zoom. + */ + public final void setMinimumDpi(int dpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi) / 2; + setMaxScale(averageDpi / dpi); + } + + /** + * This is a screen density aware alternative to {@link #setMinScale(float)}; it allows you to express the minimum + * allowed scale in terms of the maximum pixel density. + * + * @param dpi Source image pixel density at minimum zoom. + */ + public final void setMaximumDpi(int dpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi) / 2; + setMinScale(averageDpi / dpi); + } + + /** + * Returns the maximum allowed scale. + */ + public float getMaxScale() { + return maxScale; + } + + /** + * Returns the minimum allowed scale. + */ + public final float getMinScale() { + return minScale(); + } + + /** + * By default, image tiles are at least as high resolution as the screen. For a retina screen this may not be + * necessary, and may increase the likelihood of an OutOfMemoryError. This method sets a DPI at which higher + * resolution tiles should be loaded. Using a lower number will on average use less memory but result in a lower + * quality image. 160-240dpi will usually be enough. This should be called before setting the image source, + * because it affects which tiles get loaded. When using an untiled source image this method has no effect. + * + * @param minimumTileDpi Tile loading threshold. + */ + public void setMinimumTileDpi(int minimumTileDpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi) / 2; + this.minimumTileDpi = (int) Math.min(averageDpi, minimumTileDpi); + if (isReady()) { + reset(false); + invalidate(); + } + } + + /** + * Returns the source point at the center of the view. + */ + public final PointF getCenter() { + int mX = getWidth() / 2; + int mY = getHeight() / 2; + return viewToSourceCoord(mX, mY); + } + + /** + * Returns the current scale value. + */ + public final float getScale() { + return scale; + } + + /** + * Externally change the scale and translation of the source image. This may be used with getCenter() and getScale() + * to restore the scale and zoom after a screen rotate. + * + * @param scale New scale to set. + * @param sCenter New source image coordinate to center on the screen, subject to boundaries. + */ + public final void setScaleAndCenter(float scale, PointF sCenter) { + this.anim = null; + this.pendingScale = scale; + this.sPendingCenter = sCenter; + this.sRequestedCenter = sCenter; + invalidate(); + } + + /** + * Fully zoom out and return the image to the middle of the screen. This might be useful if you have a view pager + * and want images to be reset when the user has moved to another page. + */ + public final void resetScaleAndCenter() { + this.anim = null; + this.pendingScale = limitedScale(0); + if (isReady()) { + this.sPendingCenter = new PointF(sWidth() / 2, sHeight() / 2); + } else { + this.sPendingCenter = new PointF(0, 0); + } + invalidate(); + } + + /** + * Call to find whether the view is initialised, has dimensions, and will display an image on + * the next draw. If a preview has been provided, it may be the preview that will be displayed + * and the full size image may still be loading. If no preview was provided, this is called once + * the base layer tiles of the full size image are loaded. + */ + public final boolean isReady() { + return readySent; + } + + /** + * Called once when the view is initialised, has dimensions, and will display an image on the + * next draw. This is triggered at the same time as {@link OnImageEventListener#onReady()} but + * allows a subclass to receive this event without using a listener. + */ + protected void onReady() { + + } + + /** + * Call to find whether the main image (base layer tiles where relevant) have been loaded. Before + * this event the view is blank unless a preview was provided. + */ + public final boolean isImageLoaded() { + return imageLoadedSent; + } + + /** + * Called once when the full size image or its base layer tiles have been loaded. + */ + protected void onImageLoaded() { + + } + + /** + * Get source width, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSHeight()} + * for the apparent width. + */ + public final int getSWidth() { + return sWidth; + } + + /** + * Get source height, ignoring orientation. If {@link #getOrientation()} returns 90 or 270, you can use {@link #getSWidth()} + * for the apparent height. + */ + public final int getSHeight() { + return sHeight; + } + + /** + * Returns the orientation setting. This can return {@link #ORIENTATION_USE_EXIF}, in which case it doesn't tell you + * the applied orientation of the image. For that, use {@link #getAppliedOrientation()}. + */ + public final int getOrientation() { + return orientation; + } + + /** + * Returns the actual orientation of the image relative to the source file. This will be based on the source file's + * EXIF orientation if you're using ORIENTATION_USE_EXIF. Values are 0, 90, 180, 270. + */ + public final int getAppliedOrientation() { + return getRequiredRotation(); + } + + /** + * Get the current state of the view (scale, center, orientation) for restoration after rotate. Will return null if + * the view is not ready. + */ + public final ImageViewState getState() { + if (vTranslate != null && sWidth > 0 && sHeight > 0) { + return new ImageViewState(getScale(), getCenter(), getOrientation()); + } + return null; + } + + /** + * Returns true if zoom gesture detection is enabled. + */ + public final boolean isZoomEnabled() { + return zoomEnabled; + } + + /** + * Enable or disable zoom gesture detection. Disabling zoom locks the the current scale. + */ + public final void setZoomEnabled(boolean zoomEnabled) { + this.zoomEnabled = zoomEnabled; + } + + /** + * Returns true if double tap & swipe to zoom is enabled. + */ + public final boolean isQuickScaleEnabled() { + return quickScaleEnabled; + } + + /** + * Enable or disable double tap & swipe to zoom. + */ + public final void setQuickScaleEnabled(boolean quickScaleEnabled) { + this.quickScaleEnabled = quickScaleEnabled; + } + + /** + * Returns true if pan gesture detection is enabled. + */ + public final boolean isPanEnabled() { + return panEnabled; + } + + /** + * Enable or disable pan gesture detection. Disabling pan causes the image to be centered. + */ + public final void setPanEnabled(boolean panEnabled) { + this.panEnabled = panEnabled; + if (!panEnabled && vTranslate != null) { + vTranslate.x = (getWidth() / 2) - (scale * (sWidth() / 2)); + vTranslate.y = (getHeight() / 2) - (scale * (sHeight() / 2)); + if (isReady()) { + refreshRequiredTiles(true); + invalidate(); + } + } + } + + /** + * Set a solid color to render behind tiles, useful for displaying transparent PNGs. + * + * @param tileBgColor Background color for tiles. + */ + public final void setTileBackgroundColor(int tileBgColor) { + if (Color.alpha(tileBgColor) == 0) { + tileBgPaint = null; + } else { + tileBgPaint = new Paint(); + tileBgPaint.setStyle(Style.FILL); + tileBgPaint.setColor(tileBgColor); + } + invalidate(); + } + + /** + * Set the scale the image will zoom in to when double tapped. This also the scale point where a double tap is interpreted + * as a zoom out gesture - if the scale is greater than 90% of this value, a double tap zooms out. Avoid using values + * greater than the max zoom. + * + * @param doubleTapZoomScale New value for double tap gesture zoom scale. + */ + public final void setDoubleTapZoomScale(float doubleTapZoomScale) { + this.doubleTapZoomScale = doubleTapZoomScale; + } + + /** + * A density aware alternative to {@link #setDoubleTapZoomScale(float)}; this allows you to express the scale the + * image will zoom in to when double tapped in terms of the image pixel density. Values lower than the max scale will + * be ignored. A sensible starting point is 160 - the default used by this view. + * + * @param dpi New value for double tap gesture zoom scale. + */ + public final void setDoubleTapZoomDpi(int dpi) { + DisplayMetrics metrics = getResources().getDisplayMetrics(); + float averageDpi = (metrics.xdpi + metrics.ydpi) / 2; + setDoubleTapZoomScale(averageDpi / dpi); + } + + /** + * Set the type of zoom animation to be used for double taps. See static fields. + * + * @param doubleTapZoomStyle New value for zoom style. + */ + public final void setDoubleTapZoomStyle(int doubleTapZoomStyle) { + if (!VALID_ZOOM_STYLES.contains(doubleTapZoomStyle)) { + throw new IllegalArgumentException("Invalid zoom style: " + doubleTapZoomStyle); + } + this.doubleTapZoomStyle = doubleTapZoomStyle; + } + + /** + * Set the duration of the double tap zoom animation. + * + * @param durationMs Duration in milliseconds. + */ + public final void setDoubleTapZoomDuration(int durationMs) { + this.doubleTapZoomDuration = Math.max(0, durationMs); + } + + /** + * Toggle parallel loading. When enabled, tiles are loaded using the thread pool executor available + * in SDK 11+. In older versions this has no effect. Parallel loading may use more memory and there + * is a possibility that it will make the tile loading unreliable, but it reduces the chances of + * an app's background processes blocking loading. + * + * @param parallelLoadingEnabled Whether to run AsyncTasks using a thread pool executor. + */ + public void setParallelLoadingEnabled(boolean parallelLoadingEnabled) { + this.parallelLoadingEnabled = parallelLoadingEnabled; + } + + /** + * Enables visual debugging, showing tile boundaries and sizes. + */ + public final void setDebug(boolean debug) { + this.debug = debug; + } + + /** + * Check if an image has been set. The image may not have been loaded and displayed yet. + * + * @return If an image is currently set. + */ + public boolean hasImage() { + return uri != null || bitmap != null; + } + + /** + * {@inheritDoc} + */ + @Override + public void setOnLongClickListener(OnLongClickListener onLongClickListener) { + this.onLongClickListener = onLongClickListener; + } + + /** + * Add a listener allowing notification of load and error events. + */ + public void setOnImageEventListener(OnImageEventListener onImageEventListener) { + this.onImageEventListener = onImageEventListener; + } + + /** + * Add a listener for pan and zoom events. + */ + public void setOnStateChangedListener(OnStateChangedListener onStateChangedListener) { + this.onStateChangedListener = onStateChangedListener; + } + + private void sendStateChanged(float oldScale, PointF oldVTranslate, int origin) { + if (onStateChangedListener != null) { + if (scale != oldScale) { + onStateChangedListener.onScaleChanged(scale, origin); + } + if (!vTranslate.equals(oldVTranslate)) { + onStateChangedListener.onCenterChanged(getCenter(), origin); + } + } + } + + /** + * Creates a panning animation builder, that when started will animate the image to place the given coordinates of + * the image in the center of the screen. If doing this would move the image beyond the edges of the screen, the + * image is instead animated to move the center point as near to the center of the screen as is allowed - it's + * guaranteed to be on screen. + * + * @param sCenter Target center point + * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateCenter(PointF sCenter) { + if (!isReady()) { + return null; + } + return new AnimationBuilder(sCenter); + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * + * @param scale Target scale. + * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateScale(float scale) { + if (!isReady()) { + return null; + } + return new AnimationBuilder(scale); + } + + /** + * Creates a scale animation builder, that when started will animate a zoom in or out. If this would move the image + * beyond the panning limits, the image is automatically panned during the animation. + * + * @param scale Target scale. + * @return {@link AnimationBuilder} instance. Call {@link AnimationBuilder#start()} to start the anim. + */ + public AnimationBuilder animateScaleAndCenter(float scale, PointF sCenter) { + if (!isReady()) { + return null; + } + return new AnimationBuilder(scale, sCenter); + } + + /** + * Builder class used to set additional options for a scale animation. Create an instance using {@link #animateScale(float)}, + * then set your options and call {@link #start()}. + */ + public final class AnimationBuilder { + + private final float targetScale; + private final PointF targetSCenter; + private final PointF vFocus; + private long duration = 500; + private int easing = EASE_IN_OUT_QUAD; + private int origin = ORIGIN_ANIM; + private boolean interruptible = true; + private boolean panLimited = true; + private OnAnimationEventListener listener; + + private AnimationBuilder(PointF sCenter) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = null; + } + + private AnimationBuilder(float scale) { + this.targetScale = scale; + this.targetSCenter = getCenter(); + this.vFocus = null; + } + + private AnimationBuilder(float scale, PointF sCenter) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = null; + } + + private AnimationBuilder(float scale, PointF sCenter, PointF vFocus) { + this.targetScale = scale; + this.targetSCenter = sCenter; + this.vFocus = vFocus; + } + + /** + * Desired duration of the anim in milliseconds. Default is 500. + * + * @param duration duration in milliseconds. + * @return this builder for method chaining. + */ + public AnimationBuilder withDuration(long duration) { + this.duration = duration; + return this; + } + + /** + * Whether the animation can be interrupted with a touch. Default is true. + * + * @param interruptible interruptible flag. + * @return this builder for method chaining. + */ + public AnimationBuilder withInterruptible(boolean interruptible) { + this.interruptible = interruptible; + return this; + } + + /** + * Set the easing style. See static fields. {@link #EASE_IN_OUT_QUAD} is recommended, and the default. + * + * @param easing easing style. + * @return this builder for method chaining. + */ + public AnimationBuilder withEasing(int easing) { + if (!VALID_EASING_STYLES.contains(easing)) { + throw new IllegalArgumentException("Unknown easing type: " + easing); + } + this.easing = easing; + return this; + } + + /** + * Add an animation event listener. + * + * @param listener The listener. + * @return this builder for method chaining. + */ + public AnimationBuilder withOnAnimationEventListener(OnAnimationEventListener listener) { + this.listener = listener; + return this; + } + + /** + * Only for internal use. When set to true, the animation proceeds towards the actual end point - the nearest + * point to the center allowed by pan limits. When false, animation is in the direction of the requested end + * point and is stopped when the limit for each axis is reached. The latter behaviour is used for flings but + * nothing else. + */ + private AnimationBuilder withPanLimited(boolean panLimited) { + this.panLimited = panLimited; + return this; + } + + /** + * Only for internal use. Indicates what caused the animation. + */ + private AnimationBuilder withOrigin(int origin) { + this.origin = origin; + return this; + } + + /** + * Starts the animation. + */ + public void start() { + if (anim != null && anim.listener != null) { + try { + anim.listener.onInterruptedByNewAnim(); + } catch (Exception e) { + Log.w(TAG, "Error thrown by animation listener", e); + } + } + + int vxCenter = getPaddingLeft() + (getWidth() - getPaddingRight() - getPaddingLeft()) / 2; + int vyCenter = getPaddingTop() + (getHeight() - getPaddingBottom() - getPaddingTop()) / 2; + float targetScale = limitedScale(this.targetScale); + PointF targetSCenter = panLimited ? limitedSCenter(this.targetSCenter.x, this.targetSCenter.y, targetScale, new PointF()) : this.targetSCenter; + anim = new Anim(); + anim.scaleStart = scale; + anim.scaleEnd = targetScale; + anim.time = System.currentTimeMillis(); + anim.sCenterEndRequested = targetSCenter; + anim.sCenterStart = getCenter(); + anim.sCenterEnd = targetSCenter; + anim.vFocusStart = sourceToViewCoord(targetSCenter); + anim.vFocusEnd = new PointF( + vxCenter, + vyCenter + ); + anim.duration = duration; + anim.interruptible = interruptible; + anim.easing = easing; + anim.origin = origin; + anim.time = System.currentTimeMillis(); + anim.listener = listener; + + if (vFocus != null) { + // Calculate where translation will be at the end of the anim + float vTranslateXEnd = vFocus.x - (targetScale * anim.sCenterStart.x); + float vTranslateYEnd = vFocus.y - (targetScale * anim.sCenterStart.y); + ScaleAndTranslate satEnd = new ScaleAndTranslate(targetScale, new PointF(vTranslateXEnd, vTranslateYEnd)); + // Fit the end translation into bounds + fitToBounds(true, satEnd); + // Adjust the position of the focus point at end so image will be in bounds + anim.vFocusEnd = new PointF( + vFocus.x + (satEnd.vTranslate.x - vTranslateXEnd), + vFocus.y + (satEnd.vTranslate.y - vTranslateYEnd) + ); + } + + invalidate(); + } + + } + + /** + * An event listener for animations, allows events to be triggered when an animation completes, + * is aborted by another animation starting, or is aborted by a touch event. Note that none of + * these events are triggered if the activity is paused, the image is swapped, or in other cases + * where the view's internal state gets wiped or draw events stop. + */ + public interface OnAnimationEventListener { + + /** + * The animation has completed, having reached its endpoint. + */ + void onComplete(); + + /** + * The animation has been aborted before reaching its endpoint because the user touched the screen. + */ + void onInterruptedByUser(); + + /** + * The animation has been aborted before reaching its endpoint because a new animation has been started. + */ + void onInterruptedByNewAnim(); + + } + + /** + * Default implementation of {@link OnAnimationEventListener} for extension. This does nothing in any method. + */ + public static class DefaultOnAnimationEventListener implements OnAnimationEventListener { + + @Override + public void onComplete() { + } + + @Override + public void onInterruptedByUser() { + } + + @Override + public void onInterruptedByNewAnim() { + } + + } + + /** + * An event listener, allowing subclasses and activities to be notified of significant events. + */ + public interface OnImageEventListener { + + /** + * Called when the dimensions of the image and view are known, and either a preview image, + * the full size image, or base layer tiles are loaded. This indicates the scale and translate + * are known and the next draw will display an image. This event can be used to hide a loading + * graphic, or inform a subclass that it is safe to draw overlays. + */ + void onReady(); + + /** + * Called when the full size image is ready. When using tiling, this means the lowest resolution + * base layer of tiles are loaded, and when tiling is disabled, the image bitmap is loaded. + * This event could be used as a trigger to enable gestures if you wanted interaction disabled + * while only a preview is displayed, otherwise for most cases {@link #onReady()} is the best + * event to listen to. + */ + void onImageLoaded(); + + /** + * Called when a preview image could not be loaded. This method cannot be relied upon; certain + * encoding types of supported image formats can result in corrupt or blank images being loaded + * and displayed with no detectable error. The view will continue to load the full size image. + * + * @param e The exception thrown. This error is logged by the view. + */ + void onPreviewLoadError(Exception e); + + /** + * Indicates an error initiliasing the decoder when using a tiling, or when loading the full + * size bitmap when tiling is disabled. This method cannot be relied upon; certain encoding + * types of supported image formats can result in corrupt or blank images being loaded and + * displayed with no detectable error. + * + * @param e The exception thrown. This error is also logged by the view. + */ + void onImageLoadError(Exception e); + + /** + * Called when an image tile could not be loaded. This method cannot be relied upon; certain + * encoding types of supported image formats can result in corrupt or blank images being loaded + * and displayed with no detectable error. Most cases where an unsupported file is used will + * result in an error caught by {@link #onImageLoadError(Exception)}. + * + * @param e The exception thrown. This error is logged by the view. + */ + void onTileLoadError(Exception e); + + /** + * Called when a bitmap set using ImageSource.cachedBitmap is no longer being used by the View. + * This is useful if you wish to manage the bitmap after the preview is shown + */ + void onPreviewReleased(); + } + + /** + * Default implementation of {@link OnImageEventListener} for extension. This does nothing in any method. + */ + public static class DefaultOnImageEventListener implements OnImageEventListener { + + @Override + public void onReady() { + } + + @Override + public void onImageLoaded() { + } + + @Override + public void onPreviewLoadError(Exception e) { + } + + @Override + public void onImageLoadError(Exception e) { + } + + @Override + public void onTileLoadError(Exception e) { + } + + @Override + public void onPreviewReleased() { + } + + } + + /** + * An event listener, allowing activities to be notified of pan and zoom events. Initialisation + * and calls made by your code do not trigger events; touch events and animations do. Methods in + * this listener will be called on the UI thread and may be called very frequently - your + * implementation should return quickly. + */ + public interface OnStateChangedListener { + + /** + * The scale has changed. Use with {@link #getMaxScale()} and {@link #getMinScale()} to determine + * whether the image is fully zoomed in or out. + * + * @param newScale The new scale. + * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}. + */ + void onScaleChanged(float newScale, int origin); + + /** + * The source center has been changed. This can be a result of panning or zooming. + * + * @param newCenter The new source center point. + * @param origin Where the event originated from - one of {@link #ORIGIN_ANIM}, {@link #ORIGIN_TOUCH}. + */ + void onCenterChanged(PointF newCenter, int origin); + + } + + /** + * Default implementation of {@link OnStateChangedListener}. This does nothing in any method. + */ + public static class DefaultOnStateChangedListener implements OnStateChangedListener { + + @Override + public void onCenterChanged(PointF newCenter, int origin) { + } + + @Override + public void onScaleChanged(float newScale, int origin) { + } + + } + +} diff --git a/picture_library/src/main/res/anim/picture_anim_album_dismiss.xml b/picture_library/src/main/res/anim/picture_anim_album_dismiss.xml new file mode 100644 index 0000000..8d1a031 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_album_dismiss.xml @@ -0,0 +1,10 @@ + + + + + diff --git a/picture_library/src/main/res/anim/picture_anim_album_show.xml b/picture_library/src/main/res/anim/picture_anim_album_show.xml new file mode 100644 index 0000000..7844625 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_album_show.xml @@ -0,0 +1,11 @@ + + + + diff --git a/picture_library/src/main/res/anim/picture_anim_anticipate_interpolator.xml b/picture_library/src/main/res/anim/picture_anim_anticipate_interpolator.xml new file mode 100644 index 0000000..d943a83 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_anticipate_interpolator.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_down_out.xml b/picture_library/src/main/res/anim/picture_anim_down_out.xml new file mode 100644 index 0000000..747c635 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_down_out.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_enter.xml b/picture_library/src/main/res/anim/picture_anim_enter.xml new file mode 100644 index 0000000..f97220c --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_enter.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_exit.xml b/picture_library/src/main/res/anim/picture_anim_exit.xml new file mode 100644 index 0000000..96d7cc9 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_exit.xml @@ -0,0 +1,16 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_fade_in.xml b/picture_library/src/main/res/anim/picture_anim_fade_in.xml new file mode 100644 index 0000000..74905b2 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_fade_in.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_fade_out.xml b/picture_library/src/main/res/anim/picture_anim_fade_out.xml new file mode 100644 index 0000000..b2205ac --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_fade_out.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_modal_in.xml b/picture_library/src/main/res/anim/picture_anim_modal_in.xml new file mode 100644 index 0000000..cc0ee7d --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_modal_in.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_modal_out.xml b/picture_library/src/main/res/anim/picture_anim_modal_out.xml new file mode 100644 index 0000000..b1ffdc7 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_modal_out.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_overshoot_interpolator.xml b/picture_library/src/main/res/anim/picture_anim_overshoot_interpolator.xml new file mode 100644 index 0000000..c327bd5 --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_overshoot_interpolator.xml @@ -0,0 +1,3 @@ + + \ No newline at end of file diff --git a/picture_library/src/main/res/anim/picture_anim_up_in.xml b/picture_library/src/main/res/anim/picture_anim_up_in.xml new file mode 100644 index 0000000..8b68bbf --- /dev/null +++ b/picture_library/src/main/res/anim/picture_anim_up_in.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/color/picture_list_text_color.xml b/picture_library/src/main/res/color/picture_list_text_color.xml new file mode 100644 index 0000000..00e3ec8 --- /dev/null +++ b/picture_library/src/main/res/color/picture_list_text_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/color/picture_preview_text_color.xml b/picture_library/src/main/res/color/picture_preview_text_color.xml new file mode 100644 index 0000000..00e3ec8 --- /dev/null +++ b/picture_library/src/main/res/color/picture_preview_text_color.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio.png new file mode 100644 index 0000000..b47574d Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio_bg.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio_bg.png new file mode 100644 index 0000000..06a43fb Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_audio_bg.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_back.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_back.png new file mode 100644 index 0000000..392bcd4 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_back.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_blue_org_normal.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_blue_org_normal.png new file mode 100644 index 0000000..8ed6a72 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_blue_org_normal.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_camera.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_camera.png new file mode 100644 index 0000000..7ec0774 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_camera.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_close.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_close.png new file mode 100644 index 0000000..67c1512 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_close.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_data_error.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_data_error.png new file mode 100644 index 0000000..4495840 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_data_error.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_def.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_def.png new file mode 100644 index 0000000..302d1fd Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_def.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_def_qq.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_def_qq.png new file mode 100644 index 0000000..4622111 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_def_qq.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete.png new file mode 100644 index 0000000..65c21e4 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete_photo.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete_photo.png new file mode 100644 index 0000000..e1b5f13 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_delete_photo.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_grey_org_normal.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_grey_org_normal.png new file mode 100644 index 0000000..c241b93 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_grey_org_normal.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_no_data.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_no_data.png new file mode 100644 index 0000000..a9ce506 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_no_data.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_normal.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_normal.png new file mode 100644 index 0000000..6e1c6c2 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_normal.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_selected.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_selected.png new file mode 100644 index 0000000..2bd2b28 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_org_selected.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_progress.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_progress.png new file mode 100644 index 0000000..1291fac Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_progress.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel.png new file mode 100644 index 0000000..1ac42c2 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel_qq.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel_qq.png new file mode 100644 index 0000000..b1f12a4 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_sel_qq.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_shadow_bg.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_shadow_bg.png new file mode 100644 index 0000000..440cbb3 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_shadow_bg.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_video.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_video.png new file mode 100644 index 0000000..86e6d48 Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_video.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_down.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_down.png new file mode 100644 index 0000000..c5f635c Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_down.png differ diff --git a/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_up.png b/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_up.png new file mode 100644 index 0000000..eeda27f Binary files /dev/null and b/picture_library/src/main/res/drawable-xhdpi/picture_icon_wechat_up.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_down.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_down.png new file mode 100644 index 0000000..bd3614a Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_down.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_up.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_up.png new file mode 100644 index 0000000..bc537f1 Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_arrow_up.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_check.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_check.png new file mode 100644 index 0000000..077c68f Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_check.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_checked.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_checked.png new file mode 100644 index 0000000..463b24b Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_checked.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_more.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_more.png new file mode 100644 index 0000000..64268c4 Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_more.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_placeholder.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_placeholder.png new file mode 100644 index 0000000..1f7ad00 Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_placeholder.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_video_play.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_video_play.png new file mode 100644 index 0000000..a7bb903 Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_video_play.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_warning.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_warning.png new file mode 100644 index 0000000..81b5f55 Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_warning.png differ diff --git a/picture_library/src/main/res/drawable-xxhdpi/picture_icon_wechat_check.png b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_wechat_check.png new file mode 100644 index 0000000..077c68f Binary files /dev/null and b/picture_library/src/main/res/drawable-xxhdpi/picture_icon_wechat_check.png differ diff --git a/picture_library/src/main/res/drawable/picture_album_bg.xml b/picture_library/src/main/res/drawable/picture_album_bg.xml new file mode 100644 index 0000000..4451a2b --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_album_bg.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_anim_progress.xml b/picture_library/src/main/res/drawable/picture_anim_progress.xml new file mode 100644 index 0000000..966af5b --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_anim_progress.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_audio_placeholder.xml b/picture_library/src/main/res/drawable/picture_audio_placeholder.xml new file mode 100644 index 0000000..cd926f7 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_audio_placeholder.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_bottom_selector.xml b/picture_library/src/main/res/drawable/picture_btn_bottom_selector.xml new file mode 100644 index 0000000..94bf5d0 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_bottom_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_left_bottom_selector.xml b/picture_library/src/main/res/drawable/picture_btn_left_bottom_selector.xml new file mode 100644 index 0000000..eeec3ea --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_left_bottom_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_left_false.xml b/picture_library/src/main/res/drawable/picture_btn_left_false.xml new file mode 100644 index 0000000..b4662f1 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_left_false.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_left_true.xml b/picture_library/src/main/res/drawable/picture_btn_left_true.xml new file mode 100644 index 0000000..dd1a987 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_left_true.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_music_shape.xml b/picture_library/src/main/res/drawable/picture_btn_music_shape.xml new file mode 100644 index 0000000..31d966d --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_music_shape.xml @@ -0,0 +1,12 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_right_bottom_selector.xml b/picture_library/src/main/res/drawable/picture_btn_right_bottom_selector.xml new file mode 100644 index 0000000..d076a9f --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_right_bottom_selector.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_right_false.xml b/picture_library/src/main/res/drawable/picture_btn_right_false.xml new file mode 100644 index 0000000..72ebecc --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_right_false.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_btn_right_true.xml b/picture_library/src/main/res/drawable/picture_btn_right_true.xml new file mode 100644 index 0000000..6c6db57 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_btn_right_true.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_check_green.xml b/picture_library/src/main/res/drawable/picture_check_green.xml new file mode 100644 index 0000000..a22a66a --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_check_green.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_checkbox_selector.xml b/picture_library/src/main/res/drawable/picture_checkbox_selector.xml new file mode 100644 index 0000000..2b66353 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_checkbox_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_dialog_custom_bg.xml b/picture_library/src/main/res/drawable/picture_dialog_custom_bg.xml new file mode 100644 index 0000000..e83c956 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_dialog_custom_bg.xml @@ -0,0 +1,8 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_dialog_shadow.xml b/picture_library/src/main/res/drawable/picture_dialog_shadow.xml new file mode 100644 index 0000000..f552541 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_dialog_shadow.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_gif_tag.xml b/picture_library/src/main/res/drawable/picture_gif_tag.xml new file mode 100644 index 0000000..6618c5d --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_gif_tag.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_ic_camera.xml b/picture_library/src/main/res/drawable/picture_ic_camera.xml new file mode 100644 index 0000000..f8535a5 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_ic_camera.xml @@ -0,0 +1,9 @@ + + + diff --git a/picture_library/src/main/res/drawable/picture_ic_flash_auto.xml b/picture_library/src/main/res/drawable/picture_ic_flash_auto.xml new file mode 100644 index 0000000..d11caeb --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_ic_flash_auto.xml @@ -0,0 +1,9 @@ + + + diff --git a/picture_library/src/main/res/drawable/picture_ic_flash_off.xml b/picture_library/src/main/res/drawable/picture_ic_flash_off.xml new file mode 100644 index 0000000..e771850 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_ic_flash_off.xml @@ -0,0 +1,9 @@ + + + diff --git a/picture_library/src/main/res/drawable/picture_ic_flash_on.xml b/picture_library/src/main/res/drawable/picture_ic_flash_on.xml new file mode 100644 index 0000000..e936468 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_ic_flash_on.xml @@ -0,0 +1,9 @@ + + + diff --git a/picture_library/src/main/res/drawable/picture_icon_black_delete.xml b/picture_library/src/main/res/drawable/picture_icon_black_delete.xml new file mode 100644 index 0000000..603ee6d --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_icon_black_delete.xml @@ -0,0 +1,4 @@ + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_image_placeholder.xml b/picture_library/src/main/res/drawable/picture_image_placeholder.xml new file mode 100644 index 0000000..2a75dc5 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_image_placeholder.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_item_select_bg.xml b/picture_library/src/main/res/drawable/picture_item_select_bg.xml new file mode 100644 index 0000000..6f3f095 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_item_select_bg.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_layer_progress.xml b/picture_library/src/main/res/drawable/picture_layer_progress.xml new file mode 100644 index 0000000..6683a17 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_layer_progress.xml @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_num_oval.xml b/picture_library/src/main/res/drawable/picture_num_oval.xml new file mode 100644 index 0000000..b4d09ce --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_num_oval.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_orange_oval.xml b/picture_library/src/main/res/drawable/picture_orange_oval.xml new file mode 100644 index 0000000..6557c9d --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_orange_oval.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_original_blue_checkbox.xml b/picture_library/src/main/res/drawable/picture_original_blue_checkbox.xml new file mode 100644 index 0000000..c984121 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_original_blue_checkbox.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_original_checkbox.xml b/picture_library/src/main/res/drawable/picture_original_checkbox.xml new file mode 100644 index 0000000..1e3b5d2 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_original_checkbox.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_original_wechat_checkbox.xml b/picture_library/src/main/res/drawable/picture_original_wechat_checkbox.xml new file mode 100644 index 0000000..08b8a68 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_original_wechat_checkbox.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_original_wechat_normal.xml b/picture_library/src/main/res/drawable/picture_original_wechat_normal.xml new file mode 100644 index 0000000..16f5585 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_original_wechat_normal.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_original_wechat_selected.xml b/picture_library/src/main/res/drawable/picture_original_wechat_selected.xml new file mode 100644 index 0000000..860bf2f --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_original_wechat_selected.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_preview_gallery_border_bg.xml b/picture_library/src/main/res/drawable/picture_preview_gallery_border_bg.xml new file mode 100644 index 0000000..1f10506 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_preview_gallery_border_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_sb_thumb.xml b/picture_library/src/main/res/drawable/picture_sb_thumb.xml new file mode 100644 index 0000000..c01dd83 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_sb_thumb.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_seek_bar_thumb_normal.xml b/picture_library/src/main/res/drawable/picture_seek_bar_thumb_normal.xml new file mode 100644 index 0000000..1b4c44a --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_seek_bar_thumb_normal.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_seek_bar_thumb_pressed.xml b/picture_library/src/main/res/drawable/picture_seek_bar_thumb_pressed.xml new file mode 100644 index 0000000..d3770f4 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_seek_bar_thumb_pressed.xml @@ -0,0 +1,14 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_send_button_bg.xml b/picture_library/src/main/res/drawable/picture_send_button_bg.xml new file mode 100644 index 0000000..08d84e4 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_send_button_bg.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_send_button_default_bg.xml b/picture_library/src/main/res/drawable/picture_send_button_default_bg.xml new file mode 100644 index 0000000..f3141ab --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_send_button_default_bg.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_view_normal.xml b/picture_library/src/main/res/drawable/picture_view_normal.xml new file mode 100644 index 0000000..c14a27a --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_view_normal.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_view_press.xml b/picture_library/src/main/res/drawable/picture_view_press.xml new file mode 100644 index 0000000..913e31c --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_view_press.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_wechat_num_oval_normal.xml b/picture_library/src/main/res/drawable/picture_wechat_num_oval_normal.xml new file mode 100644 index 0000000..632dc79 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_wechat_num_oval_normal.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_wechat_num_oval_selected.xml b/picture_library/src/main/res/drawable/picture_wechat_num_oval_selected.xml new file mode 100644 index 0000000..9588c97 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_wechat_num_oval_selected.xml @@ -0,0 +1,13 @@ + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_wechat_num_selector.xml b/picture_library/src/main/res/drawable/picture_wechat_num_selector.xml new file mode 100644 index 0000000..b91459d --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_wechat_num_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/drawable/picture_wechat_select_cb.xml b/picture_library/src/main/res/drawable/picture_wechat_select_cb.xml new file mode 100644 index 0000000..4a6bed8 --- /dev/null +++ b/picture_library/src/main/res/drawable/picture_wechat_select_cb.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_activity_external_preview.xml b/picture_library/src/main/res/layout/picture_activity_external_preview.xml new file mode 100644 index 0000000..bca5d70 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_activity_external_preview.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_activity_video_play.xml b/picture_library/src/main/res/layout/picture_activity_video_play.xml new file mode 100644 index 0000000..0773072 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_activity_video_play.xml @@ -0,0 +1,44 @@ + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_album_folder_item.xml b/picture_library/src/main/res/layout/picture_album_folder_item.xml new file mode 100644 index 0000000..cb85c1e --- /dev/null +++ b/picture_library/src/main/res/layout/picture_album_folder_item.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_alert_dialog.xml b/picture_library/src/main/res/layout/picture_alert_dialog.xml new file mode 100644 index 0000000..6423338 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_alert_dialog.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_audio_dialog.xml b/picture_library/src/main/res/layout/picture_audio_dialog.xml new file mode 100644 index 0000000..a509f97 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_audio_dialog.xml @@ -0,0 +1,112 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_camera_view.xml b/picture_library/src/main/res/layout/picture_camera_view.xml new file mode 100644 index 0000000..0d75481 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_camera_view.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_dialog_camera_selected.xml b/picture_library/src/main/res/layout/picture_dialog_camera_selected.xml new file mode 100644 index 0000000..caaf2c0 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_dialog_camera_selected.xml @@ -0,0 +1,58 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_empty.xml b/picture_library/src/main/res/layout/picture_empty.xml new file mode 100644 index 0000000..668d0e7 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_empty.xml @@ -0,0 +1,5 @@ + + diff --git a/picture_library/src/main/res/layout/picture_image_grid_item.xml b/picture_library/src/main/res/layout/picture_image_grid_item.xml new file mode 100644 index 0000000..85e317a --- /dev/null +++ b/picture_library/src/main/res/layout/picture_image_grid_item.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_image_preview.xml b/picture_library/src/main/res/layout/picture_image_preview.xml new file mode 100644 index 0000000..3d9d8d9 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_image_preview.xml @@ -0,0 +1,25 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_item_camera.xml b/picture_library/src/main/res/layout/picture_item_camera.xml new file mode 100644 index 0000000..339d241 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_item_camera.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_play_audio.xml b/picture_library/src/main/res/layout/picture_play_audio.xml new file mode 100644 index 0000000..303f7f0 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_play_audio.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_preview.xml b/picture_library/src/main/res/layout/picture_preview.xml new file mode 100644 index 0000000..ccc5fc4 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_preview.xml @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_preview_title_bar.xml b/picture_library/src/main/res/layout/picture_preview_title_bar.xml new file mode 100644 index 0000000..d088036 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_preview_title_bar.xml @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_prompt_dialog.xml b/picture_library/src/main/res/layout/picture_prompt_dialog.xml new file mode 100644 index 0000000..8493543 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_prompt_dialog.xml @@ -0,0 +1,37 @@ + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_selector.xml b/picture_library/src/main/res/layout/picture_selector.xml new file mode 100644 index 0000000..3d2523d --- /dev/null +++ b/picture_library/src/main/res/layout/picture_selector.xml @@ -0,0 +1,102 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_title_bar.xml b/picture_library/src/main/res/layout/picture_title_bar.xml new file mode 100644 index 0000000..0a49c27 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_title_bar.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_wechat_preview_gallery.xml b/picture_library/src/main/res/layout/picture_wechat_preview_gallery.xml new file mode 100644 index 0000000..caf883f --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wechat_preview_gallery.xml @@ -0,0 +1,31 @@ + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_wechat_style_preview.xml b/picture_library/src/main/res/layout/picture_wechat_style_preview.xml new file mode 100644 index 0000000..dd96740 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wechat_style_preview.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_wechat_style_preview_title_bar.xml b/picture_library/src/main/res/layout/picture_wechat_style_preview_title_bar.xml new file mode 100644 index 0000000..6788a91 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wechat_style_preview_title_bar.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/picture_library/src/main/res/layout/picture_wechat_style_selector.xml b/picture_library/src/main/res/layout/picture_wechat_style_selector.xml new file mode 100644 index 0000000..f8f5e90 --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wechat_style_selector.xml @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_wechat_style_title_bar.xml b/picture_library/src/main/res/layout/picture_wechat_style_title_bar.xml new file mode 100644 index 0000000..fbb9b5d --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wechat_style_title_bar.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/picture_library/src/main/res/layout/picture_wind_base_dialog.xml b/picture_library/src/main/res/layout/picture_wind_base_dialog.xml new file mode 100644 index 0000000..b6ba96b --- /dev/null +++ b/picture_library/src/main/res/layout/picture_wind_base_dialog.xml @@ -0,0 +1,77 @@ + + + + + + + + + + + + + + +