() {
+
+ @Override
+ public void onSuccess(Result feedbackResp) {
+
+ }
+ @Override
+ public void onError(int code, String msg) {
+ getErrorMesageMutableLiveData().postValue(new ErrorMesage(code, msg));
+ }
+ });
+ }
+
+}
diff --git a/app/src/main/java/com/dskj/daikuan/viewModel/home/SwitchVideoModel.java b/app/src/main/java/com/dskj/daikuan/viewModel/home/SwitchVideoModel.java
new file mode 100644
index 0000000..e908339
--- /dev/null
+++ b/app/src/main/java/com/dskj/daikuan/viewModel/home/SwitchVideoModel.java
@@ -0,0 +1,39 @@
+package com.dskj.daikuan.viewModel.home;
+
+import androidx.annotation.NonNull;
+
+/**
+ * Created by shuyu on 2016/12/7.
+ */
+
+public class SwitchVideoModel {
+ private String url;
+ private String name;
+
+ public SwitchVideoModel(String name, String url) {
+ this.name = name;
+ this.url = url;
+ }
+
+ public String getUrl() {
+ return url;
+ }
+
+ public void setUrl(String url) {
+ this.url = url;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ @NonNull
+ @Override
+ public String toString() {
+ return this.name;
+ }
+}
diff --git a/app/src/main/java/com/hitomi/tilibrary/transfer/ProgressBarIndicatorNew.java b/app/src/main/java/com/hitomi/tilibrary/transfer/ProgressBarIndicatorNew.java
new file mode 100644
index 0000000..3576752
--- /dev/null
+++ b/app/src/main/java/com/hitomi/tilibrary/transfer/ProgressBarIndicatorNew.java
@@ -0,0 +1,69 @@
+package com.hitomi.tilibrary.transfer;
+
+import android.content.Context;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.FrameLayout;
+import android.widget.ProgressBar;
+
+import com.hitomi.tilibrary.style.IProgressIndicator;
+
+/**
+ * 图片加载时使用 Android 默认的 ProgressBar
+ *
+ * email: 196425254@qq.com
+ */
+public class ProgressBarIndicatorNew implements IProgressIndicator {
+
+// private SparseArray progressBarArray = new SparseArray<>();
+
+// private int dip2Px(Context context, float dpValue) {
+//// final float scale = context.getResources().getDisplayMetrics().density;
+//// return (int) (dpValue * scale + 0.5f);
+// }
+
+ @Override
+ public void attach(int position, FrameLayout parent) {
+// Context context = parent.getContext();
+//
+// int progressSize = dip2Px(context, 50);
+// FrameLayout.LayoutParams progressLp = new FrameLayout.LayoutParams(
+// progressSize, progressSize);
+// progressLp.gravity = Gravity.CENTER;
+//
+// ProgressBar progressBar = new ProgressBar(context);
+// progressBar.setLayoutParams(progressLp);
+//
+// parent.addView(progressBar, parent.getChildCount());
+// progressBarArray.put(position, progressBar);
+ }
+
+ @Override
+ public void hideView(int position) {
+// ProgressBar progressBar = progressBarArray.get(position);
+// if (progressBar != null)
+// progressBar.setVisibility(View.GONE);
+ }
+
+ @Override
+ public void onStart(int position) {
+ }
+
+ @Override
+ public void onProgress(int position, int progress) {
+ }
+
+ @Override
+ public void onFinish(int position) {
+// ProgressBar progressBar = progressBarArray.get(position);
+// if (progressBar == null) return;
+//
+// ViewGroup vg = (ViewGroup) progressBar.getParent();
+// ;
+// if (vg != null) {
+// vg.removeView(progressBar);
+// }
+ }
+}
diff --git a/app/src/main/java/com/hitomi/tilibrary/transfer/TransfereeNew.java b/app/src/main/java/com/hitomi/tilibrary/transfer/TransfereeNew.java
new file mode 100644
index 0000000..432211e
--- /dev/null
+++ b/app/src/main/java/com/hitomi/tilibrary/transfer/TransfereeNew.java
@@ -0,0 +1,276 @@
+package com.hitomi.tilibrary.transfer;
+
+import android.app.Activity;
+import android.app.Application;
+import android.app.Dialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.view.KeyEvent;
+import android.widget.ImageView;
+
+import androidx.appcompat.app.AlertDialog;
+
+import com.gyf.immersionbar.ImmersionBar;
+import com.hitomi.tilibrary.style.index.CircleIndexIndicator;
+import com.hitomi.tilibrary.style.progress.ProgressBarIndicator;
+import com.hitomi.tilibrary.utils.AppManager;
+import com.hitomi.tilibrary.utils.FileUtils;
+import com.hitomi.tilibrary.view.video.ExoVideoView;
+import com.hitomi.tilibrary.view.video.source.ExoSourceManager;
+
+import java.io.File;
+
+/**
+ * Main workflow:
+ * 1、点击缩略图展示缩略图到 transferee 过渡动画
+ * 2、显示下载高清图片进度
+ * 3、加载完成显示高清图片
+ * 4、高清图支持手势缩放
+ * 5、关闭 transferee 展示 transferee 到原缩略图的过渡动画
+ * Created by Vans Z on 2017/1/19.
+ *
+ * email: 196425254@qq.com
+ */
+public class TransfereeNew implements DialogInterface.OnShowListener,
+ DialogInterface.OnKeyListener,
+ TransferLayout.OnLayoutResetListener,
+ AppManager.OnAppStateChangeListener {
+
+ private Context context;
+ private Dialog transDialog;
+
+ private TransferLayout transLayout;
+ private TransferConfig transConfig;
+ private OnTransfereeStateChangeListener transListener;
+
+ // 因为Dialog的关闭有动画延迟,固不能使用 dialog.isShowing, 去判断 transferee 的显示逻辑
+ private boolean shown;
+
+ /**
+ * 构造方法私有化,通过{@link #getDefault(Context)} 创建 transferee
+ *
+ * @param context 上下文环境
+ */
+ private TransfereeNew(Context context) {
+ this.context = context;
+ createLayout();
+ createDialog();
+ AppManager.getInstance().init((Application) context.getApplicationContext());
+ }
+
+ /**
+ * @param context
+ * @return {@link TransfereeNew}
+ */
+ public static TransfereeNew getDefault(Context context) {
+ return new TransfereeNew(context);
+ }
+
+ private void createLayout() {
+ transLayout = new TransferLayout(context);
+ transLayout.setOnLayoutResetListener(this);
+ }
+
+ private void createDialog() {
+ transDialog = new AlertDialog.Builder(context,
+ android.R.style.Theme_Translucent_NoTitleBar_Fullscreen)
+ .setView(transLayout)
+ .create();
+ transDialog.setOnShowListener(this);
+ transDialog.setOnKeyListener(this);
+ }
+
+ /**
+ * 检查参数,如果必须参数缺少,就使用缺省参数或者抛出异常
+ */
+ private void checkConfig() {
+ if (transConfig == null)
+ throw new IllegalArgumentException("The parameter TransferConfig can't be null");
+ if (transConfig.isSourceEmpty())
+ throw new IllegalArgumentException("The parameter sourceUrlList or sourceUriList can't be empty");
+ if (transConfig.getImageLoader() == null)
+ throw new IllegalArgumentException("Need to specify an ImageLoader");
+
+ transConfig.setNowThumbnailIndex(Math.max(transConfig.getNowThumbnailIndex(), 0));
+ transConfig.setOffscreenPageLimit(transConfig.getOffscreenPageLimit() <= 0
+ ? 1 : transConfig.getOffscreenPageLimit());
+ transConfig.setDuration(transConfig.getDuration() <= 0
+ ? 300 : transConfig.getDuration());
+ transConfig.setProgressIndicator(transConfig.getProgressIndicator() == null
+ ? new ProgressBarIndicator() : transConfig.getProgressIndicator());
+ transConfig.setIndexIndicator(transConfig.getIndexIndicator() == null
+ ? new CircleIndexIndicator() : transConfig.getIndexIndicator());
+ }
+
+ /**
+ * 配置 transferee 参数对象
+ *
+ * @param config 参数对象
+ * @return transferee
+ */
+ public TransfereeNew apply(TransferConfig config) {
+ if (!shown) {
+ transConfig = config;
+ OriginalViewHelper.getInstance().fillOriginImages(config);
+ checkConfig();
+ transLayout.apply(config);
+ }
+ return this;
+ }
+
+ /**
+ * transferee 是否显示
+ *
+ * @return true :显示, false :关闭
+ */
+ public boolean isShown() {
+ return shown;
+ }
+
+ /**
+ * 显示 transferee
+ */
+ public void show() {
+ if (shown) return;
+ transDialog.show();
+ adjustTopAndBottom();
+ if (transListener != null) {
+ transListener.onShow();
+ }
+ shown = true;
+ }
+
+ /**
+ * 显示 transferee, 并设置 OnTransfereeChangeListener
+ *
+ * @param listener {@link OnTransfereeStateChangeListener}
+ */
+ public void show(OnTransfereeStateChangeListener listener) {
+ if (shown || listener == null) return;
+ transDialog.show();
+ adjustTopAndBottom();
+ transListener = listener;
+ transListener.onShow();
+ shown = true;
+ }
+
+ /**
+ * 关闭 transferee
+ */
+ public void dismiss() {
+ if (shown && transLayout.dismiss(transConfig.getNowThumbnailIndex())) {
+ shown = false;
+ }
+ }
+
+ /**
+ * 获取图片文件
+ */
+ public File getImageFile(String imageUrl) {
+ return transConfig.getImageLoader().getCache(imageUrl);
+ }
+
+ /**
+ * 清除 transferee 缓存,包括图片和视频文件缓存,注意清除视频缓存必须保证 transferee 是关闭状态
+ */
+ public void clear() {
+ if (transConfig != null && transConfig.getImageLoader() != null) {
+ transConfig.getImageLoader().clearCache();
+ }
+ File cacheFile = new File(context.getCacheDir(), ExoVideoView.CACHE_DIR);
+ if (cacheFile.exists() && !shown) {
+ FileUtils.deleteDir(new File(cacheFile, VideoThumbState.FRAME_DIR));
+ ExoSourceManager.clearCache(context, cacheFile, null);
+ }
+ }
+
+ /**
+ * dialog 打开时的监听器
+ */
+ @Override
+ public void onShow(DialogInterface dialog) {
+ AppManager.getInstance().register(this);
+ transLayout.show();
+ }
+
+ /**
+ * 调整顶部和底部内边距
+ */
+ private void adjustTopAndBottom() {
+ if (context instanceof Activity) {
+ // 隐藏状态栏和导航栏,全屏化
+ Activity activity = (Activity) context;
+ ImmersionBar.with(activity, transDialog)
+ .fullScreen(true)
+ .init();
+// int top = ImmersionBar.getNotchHeight(activity);
+// int bottom = ImmersionBar.getNavigationBarHeight(activity);
+ transLayout.setPadding(0, 0, 0, 0);
+ }
+ }
+
+ @Override
+ public void onReset() {
+ AppManager.getInstance().unregister(this);
+ transDialog.dismiss();
+ if (transListener != null)
+ transListener.onDismiss();
+ shown = false;
+ }
+
+ @Override
+ public boolean onKey(DialogInterface dialog, int keyCode, KeyEvent event) {
+ if (keyCode == KeyEvent.KEYCODE_BACK &&
+ event.getAction() == KeyEvent.ACTION_UP &&
+ !event.isCanceled()) {
+ dismiss();
+ return true;
+ }
+ return false;
+ }
+
+ @Override
+ public void onForeground() {
+ transLayout.pauseOrPlayVideo(false);
+ }
+
+ @Override
+ public void onBackground() {
+ transLayout.pauseOrPlayVideo(true);
+ }
+
+ /**
+ * 设置 Transferee 显示和关闭的监听器
+ *
+ * @param listener {@link OnTransfereeStateChangeListener}
+ */
+ public void setOnTransfereeStateChangeListener(OnTransfereeStateChangeListener listener) {
+ transListener = listener;
+ }
+
+ /**
+ * 资源销毁,防止内存泄漏
+ */
+ public void destroy() {
+ if (transConfig != null) {
+ transConfig.destroy();
+ transConfig = null;
+ }
+ }
+
+ /**
+ * Transferee 显示的时候调用 {@link OnTransfereeStateChangeListener#onShow()}
+ *
+ * Transferee 关闭的时候调用 {@link OnTransfereeStateChangeListener#onDismiss()}
+ */
+ public interface OnTransfereeStateChangeListener {
+ void onShow();
+
+ void onDismiss();
+ }
+
+ public interface OnTransfereeLongClickListener {
+ void onLongClick(ImageView imageView, String imageUri, int pos);
+ }
+
+}
diff --git a/app/src/main/res/anim/animate.xml b/app/src/main/res/anim/animate.xml
new file mode 100644
index 0000000..490eac7
--- /dev/null
+++ b/app/src/main/res/anim/animate.xml
@@ -0,0 +1,7 @@
+
+
diff --git a/app/src/main/res/anim/item_animation_fall_down.xml b/app/src/main/res/anim/item_animation_fall_down.xml
new file mode 100644
index 0000000..87c9ed7
--- /dev/null
+++ b/app/src/main/res/anim/item_animation_fall_down.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/color/select_color.xml b/app/src/main/res/color/select_color.xml
new file mode 100644
index 0000000..044e023
--- /dev/null
+++ b/app/src/main/res/color/select_color.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/color/select_color1.xml b/app/src/main/res/color/select_color1.xml
new file mode 100644
index 0000000..5d2a127
--- /dev/null
+++ b/app/src/main/res/color/select_color1.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
new file mode 100644
index 0000000..2b068d1
--- /dev/null
+++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/actionbar_bg.xml b/app/src/main/res/drawable/actionbar_bg.xml
new file mode 100644
index 0000000..a56c2dc
--- /dev/null
+++ b/app/src/main/res/drawable/actionbar_bg.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/bg_ripple.xml b/app/src/main/res/drawable/bg_ripple.xml
new file mode 100644
index 0000000..91536ad
--- /dev/null
+++ b/app/src/main/res/drawable/bg_ripple.xml
@@ -0,0 +1,6 @@
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/dlg_input_video_bg.xml b/app/src/main/res/drawable/dlg_input_video_bg.xml
new file mode 100644
index 0000000..1e5d9cf
--- /dev/null
+++ b/app/src/main/res/drawable/dlg_input_video_bg.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml
new file mode 100644
index 0000000..07d5da9
--- /dev/null
+++ b/app/src/main/res/drawable/ic_launcher_background.xml
@@ -0,0 +1,170 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/inputbg.xml b/app/src/main/res/drawable/inputbg.xml
new file mode 100644
index 0000000..a1bb3d9
--- /dev/null
+++ b/app/src/main/res/drawable/inputbg.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/inputbg_false.xml b/app/src/main/res/drawable/inputbg_false.xml
new file mode 100644
index 0000000..90f7b60
--- /dev/null
+++ b/app/src/main/res/drawable/inputbg_false.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/inputbg_true.xml b/app/src/main/res/drawable/inputbg_true.xml
new file mode 100644
index 0000000..11417f4
--- /dev/null
+++ b/app/src/main/res/drawable/inputbg_true.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/item_bg_default.xml b/app/src/main/res/drawable/item_bg_default.xml
new file mode 100644
index 0000000..4e85c89
--- /dev/null
+++ b/app/src/main/res/drawable/item_bg_default.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/item_bg_default1.xml b/app/src/main/res/drawable/item_bg_default1.xml
new file mode 100644
index 0000000..7dc802e
--- /dev/null
+++ b/app/src/main/res/drawable/item_bg_default1.xml
@@ -0,0 +1,25 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/jz_bottom_seek_progress.xml b/app/src/main/res/drawable/jz_bottom_seek_progress.xml
new file mode 100644
index 0000000..8f4948e
--- /dev/null
+++ b/app/src/main/res/drawable/jz_bottom_seek_progress.xml
@@ -0,0 +1,28 @@
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/jz_seek_post.xml b/app/src/main/res/drawable/jz_seek_post.xml
new file mode 100644
index 0000000..51affe0
--- /dev/null
+++ b/app/src/main/res/drawable/jz_seek_post.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/save_btn_back.xml b/app/src/main/res/drawable/save_btn_back.xml
new file mode 100644
index 0000000..9d9f5e5
--- /dev/null
+++ b/app/src/main/res/drawable/save_btn_back.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/search_bg.xml b/app/src/main/res/drawable/search_bg.xml
new file mode 100644
index 0000000..69d84da
--- /dev/null
+++ b/app/src/main/res/drawable/search_bg.xml
@@ -0,0 +1,19 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/tab_line.xml b/app/src/main/res/drawable/tab_line.xml
new file mode 100644
index 0000000..fdf31c3
--- /dev/null
+++ b/app/src/main/res/drawable/tab_line.xml
@@ -0,0 +1,32 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/tab_line1.xml b/app/src/main/res/drawable/tab_line1.xml
new file mode 100644
index 0000000..6156b0c
--- /dev/null
+++ b/app/src/main/res/drawable/tab_line1.xml
@@ -0,0 +1,15 @@
+
+
+
+ -
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_login.xml b/app/src/main/res/layout/activity_login.xml
new file mode 100644
index 0000000..634e43a
--- /dev/null
+++ b/app/src/main/res/layout/activity_login.xml
@@ -0,0 +1,120 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_main2.xml b/app/src/main/res/layout/activity_main2.xml
new file mode 100644
index 0000000..e30b0b2
--- /dev/null
+++ b/app/src/main/res/layout/activity_main2.xml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_shenqing.xml b/app/src/main/res/layout/activity_shenqing.xml
new file mode 100644
index 0000000..39ae84f
--- /dev/null
+++ b/app/src/main/res/layout/activity_shenqing.xml
@@ -0,0 +1,361 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/activity_start_up.xml b/app/src/main/res/layout/activity_start_up.xml
new file mode 100644
index 0000000..532eb34
--- /dev/null
+++ b/app/src/main/res/layout/activity_start_up.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_xiangqing.xml b/app/src/main/res/layout/activity_xiangqing.xml
new file mode 100644
index 0000000..c169266
--- /dev/null
+++ b/app/src/main/res/layout/activity_xiangqing.xml
@@ -0,0 +1,353 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/agent_child_item.xml b/app/src/main/res/layout/agent_child_item.xml
new file mode 100644
index 0000000..d7f6a2f
--- /dev/null
+++ b/app/src/main/res/layout/agent_child_item.xml
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/layout_action_bar1.xml b/app/src/main/res/layout/layout_action_bar1.xml
new file mode 100644
index 0000000..3d5720f
--- /dev/null
+++ b/app/src/main/res/layout/layout_action_bar1.xml
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/layout_custom.xml b/app/src/main/res/layout/layout_custom.xml
new file mode 100644
index 0000000..1659a5a
--- /dev/null
+++ b/app/src/main/res/layout/layout_custom.xml
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/switch_video_dialog.xml b/app/src/main/res/layout/switch_video_dialog.xml
new file mode 100644
index 0000000..244d122
--- /dev/null
+++ b/app/src/main/res/layout/switch_video_dialog.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/switch_video_dialog_item.xml b/app/src/main/res/layout/switch_video_dialog_item.xml
new file mode 100644
index 0000000..c891468
--- /dev/null
+++ b/app/src/main/res/layout/switch_video_dialog_item.xml
@@ -0,0 +1,14 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..eca70cf
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp
new file mode 100644
index 0000000..c209e78
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..b2dfe3d
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp
new file mode 100644
index 0000000..4f0f1d6
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..62b611d
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
new file mode 100644
index 0000000..948a307
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
new file mode 100644
index 0000000..1b9a695
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ
diff --git a/app/src/main/res/mipmap-xhdpi/more_live_right_img.png b/app/src/main/res/mipmap-xhdpi/more_live_right_img.png
new file mode 100644
index 0000000..2bdcb88
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/more_live_right_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/add_img.png b/app/src/main/res/mipmap-xxhdpi/add_img.png
new file mode 100644
index 0000000..0017a4a
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/add_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/edit_false.png b/app/src/main/res/mipmap-xxhdpi/edit_false.png
new file mode 100644
index 0000000..a5d1d28
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/edit_false.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/edit_true.png b/app/src/main/res/mipmap-xxhdpi/edit_true.png
new file mode 100644
index 0000000..e777d9d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/edit_true.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_black_back.png b/app/src/main/res/mipmap-xxhdpi/ic_black_back.png
new file mode 100644
index 0000000..19a478d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_black_back.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_black_back_white.png b/app/src/main/res/mipmap-xxhdpi/ic_black_back_white.png
new file mode 100644
index 0000000..67d2245
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_black_back_white.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/login_bot.png b/app/src/main/res/mipmap-xxhdpi/login_bot.png
new file mode 100644
index 0000000..51ab8d4
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/login_bot.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/logo_img.png b/app/src/main/res/mipmap-xxhdpi/logo_img.png
new file mode 100644
index 0000000..d9c2f57
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/logo_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/nodate_img.png b/app/src/main/res/mipmap-xxhdpi/nodate_img.png
new file mode 100644
index 0000000..03da076
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/nodate_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/select_img.png b/app/src/main/res/mipmap-xxhdpi/select_img.png
new file mode 100644
index 0000000..0f7297d
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/select_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/sese.png b/app/src/main/res/mipmap-xxhdpi/sese.png
new file mode 100644
index 0000000..28fa8df
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/sese.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/sfz_add_img.png b/app/src/main/res/mipmap-xxhdpi/sfz_add_img.png
new file mode 100644
index 0000000..8dcb155
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/sfz_add_img.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/sfzbm.png b/app/src/main/res/mipmap-xxhdpi/sfzbm.png
new file mode 100644
index 0000000..4d1f2ed
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/sfzbm.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/sfzzm.png b/app/src/main/res/mipmap-xxhdpi/sfzzm.png
new file mode 100644
index 0000000..cfcfb78
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/sfzzm.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/top_bg.png b/app/src/main/res/mipmap-xxhdpi/top_bg.png
new file mode 100644
index 0000000..4054a44
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/top_bg.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/new_bg_index.png b/app/src/main/res/mipmap-xxxhdpi/new_bg_index.png
new file mode 100644
index 0000000..0a71696
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/new_bg_index.png differ
diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml
new file mode 100644
index 0000000..72cfd0f
--- /dev/null
+++ b/app/src/main/res/values-night/colors.xml
@@ -0,0 +1,76 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #FF000000
+ #000000
+ #E83126
+ #2062D3
+ #303F9F
+ #FF4081
+ #7B7888
+ #8C8D92
+ #00000000
+ #222222
+ #F4F6F8
+ #24272E
+ #DD493D
+ #F5F3F8
+ #FE6A9E
+ #99FFFFFF
+ #993376EA
+ #e1e3e6
+ #eeeeee
+ #AAAAAA
+ #F8F8F8
+ #BBBBBB
+ #C4C4C4
+ #66000000
+ #F4F4F4
+ #E7EAF9
+ #777777
+ #E8ECFF
+ #2B2B2B
+ #3376EA
+ #2D3338
+ #1D1F25
+ #CCCCCC
+ #99000000
+ #CF8823
+ #616CC1
+ #666666
+
+ #0066FF
+ #AF593E
+ #01A368
+ #FF861F
+ #ED0A3F
+ #FF3F34
+ #76D7EA
+ #8359A3
+ #FBE870
+ #C5E17A
+ #151414
+ #99000000
+
+
+ #F8F8F8
+ #FFFFFF
+
+
+ #F8F8F8
+
+ #F3F3F3
+ #FFFFFF
+ #E83126
+ #e6000000
+ #66FFFFFF
+ #EEEEEE
+ #DDDDDD
+
+ #45000000
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml
new file mode 100644
index 0000000..693f630
--- /dev/null
+++ b/app/src/main/res/values-night/themes.xml
@@ -0,0 +1,16 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-zh/values-zh.xml b/app/src/main/res/values-zh/values-zh.xml
new file mode 100644
index 0000000..afbec21
--- /dev/null
+++ b/app/src/main/res/values-zh/values-zh.xml
@@ -0,0 +1,4 @@
+
+
+ 确定
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 0000000..389eb85
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,77 @@
+
+
+ #FFBB86FC
+ #FF6200EE
+ #FF3700B3
+ #FF03DAC5
+ #FF018786
+ #000019
+ #FFFFFF
+ #E83126
+ #2062D3
+ #303F9F
+ #FF4081
+ #7B7888
+ #8C8D92
+ #00000000
+ #222222
+ #F4F6F8
+ #24272E
+ #DD493D
+ #F5F3F8
+ #FE6A9E
+ #99FFFFFF
+ #993376EA
+ #e1e3e6
+ #eeeeee
+ #AAAAAA
+ #F8F8F8
+ #333333
+ #C4C4C4
+ #66000000
+ #F4F4F4
+ #E7EAF9
+ #777777
+ #E8ECFF
+ #2B2B2B
+ #3376EA
+ #2D3338
+ #1D1F25
+ #CCCCCC
+ #99000000
+ #CF8823
+ #616CC1
+ #666666
+
+ #0066FF
+ #AF593E
+ #01A368
+ #FF861F
+ #ED0A3F
+ #FF3F34
+ #76D7EA
+ #8359A3
+ #FBE870
+ #C5E17A
+ #151414
+ #99000000
+
+
+ #F8F8F8
+ #FFFFFF
+
+
+ #F8F8F8
+
+ #F3F3F3
+ #FE8B59
+ #E83126
+ #959595
+ #66FFFFFF
+ #EEEEEE
+ #DDDDDD
+ #E12D48
+
+ #45000000
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..e946595
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,116 @@
+
+
+ 16dp
+ 16dp
+
+
+
+
+ 10sp
+ 11sp
+ 12sp
+ 13sp
+ 14sp
+ 15sp
+ 16sp
+ 17sp
+ 18sp
+ 19sp
+ 20sp
+ 22sp
+ 25sp
+ 36sp
+ 38sp
+
+
+ 0dp
+ 1dp
+ 2dp
+ 3dp
+ 4dp
+ 5dp
+ 6dp
+ 7dp
+ 7.5dp
+
+ 8dp
+ 10dp
+ 11dp
+
+ 12dp
+ 14dp
+
+ 15dp
+ 16dp
+ 18dp
+ 20dp
+ 22dp
+ 24dp
+
+ 25dp
+ 28dp
+ 30dp
+ 32dp
+ 33dp
+
+ 35dp
+ 36dp
+ 38dp
+ 40dp
+ 42dp
+ 44dp
+ 45dp
+ 48dp
+ 50dp
+ 50dp
+
+ 52dp
+ 55dp
+ 60dp
+ 65dp
+ 68dp
+
+ 70dp
+ 75dp
+ 80dp
+ 95dp
+
+ 95dp
+
+ 90dp
+ 100dp
+ 102dp
+
+ 105dp
+ 106dp
+ 110dp
+
+ 120dp
+ 130dp
+ 132dp
+
+
+ 150dp
+ 154dp
+
+ 160dp
+ 175dp
+ 180dp
+ 193dp
+
+ 200dp
+ 210dp
+ 250dp
+
+
+ 10sp
+ 10sp
+
+ 16dp
+ 30dp
+ 8dp
+ 16dp
+ 50dp
+
+
+
diff --git a/app/src/main/res/values/font_certs.xml b/app/src/main/res/values/font_certs.xml
new file mode 100644
index 0000000..d2226ac
--- /dev/null
+++ b/app/src/main/res/values/font_certs.xml
@@ -0,0 +1,17 @@
+
+
+
+ - @array/com_google_android_gms_fonts_certs_dev
+ - @array/com_google_android_gms_fonts_certs_prod
+
+
+ -
+ MIIEqDCCA5CgAwIBAgIJANWFuGx90071MA0GCSqGSIb3DQEBBAUAMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTAeFw0wODA0MTUyMzM2NTZaFw0zNTA5MDEyMzM2NTZaMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbTCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBANbOLggKv+IxTdGNs8/TGFy0PTP6DHThvbbR24kT9ixcOd9W+EaBPWW+wPPKQmsHxajtWjmQwWfna8mZuSeJS48LIgAZlKkpFeVyxW0qMBujb8X8ETrWy550NaFtI6t9+u7hZeTfHwqNvacKhp1RbE6dBRGWynwMVX8XW8N1+UjFaq6GCJukT4qmpN2afb8sCjUigq0GuMwYXrFVee74bQgLHWGJwPmvmLHC69EH6kWr22ijx4OKXlSIx2xT1AsSHee70w5iDBiK4aph27yH3TxkXy9V89TDdexAcKk/cVHYNnDBapcavl7y0RiQ4biu8ymM8Ga/nmzhRKya6G0cGw8CAQOjgfwwgfkwHQYDVR0OBBYEFI0cxb6VTEM8YYY6FbBMvAPyT+CyMIHJBgNVHSMEgcEwgb6AFI0cxb6VTEM8YYY6FbBMvAPyT+CyoYGapIGXMIGUMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEQMA4GA1UEChMHQW5kcm9pZDEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDEiMCAGCSqGSIb3DQEJARYTYW5kcm9pZEBhbmRyb2lkLmNvbYIJANWFuGx90071MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEEBQADggEBABnTDPEF+3iSP0wNfdIjIz1AlnrPzgAIHVvXxunW7SBrDhEglQZBbKJEk5kT0mtKoOD1JMrSu1xuTKEBahWRbqHsXclaXjoBADb0kkjVEJu/Lh5hgYZnOjvlba8Ld7HCKePCVePoTJBdI4fvugnL8TsgK05aIskyY0hKI9L8KfqfGTl1lzOv2KoWD0KWwtAWPoGChZxmQ+nBli+gwYMzM1vAkP+aayLe0a1EQimlOalO762r0GXO0ks+UeXde2Z4e+8S/pf7pITEI/tP+MxJTALw9QUWEv9lKTk+jkbqxbsh8nfBUapfKqYn0eidpwq2AzVp3juYl7//fKnaPhJD9gs=
+
+
+
+ -
+ MIIEQzCCAyugAwIBAgIJAMLgh0ZkSjCNMA0GCSqGSIb3DQEBBAUAMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDAeFw0wODA4MjEyMzEzMzRaFw0zNjAxMDcyMzEzMzRaMHQxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpDYWxpZm9ybmlhMRYwFAYDVQQHEw1Nb3VudGFpbiBWaWV3MRQwEgYDVQQKEwtHb29nbGUgSW5jLjEQMA4GA1UECxMHQW5kcm9pZDEQMA4GA1UEAxMHQW5kcm9pZDCCASAwDQYJKoZIhvcNAQEBBQADggENADCCAQgCggEBAKtWLgDYO6IIrgqWbxJOKdoR8qtW0I9Y4sypEwPpt1TTcvZApxsdyxMJZ2JORland2qSGT2y5b+3JKkedxiLDmpHpDsz2WCbdxgxRczfey5YZnTJ4VZbH0xqWVW/8lGmPav5xVwnIiJS6HXk+BVKZF+JcWjAsb/GEuq/eFdpuzSqeYTcfi6idkyugwfYwXFU1+5fZKUaRKYCwkkFQVfcAs1fXA5V+++FGfvjJ/CxURaSxaBvGdGDhfXE28LWuT9ozCl5xw4Yq5OGazvV24mZVSoOO0yZ31j7kYvtwYK6NeADwbSxDdJEqO4k//0zOHKrUiGYXtqw/A0LFFtqoZKFjnkCAQOjgdkwgdYwHQYDVR0OBBYEFMd9jMIhF1Ylmn/Tgt9r45jk14alMIGmBgNVHSMEgZ4wgZuAFMd9jMIhF1Ylmn/Tgt9r45jk14aloXikdjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTEWMBQGA1UEBxMNTW91bnRhaW4gVmlldzEUMBIGA1UEChMLR29vZ2xlIEluYy4xEDAOBgNVBAsTB0FuZHJvaWQxEDAOBgNVBAMTB0FuZHJvaWSCCQDC4IdGZEowjTAMBgNVHRMEBTADAQH/MA0GCSqGSIb3DQEBBAUAA4IBAQBt0lLO74UwLDYKqs6Tm8/yzKkEu116FmH4rkaymUIE0P9KaMftGlMexFlaYjzmB2OxZyl6euNXEsQH8gjwyxCUKRJNexBiGcCEyj6z+a1fuHHvkiaai+KL8W1EyNmgjmyy8AW7P+LLlkR+ho5zEHatRbM/YAnqGcFh5iZBqpknHf1SKMXFh4dd239FJ1jWYfbMDMy3NS5CTMQ2XFI1MvcyUTdZPErjQfTbQe3aDQsQcafEQPD+nqActifKZ0Np0IS9L9kR/wbNvyz6ENwPiTrjV2KRkEjH78ZMcUQXg0L3BYHJ3lc69Vs5Ddf9uUGGMYldX3WfMBEmh/9iFBDAaTCK
+
+
+
diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml
new file mode 100644
index 0000000..84abe66
--- /dev/null
+++ b/app/src/main/res/values/ids.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 0000000..68b2bda
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,11 @@
+
+ 贷款
+
+ 文件权限已禁用
+ 定位权限已禁用
+ 通讯录权限已禁用
+
+ 文件权限已禁用,请去设置中开启
+ 定位权限已禁用,请去设置中开启
+ 通讯录权限已禁用,请去设置中开启
+
\ No newline at end of file
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
new file mode 100644
index 0000000..a62aa32
--- /dev/null
+++ b/app/src/main/res/values/themes.xml
@@ -0,0 +1,823 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ />
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/file_paths.xml b/app/src/main/res/xml/file_paths.xml
new file mode 100644
index 0000000..24d04b4
--- /dev/null
+++ b/app/src/main/res/xml/file_paths.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/file_paths_csj.xml b/app/src/main/res/xml/file_paths_csj.xml
new file mode 100644
index 0000000..81c21ec
--- /dev/null
+++ b/app/src/main/res/xml/file_paths_csj.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/gdt_file_path.xml b/app/src/main/res/xml/gdt_file_path.xml
new file mode 100644
index 0000000..d512ee9
--- /dev/null
+++ b/app/src/main/res/xml/gdt_file_path.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/network_security_config.xml b/app/src/main/res/xml/network_security_config.xml
new file mode 100644
index 0000000..dca93c0
--- /dev/null
+++ b/app/src/main/res/xml/network_security_config.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/app/src/test/java/com/bikao/cleanmark/ExampleUnitTest.java b/app/src/test/java/com/bikao/cleanmark/ExampleUnitTest.java
new file mode 100644
index 0000000..500f069
--- /dev/null
+++ b/app/src/test/java/com/bikao/cleanmark/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package com.bikao.cleanmark;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see Testing documentation
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() {
+ assertEquals(4, 2 + 2);
+ }
+}
\ No newline at end of file
diff --git a/app/videomark.jks b/app/videomark.jks
new file mode 100644
index 0000000..7f93371
Binary files /dev/null and b/app/videomark.jks differ
diff --git a/basicLib/.gitignore b/basicLib/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/basicLib/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/basicLib/build.gradle b/basicLib/build.gradle
new file mode 100644
index 0000000..e41a485
--- /dev/null
+++ b/basicLib/build.gradle
@@ -0,0 +1,58 @@
+apply plugin: 'com.android.library'
+
+android {
+ compileSdkVersion 31
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 31
+ versionCode 1
+ versionName "1.0"
+
+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
+
+ }
+
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ //启用DataBinding
+ buildFeatures{
+ dataBinding = true
+ // for view binding :
+ // viewBinding = true
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ api 'android.arch.lifecycle:extensions:1.1.1'
+ api 'com.squareup.retrofit2:retrofit:2.5.0'
+ api 'com.squareup.retrofit2:converter-scalars:2.3.0'
+ api 'com.squareup.retrofit2:converter-gson:2.4.0'
+ api 'com.squareup.retrofit2:adapter-rxjava2:2.4.0'
+ api 'io.reactivex.rxjava2:rxjava:2.1.16'
+ api 'io.reactivex.rxjava2:rxandroid:2.0.2'
+
+ api 'androidx.recyclerview:recyclerview:1.1.0'
+ api 'com.github.bumptech.glide:glide:4.11.0'
+ annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'
+// api 'com.android.support:design:28.0.0'
+ api 'androidx.appcompat:appcompat:1.1.0'
+ api 'androidx.constraintlayout:constraintlayout:1.1.3'
+ api 'androidx.viewpager2:viewpager2:1.0.0'
+ api 'com.google.android.material:material:1.1.0'
+ testImplementation "junit:junit:4.13.1"
+ androidTestImplementation 'androidx.test.ext:junit:1.1.1'
+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
+ api 'io.github.hariprasanths:bounceview-android:0.2.0'
+
+}
diff --git a/basicLib/proguard-rules.pro b/basicLib/proguard-rules.pro
new file mode 100644
index 0000000..cf50408
--- /dev/null
+++ b/basicLib/proguard-rules.pro
@@ -0,0 +1,22 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+
diff --git a/basicLib/src/main/AndroidManifest.xml b/basicLib/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..1e3d513
--- /dev/null
+++ b/basicLib/src/main/AndroidManifest.xml
@@ -0,0 +1,2 @@
+
diff --git a/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVAdapter.java b/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVAdapter.java
new file mode 100644
index 0000000..73ab453
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVAdapter.java
@@ -0,0 +1,132 @@
+package com.azhon.basic.adapter;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+import androidx.databinding.ViewDataBinding;
+import androidx.recyclerview.widget.RecyclerView;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.adapter
+ * 文件名: BaseDBRVAdapter
+ * 创建时间: 2019-03-27 on 16:22
+ * 描述: TODO 结合dataBinding的RecyclerView Adapter
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseDBRVAdapter extends RecyclerView.Adapter {
+
+ private List data;
+ private int itemId;
+ protected Context context;
+ protected int variableId;
+ protected OnItemClickListener listener;
+
+
+ public BaseDBRVAdapter(@LayoutRes int itemId, int variableId) {
+ this.itemId = itemId;
+ this.variableId = variableId;
+ data = new ArrayList<>();
+ }
+
+ public BaseDBRVAdapter(List data, @LayoutRes int itemId, int variableId) {
+ this.data = data == null ? new ArrayList() : data;
+ this.itemId = itemId;
+ this.variableId = variableId;
+ }
+
+
+ @NonNull
+ @Override
+ public BaseDBRVHolder onCreateViewHolder(@NonNull ViewGroup parent, int position) {
+ this.context = parent.getContext();
+ LayoutInflater inflater = LayoutInflater.from(context);
+ DB binding = DataBindingUtil.inflate(inflater, itemId, parent, false);
+ return new BaseDBRVHolder(binding.getRoot());
+ }
+
+ @Override
+ public void onBindViewHolder(@NonNull BaseDBRVHolder holder, final int position) {
+ DB binding = DataBindingUtil.getBinding(holder.itemView);
+ final Data itemData = data.get(position);
+ binding.setVariable(variableId, itemData);
+ onBindViewHolder(itemData, binding, position);
+ //迫使数据立即绑定
+ binding.executePendingBindings();
+ //设置点击事件
+ if (listener != null) {
+ holder.itemView.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ listener.onItemClick(itemData, position);
+ }
+ });
+ holder.itemView.setOnLongClickListener(new View.OnLongClickListener() {
+ @Override
+ public boolean onLongClick(View v) {
+ return listener.onItemLongClick(itemData, position);
+ }
+ });
+ }
+ }
+
+
+ @Override
+ public int getItemCount() {
+ return data.size();
+ }
+
+ /**
+ * 绑定数据
+ */
+ protected void onBindViewHolder(Data data, DB binding, int position) {
+ }
+
+ /**
+ * 设置新数据
+ *
+ * @param data
+ */
+ public void setNewData(List data) {
+ this.data.clear();
+ this.data.addAll(data);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * 添加数据
+ *
+ * @param data
+ */
+ public void addData(Data data) {
+ this.data.add(data);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * 添加数据
+ *
+ * @param data
+ */
+ public void addData(List data) {
+ this.data.addAll(data);
+ notifyDataSetChanged();
+ }
+
+ /**
+ * 设置Item 长按、点击事件
+ */
+ public void setOnItemListener(OnItemClickListener listener) {
+ this.listener = listener;
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVHolder.java b/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVHolder.java
new file mode 100644
index 0000000..fe7b4d9
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/adapter/BaseDBRVHolder.java
@@ -0,0 +1,22 @@
+package com.azhon.basic.adapter;
+
+import android.view.View;
+
+import androidx.recyclerview.widget.RecyclerView;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.adapter
+ * 文件名: BaseDBRVHolder
+ * 创建时间: 2019-03-27 on 22:12
+ * 描述: TODO 结合dataBinding的RecyclerView Holder
+ *
+ * @author xuhuixiang
+ */
+
+public final class BaseDBRVHolder extends RecyclerView.ViewHolder {
+
+ public BaseDBRVHolder(View itemView) {
+ super(itemView);
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/adapter/OnItemClickListener.java b/basicLib/src/main/java/com/azhon/basic/adapter/OnItemClickListener.java
new file mode 100644
index 0000000..a46523b
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/adapter/OnItemClickListener.java
@@ -0,0 +1,31 @@
+package com.azhon.basic.adapter;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.adapter
+ * 文件名: OnItemClickListener
+ * 创建时间: 2019-03-29 on 13:53
+ * 描述: TODO RecyclerView Item 长按、点击事件
+ *
+ * @author xuhuixiang
+ */
+
+public interface OnItemClickListener {
+
+ /**
+ * Item 点击事件
+ *
+ * @param data item的数据
+ * @param position item的下标
+ */
+ void onItemClick(Data data, int position);
+
+ /**
+ * Item 长按事件
+ *
+ * @param data item的数据
+ * @param position item的下标
+ */
+ boolean onItemLongClick(Data data, int position);
+
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/base/BaseActivity.java b/basicLib/src/main/java/com/azhon/basic/base/BaseActivity.java
new file mode 100644
index 0000000..0fb8683
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/base/BaseActivity.java
@@ -0,0 +1,70 @@
+package com.azhon.basic.base;
+
+import android.app.Activity;
+import android.os.Build;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.databinding.ViewDataBinding;
+import androidx.lifecycle.Observer;
+
+import com.azhon.basic.bean.DialogBean;
+import com.azhon.basic.lifecycle.BaseViewModel;
+
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.base
+ * 文件名: BaseActivity
+ * 创建时间: 2019-03-27 on 10:46
+ * 描述: TODO ViewModel、ViewDataBinding都需要的基类
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseActivity
+ extends BaseNoModelActivity {
+
+ protected VM viewModel;
+
+ @Override
+ protected DB initDataBinding(int layoutId) {
+ DB db = super.initDataBinding(layoutId);
+ /**
+ * 将这两个初始化函数插在{@link BaseActivity#initDataBinding}
+ */
+ viewModel = initViewModel();
+ initObserve();
+ return db;
+ }
+
+
+
+ /**
+ * 初始化ViewModel
+ */
+ protected abstract VM initViewModel();
+
+ /**
+ * 监听当前ViewModel中 showDialog和error的值
+ */
+ private void initObserve() {
+ if (viewModel == null) return;
+ viewModel.getShowDialog(this, bean -> {
+ if (bean.isShow()) {
+ showDialog(bean.getMsg());
+ } else {
+ dismissDialog();
+ }
+ });
+ viewModel.getError(this, obj -> showError(obj));
+ }
+
+ /**
+ * ViewModel层发生了错误
+ */
+ protected abstract void showError(Object obj);
+
+
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/base/BaseFragment.java b/basicLib/src/main/java/com/azhon/basic/base/BaseFragment.java
new file mode 100644
index 0000000..e20289a
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/base/BaseFragment.java
@@ -0,0 +1,105 @@
+package com.azhon.basic.base;
+
+import android.annotation.SuppressLint;
+import android.os.Looper;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import androidx.databinding.ViewDataBinding;
+import androidx.lifecycle.Observer;
+
+import com.azhon.basic.bean.DialogBean;
+import com.azhon.basic.lifecycle.BaseViewModel;
+
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.base
+ * 文件名: BaseFragment
+ * 创建时间: 2019-03-28 on 17:35
+ * 描述: TODO ViewModel、ViewDataBinding都需要的基类
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseFragment
+ extends BaseNoModelFragment {
+
+ protected VM viewModel;
+ private static Toast toast;
+
+ @Override
+ protected DB initDataBinding(LayoutInflater inflater, int layoutId, ViewGroup container) {
+ DB db = super.initDataBinding(inflater, layoutId, container);
+ /**
+ * 将这两个初始化函数插在{@link BaseFragment#initDataBinding}
+ */
+ viewModel = initViewModel();
+ initObserve();
+ return db;
+ }
+
+ /**
+ * 初始化ViewModel
+ */
+ protected abstract VM initViewModel();
+
+ /**
+ * 监听当前ViewModel中 showDialog和error的值
+ */
+ private void initObserve() {
+ if (viewModel == null) return;
+ viewModel.getShowDialog(this, new Observer() {
+ @Override
+ public void onChanged(DialogBean bean) {
+ if (bean.isShow()) {
+ showDialog(bean.getMsg());
+ } else {
+ dismissDialog();
+ }
+ }
+ });
+ viewModel.getError(this, new Observer() {
+ @Override
+ public void onChanged(Object obj) {
+ showError(obj);
+ }
+ });
+ }
+
+ /**
+ * ViewModel层发生了错误
+ */
+ protected abstract void showError(Object obj);
+
+
+// /**
+// * 显示提示 toast
+// *
+// * @param msg 提示信息
+// */
+// @SuppressLint("ShowToast")
+// public void showToast(String msg) {
+// try {
+// if (null == toast) {
+// toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
+// } else {
+// toast.setText(msg);
+// }
+// getActivity().runOnUiThread(new Runnable() {
+// @Override
+// public void run() {
+// toast.show();
+// }
+// });
+// } catch (Exception e) {
+// e.printStackTrace();
+// //解决在子线程中调用Toast的异常情况处理
+// Looper.prepare();
+// Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+// Looper.loop();
+// }
+// }
+
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/base/BaseLazyFragment.java b/basicLib/src/main/java/com/azhon/basic/base/BaseLazyFragment.java
new file mode 100644
index 0000000..4c2921f
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/base/BaseLazyFragment.java
@@ -0,0 +1,36 @@
+package com.azhon.basic.base;
+
+import androidx.databinding.ViewDataBinding;
+import androidx.lifecycle.LifecycleObserver;
+
+import com.azhon.basic.lifecycle.BaseViewModel;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.base
+ * 文件名: BaseLazyFragment
+ * 创建时间: 2019-03-28 on 18:01
+ * 描述: TODO 懒加载Fragment基类,适用于一个页面多个Tab页面
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseLazyFragment
+ extends BaseFragment implements LifecycleObserver {
+
+ public boolean visibleToUser;
+
+ @Override
+ public void onResume() {
+ super.onResume();
+ if (!visibleToUser) {
+ visibleToUser = true;
+ lazyLoad();
+ }
+ }
+
+ /**
+ * 懒加载,只有在Fragment第一次创建且第一次对用户可见
+ */
+ protected abstract void lazyLoad();
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelActivity.java b/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelActivity.java
new file mode 100644
index 0000000..cccee46
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelActivity.java
@@ -0,0 +1,328 @@
+package com.azhon.basic.base;
+
+import android.annotation.SuppressLint;
+import android.app.Activity;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.graphics.Color;
+import android.graphics.ColorMatrix;
+import android.graphics.ColorMatrixColorFilter;
+import android.graphics.Paint;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.View;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Toast;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.databinding.DataBindingUtil;
+import androidx.databinding.ViewDataBinding;
+
+import com.azhon.basic.utils.ActivityUtil;
+import com.azhon.basic.view.LoadingDialog;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.base
+ * 文件名: BaseNoModelActivity
+ * 创建时间: 2019-03-28 on 10:28
+ * 描述: TODO 不需要ViewModel的页面基类
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseNoModelActivity extends AppCompatActivity {
+
+ protected DB dataBinding;
+ protected Context context;
+ private LoadingDialog loadingDialog;
+ public Bundle bundle;
+ //获取TAG的activity名称
+ protected final String TAG = this.getClass().getSimpleName();
+ //是否显示标题栏
+ private boolean isShowTitle = false;
+ //是否显示状态栏
+ private boolean isShowStatusBar = true;
+ //是否允许旋转屏幕
+ private boolean isAllowScreenRoate = true;
+ //封装Toast对象
+ private static Toast toast;
+
+
+ @SuppressLint("SourceLockedOrientationActivity")
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Window window = getWindow();
+ View decor = window.getDecorView();
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+ | View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.setStatusBarColor(Color.TRANSPARENT);
+ if(this.getApplicationContext().getResources().getConfiguration().uiMode == 0x21){
+ decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }else {
+ decor.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+// Paint paint = new Paint();
+// ColorMatrix matrix = new ColorMatrix();
+// matrix.setSaturation(0);
+// paint.setColorFilter(new ColorMatrixColorFilter(matrix));
+// getWindow().getDecorView().setLayerType(View.LAYER_TYPE_HARDWARE, paint);
+
+ super.onCreate(savedInstanceState);
+ context = this;
+ this.bundle=savedInstanceState;
+
+ ActivityUtil.getInstance().addActivity(this);
+ int layoutId = initLayout();
+ setContentView(layoutId);
+// setStatusBarColor(this,Color.TRANSPARENT);
+ dataBinding = initDataBinding(layoutId);
+ initView();
+ initData();
+ }
+
+
+ /**
+ * 修改状态栏颜色,支持4.4以上版本
+ * @param activity
+ * @param colorId
+ */
+ public static void setStatusBarColor(Activity activity, int colorId) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Window window = activity.getWindow();
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.setStatusBarColor(colorId);
+ window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+
+ /**
+ * 修改状态栏颜色,支持4.4以上版本
+ * @param activity
+ * @param colorId
+ */
+ public static void setStatusBarColorW(Activity activity, int colorId,boolean statusBarTextIsWhiteColor) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ Window window = activity.getWindow();
+ window.clearFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS);
+ window.setStatusBarColor(activity.getResources().getColor(colorId));
+ if(statusBarTextIsWhiteColor) {
+ window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);
+ }else{
+ window.getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ }
+ }
+ }
+
+ public void hideActionBar(){
+ //this.getSupportActionBar().hide();
+
+ }
+ /**
+ * 初始化要加载的布局资源ID
+ * 此函数优先执行于onCreate()可以做window操作
+ */
+ protected abstract int initLayout();
+
+
+ /**
+ * 初始化DataBinding
+ */
+ protected DB initDataBinding(@LayoutRes int layoutId) {
+ return DataBindingUtil.setContentView(this, layoutId);
+ }
+
+ /**
+ * 初始化视图
+ */
+ protected abstract void initView();
+
+ /**
+ * 初始化数据
+ */
+ protected abstract void initData();
+
+ /**
+ * 显示用户等待框
+ *
+ * @param msg 提示信息
+ */
+ protected void showDialog(String msg) {
+ if (loadingDialog != null && loadingDialog.isShowing()) {
+ loadingDialog.setLoadingMsg(msg);
+ } else {
+ loadingDialog = new LoadingDialog(context);
+ loadingDialog.setLoadingMsg(msg);
+ loadingDialog.show();
+ }
+ }
+
+
+ /**
+ * 隐藏等待框
+ */
+ protected void dismissDialog() {
+ if (loadingDialog != null && loadingDialog.isShowing()) {
+ loadingDialog.dismiss();
+ loadingDialog = null;
+ }
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (dataBinding != null) {
+ dataBinding.unbind();
+ }
+ ActivityUtil.getInstance().removeActivity(this);
+ }
+
+
+ /**
+ * 设置是否显示标题栏
+ *
+ * @param showTitle true or false
+ */
+ public void setShowTitle(boolean showTitle) {
+ isShowTitle = showTitle;
+ }
+
+ /**
+ * 设置是否显示状态栏
+ *
+ * @param showStatusBar true or false
+ */
+ public void setShowStatusBar(boolean showStatusBar) {
+ isShowStatusBar = showStatusBar;
+ }
+
+ /**
+ * 是否允许屏幕旋转
+ *
+ * @param allowScreenRoate true or false
+ */
+ public void setAllowScreenRoate(boolean allowScreenRoate) {
+ isAllowScreenRoate = allowScreenRoate;
+ }
+
+ /**
+ * 保证同一按钮在1秒内只会响应一次点击事件
+ */
+ public abstract class OnSingleClickListener implements View.OnClickListener {
+ //两次点击按钮之间的间隔,目前为1000ms
+ private static final int MIN_CLICK_DELAY_TIME = 1000;
+ private long lastClickTime;
+
+ public abstract void onSingleClick(View view);
+
+ @Override
+ public void onClick(View view) {
+ long curClickTime = System.currentTimeMillis();
+ if ((curClickTime - lastClickTime) >= MIN_CLICK_DELAY_TIME) {
+ lastClickTime = curClickTime;
+ onSingleClick(view);
+ }
+ }
+ }
+
+ /**
+ * 同一按钮在短时间内可重复响应点击事件
+ */
+ public abstract class OnMultiClickListener implements View.OnClickListener {
+ public abstract void onMultiClick(View view);
+
+ @Override
+ public void onClick(View v) {
+ onMultiClick(v);
+ }
+ }
+
+// /**
+// * 显示提示 toast
+// *
+// * @param msg 提示信息
+// */
+// @SuppressLint("ShowToast")
+// public void showToast(String msg) {
+// try {
+// if (null == toast) {
+// toast = Toast.makeText(context, msg, Toast.LENGTH_SHORT);
+// } else {
+// toast.setText(msg);
+// }
+// runOnUiThread(new Runnable() {
+// @Override
+// public void run() {
+// toast.show();
+// }
+// });
+// } catch (Exception e) {
+// e.printStackTrace();
+// //解决在子线程中调用Toast的异常情况处理
+// Looper.prepare();
+// Toast.makeText(context, msg, Toast.LENGTH_SHORT).show();
+// Looper.loop();
+// }
+// }
+
+
+ /**
+ * 隐藏软键盘
+ */
+ public void hideSoftInput() {
+ InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (getCurrentFocus() != null && null != imm) {
+ imm.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), 0);
+ }
+ }
+
+ /**
+ * 显示软键盘
+ */
+ public void showSoftInput() {
+ InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE);
+ if (getCurrentFocus() != null && null != imm) {
+ imm.showSoftInputFromInputMethod(getCurrentFocus().getWindowToken(), 0);
+ }
+ }
+
+
+ protected void showToast(String message){
+// try {
+// SnackbarUtils.Short(dataBinding.getRoot(),message).show();
+//
+// }catch (Exception e){e.printStackTrace();}
+ Toast.makeText(this,message,Toast.LENGTH_SHORT).show();
+
+ }
+
+ protected void showToast(String message,int color){
+ try {
+// SnackbarUtils.Short(dataBinding.getRoot(),message,color).show();
+ Toast.makeText(this,message,Toast.LENGTH_SHORT).show();
+
+
+ }catch (Exception e){e.printStackTrace();}
+
+ }
+
+ protected void showToast(View view,String message){
+ try {
+// SnackbarUtils.Short(view,message).show();
+ Toast.makeText(this,message,Toast.LENGTH_SHORT).show();
+
+ }catch (Exception e){e.printStackTrace();}
+
+
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelFragment.java b/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelFragment.java
new file mode 100644
index 0000000..5835586
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/base/BaseNoModelFragment.java
@@ -0,0 +1,180 @@
+package com.azhon.basic.base;
+
+import android.content.Context;
+import android.os.Bundle;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.Toast;
+
+import androidx.annotation.LayoutRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.databinding.DataBindingUtil;
+import androidx.databinding.ViewDataBinding;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+
+import com.azhon.basic.view.LoadingDialog;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.base
+ * 文件名: BaseNoModelFragment
+ * 创建时间: 2019-03-28 on 17:06
+ * 描述: TODO 不需要ViewModel的页面基类
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseNoModelFragment extends Fragment {
+
+ protected DB dataBinding;
+ protected Context context;
+ protected FragmentActivity activity;
+ private LoadingDialog loadingDialog;
+
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ this.context = context;
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
+ dataBinding = initDataBinding(inflater, setLayoutResourceID(), container);
+ return dataBinding.getRoot();
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ activity = getActivity();
+ setUpView();
+ setUpData();
+ }
+
+ /**
+ * 初始化要加载的布局资源ID
+ */
+ protected abstract int setLayoutResourceID();
+
+
+ /**
+ * 初始化DataBinding
+ */
+ protected DB initDataBinding(LayoutInflater inflater, @LayoutRes int layoutId, ViewGroup container) {
+ return DataBindingUtil.inflate(inflater, layoutId, container, false);
+ }
+
+ /**
+ * 初始化视图
+ */
+ protected abstract void setUpView();
+
+ /**
+ * 初始化数据
+ */
+ protected abstract void setUpData();
+
+ /**
+ * 显示用户等待框
+ *
+ * @param msg 提示信息
+ */
+ protected void showDialog(String msg) {
+ if (loadingDialog != null && loadingDialog.isShowing()) {
+ loadingDialog.setLoadingMsg(msg);
+ } else {
+ loadingDialog = new LoadingDialog(context);
+ loadingDialog.setLoadingMsg(msg);
+ loadingDialog.show();
+ }
+ }
+ protected void showToast(String message){
+ try {
+// SnackbarUtils.Short(dataBinding.getRoot(),message).show();
+ Toast.makeText(dataBinding.getRoot().getContext(),message,Toast.LENGTH_SHORT).show();
+
+
+ }catch (Exception e){e.printStackTrace();}
+
+ }
+
+ protected void showToast(View view,String message){
+ try {
+// SnackbarUtils.Short(view,message).show();
+ Toast.makeText(dataBinding.getRoot().getContext(),message,Toast.LENGTH_SHORT).show();
+
+ }catch (Exception e){e.printStackTrace();}
+
+
+ }
+
+ protected void showToast(View view,String message,int color){
+ try {
+// SnackbarUtils.Short(view,message,color).show();
+ Toast.makeText(dataBinding.getRoot().getContext(),message,Toast.LENGTH_SHORT).show();
+
+ }catch (Exception e){e.printStackTrace();}
+
+
+ }
+
+ protected void showToast(String message,int color){
+ try {
+// SnackbarUtils.Short(dataBinding.getRoot(),message,color).show();
+ Toast.makeText(dataBinding.getRoot().getContext(),message,Toast.LENGTH_SHORT).show();
+
+
+ }catch (Exception e){e.printStackTrace();}
+
+ }
+
+ /**
+ * 隐藏等待框
+ */
+ protected void dismissDialog() {
+ if (loadingDialog != null && loadingDialog.isShowing()) {
+ loadingDialog.dismiss();
+ loadingDialog = null;
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (dataBinding != null) {
+ dataBinding.unbind();
+ }
+ }
+
+
+ public View getContentView() {
+ return dataBinding.getRoot();
+ }
+
+ public Context getMContext() {
+ return context;
+ }
+
+
+
+ /**
+ * 隐藏软键盘
+ */
+ public void hideSoftInput() {
+ View v = getActivity().getCurrentFocus();
+ if (v != null && v.getWindowToken() != null) {
+ InputMethodManager manager = (InputMethodManager) getContext()
+ .getSystemService(Context.INPUT_METHOD_SERVICE);
+ boolean isOpen = manager.isActive();
+ if (isOpen) {
+ manager.hideSoftInputFromWindow(v.getWindowToken(), InputMethodManager.HIDE_NOT_ALWAYS);
+ }
+ }
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/bean/DialogBean.java b/basicLib/src/main/java/com/azhon/basic/bean/DialogBean.java
new file mode 100644
index 0000000..51da6f6
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/bean/DialogBean.java
@@ -0,0 +1,33 @@
+package com.azhon.basic.bean;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.bean
+ * 文件名: DialogBean
+ * 创建时间: 2019-03-27 on 20:54
+ * 描述: TODO 封装的对话框实体类
+ *
+ * @author xuhuixiang
+ */
+
+public final class DialogBean {
+
+ private boolean isShow;
+ private String msg;
+
+ public boolean isShow() {
+ return isShow;
+ }
+
+ public void setShow(boolean show) {
+ isShow = show;
+ }
+
+ public String getMsg() {
+ return msg;
+ }
+
+ public void setMsg(String msg) {
+ this.msg = msg;
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/lifecycle/BaseViewModel.java b/basicLib/src/main/java/com/azhon/basic/lifecycle/BaseViewModel.java
new file mode 100644
index 0000000..8ba0b65
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/lifecycle/BaseViewModel.java
@@ -0,0 +1,69 @@
+package com.azhon.basic.lifecycle;
+
+import androidx.lifecycle.LifecycleOwner;
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.Observer;
+import androidx.lifecycle.ViewModel;
+
+import com.azhon.basic.bean.DialogBean;
+
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.lifecycle
+ * 文件名: BaseViewModel
+ * 创建时间: 2019-03-27 on 10:44
+ * 描述: TODO ViewModel基类,管理rxJava发出的请求,ViewModel销毁同时也取消请求
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseViewModel extends ViewModel {
+
+ /**
+ * 管理RxJava请求
+ */
+ private CompositeDisposable compositeDisposable;
+ /**
+ * 用来通知 Activity/Fragment 是否显示等待Dialog
+ */
+ protected DialogLiveData showDialog = new DialogLiveData<>();
+ /**
+ * 当ViewModel层出现错误需要通知到Activity/Fragment
+ */
+ protected MutableLiveData error = new MutableLiveData<>();
+
+ /**
+ * 添加 rxJava 发出的请求
+ */
+ protected void addDisposable(Disposable disposable) {
+ if (compositeDisposable == null || compositeDisposable.isDisposed()) {
+ compositeDisposable = new CompositeDisposable();
+ }
+ compositeDisposable.add(disposable);
+ }
+
+ public void getShowDialog(LifecycleOwner owner, Observer observer) {
+ showDialog.observe(owner, observer);
+ }
+
+ public void getError(LifecycleOwner owner, Observer observer) {
+ error.observe(owner, observer);
+ }
+
+ /**
+ * ViewModel销毁同时也取消请求
+ */
+ @Override
+ protected void onCleared() {
+ super.onCleared();
+ if (compositeDisposable != null) {
+ compositeDisposable.dispose();
+ compositeDisposable = null;
+ }
+ showDialog = null;
+ error = null;
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/lifecycle/DialogLiveData.java b/basicLib/src/main/java/com/azhon/basic/lifecycle/DialogLiveData.java
new file mode 100644
index 0000000..c75b3ba
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/lifecycle/DialogLiveData.java
@@ -0,0 +1,32 @@
+package com.azhon.basic.lifecycle;
+
+import androidx.lifecycle.MutableLiveData;
+
+import com.azhon.basic.bean.DialogBean;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.lifecycle
+ * 文件名: DialogLiveData
+ * 创建时间: 2019-03-27 on 20:57
+ * 描述: TODO
+ *
+ * @author xuhuixiang
+ */
+
+public final class DialogLiveData extends MutableLiveData {
+
+ private DialogBean bean = new DialogBean();
+
+ public void setValue(boolean isShow) {
+ bean.setShow(isShow);
+ bean.setMsg("");
+ setValue((T) bean);
+ }
+
+ public void setValue(boolean isShow, String msg) {
+ bean.setShow(isShow);
+ bean.setMsg(msg);
+ setValue((T) bean);
+ }
+}
\ No newline at end of file
diff --git a/basicLib/src/main/java/com/azhon/basic/retrofit/BaseApi.java b/basicLib/src/main/java/com/azhon/basic/retrofit/BaseApi.java
new file mode 100644
index 0000000..4e573e3
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/retrofit/BaseApi.java
@@ -0,0 +1,46 @@
+package com.azhon.basic.retrofit;
+
+import okhttp3.OkHttpClient;
+import retrofit2.Retrofit;
+import retrofit2.adapter.rxjava2.RxJava2CallAdapterFactory;
+import retrofit2.converter.gson.GsonConverterFactory;
+import retrofit2.converter.scalars.ScalarsConverterFactory;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.retrofit
+ * 文件名: BaseApi
+ * 创建时间: 2019-03-27 on 14:52
+ * 描述: TODO 封装基础的Retrofit
+ *
+ * @author xuhuixiang
+ */
+
+public abstract class BaseApi {
+
+ /**
+ * 初始化Retrofit
+ */
+ public Retrofit initRetrofit(String baseUrl) {
+ Retrofit.Builder builder = new Retrofit.Builder();
+ //支持返回Call
+ builder.addConverterFactory(ScalarsConverterFactory.create());
+ //支持直接格式化json返回Bean对象
+ builder.addConverterFactory(GsonConverterFactory.create());
+ //支持RxJava
+ builder.addCallAdapterFactory(RxJava2CallAdapterFactory.create());
+ builder.baseUrl(baseUrl);
+ OkHttpClient client = setClient();
+ if (client != null) {
+ builder.client(client);
+ }
+ return builder.build();
+ }
+
+ /**
+ * 设置OkHttpClient,添加拦截器等
+ *
+ * @return 可以返回为null
+ */
+ protected abstract OkHttpClient setClient();
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/ActivityUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/ActivityUtil.java
new file mode 100644
index 0000000..42ec718
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/ActivityUtil.java
@@ -0,0 +1,92 @@
+package com.azhon.basic.utils;
+
+import android.app.Activity;
+
+import java.util.Stack;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.utils
+ * 文件名: ActivityUtil
+ * 创建时间: 2019-03-29 on 17:13
+ * 描述: TODO 管理所有Activity的实例
+ *
+ * @author xuhuixiang
+ */
+
+public class ActivityUtil {
+
+ private static Stack stack;
+ private static ActivityUtil manager;
+
+ /**
+ * 获取实例
+ */
+ public static synchronized ActivityUtil getInstance() {
+ if (manager == null) {
+ manager = new ActivityUtil();
+ stack = new Stack<>();
+ }
+ return manager;
+ }
+
+ /**
+ * 添加Activity
+ */
+ public synchronized void addActivity(Activity activity) {
+ stack.add(activity);
+ }
+
+ /**
+ * 移除Activity
+ */
+ public synchronized void removeActivity(Activity activity) {
+ stack.remove(activity);
+ }
+
+ /**
+ * 结束指定类名的Activity
+ */
+ public void finishActivity(Class> cls) {
+ for (Activity activity : stack) {
+ if (activity.getClass().equals(cls)) {
+ finishActivity(activity);
+ return;
+ }
+ }
+ }
+
+ /**
+ * 结束指定的Activity
+ */
+ public void finishActivity(Activity activity) {
+ if (activity != null) {
+ activity.finish();
+ stack.remove(activity);
+ }
+ }
+
+ /**
+ * 是否存在Activity
+ */
+ public boolean containsActivity(Class> cls) {
+ for (Activity activity : stack) {
+ if (activity.getClass().equals(cls)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 结束所有Activity
+ */
+ public void finishAllActivity() {
+ for (Activity activity : stack) {
+ if (activity != null) {
+ activity.finish();
+ }
+ }
+ stack.clear();
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/AnimUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/AnimUtil.java
new file mode 100644
index 0000000..ef1b25d
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/AnimUtil.java
@@ -0,0 +1,26 @@
+package com.azhon.basic.utils;
+
+import android.view.View;
+
+import com.github.hariprasanths.bounceview.BounceView;
+
+/**
+ * 控件的点击缩放事件
+ */
+public class AnimUtil {
+ /**
+ * 控件设置动画事件
+ * @param view
+ */
+ public static void setAnimView(View view){
+ BounceView.addAnimTo(view).setScaleForPushInAnim(1.02f,1.02f).setScaleForPopOutAnim(1.0f,1.0f);
+ }
+
+ /**
+ * 控件设置动画事件
+ * @param view
+ */
+ public static void setAnimView(View view,float inAnim){
+ BounceView.addAnimTo(view).setScaleForPushInAnim(inAnim,inAnim).setScaleForPopOutAnim(1.0f,1.0f);
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/ApkUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/ApkUtil.java
new file mode 100644
index 0000000..d5a9e86
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/ApkUtil.java
@@ -0,0 +1,76 @@
+package com.azhon.basic.utils;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.net.Uri;
+import android.os.Build;
+
+import androidx.core.content.FileProvider;
+
+import java.io.File;
+
+/**
+ * 文件名: ApkUtil
+ * 创建时间: 2018/5/4 on 15:49
+ * 描述: TODO
+ *
+ * @author xuhuixiang
+ */
+
+public class ApkUtil {
+ /**
+ * 安装一个apk
+ *
+ * @param context 上下文
+ * @param apk 安装包文件
+ */
+ public static void installApk(Context context, File apk) {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setAction(Intent.ACTION_VIEW);
+ intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ intent.addCategory(Intent.CATEGORY_DEFAULT);
+ Uri uri;
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ uri = FileProvider.getUriForFile(context, context.getPackageName(), apk);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ } else {
+ uri = Uri.fromFile(apk);
+ }
+ intent.setDataAndType(uri, "application/vnd.android.package-archive");
+ context.startActivity(intent);
+ }
+
+ /**
+ * 获取当前app的升级版本号
+ *
+ * @param context 上下文
+ */
+ public static long getVersionCode(Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ return packageInfo.getLongVersionCode();
+ } catch (Exception e) {
+ e.printStackTrace();
+ return 1;
+ }
+ }
+
+ /**
+ * 获取当前app的版本号
+ *
+ * @param context 上下文
+ */
+ public static String getVersionName(Context context) {
+ try {
+ PackageManager packageManager = context.getPackageManager();
+ PackageInfo packageInfo = packageManager.getPackageInfo(context.getPackageName(), 0);
+ return packageInfo.versionName;
+ } catch (Exception e) {
+ e.printStackTrace();
+ return "1.0.0";
+ }
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/DensityUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/DensityUtil.java
new file mode 100644
index 0000000..e6aa2ec
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/DensityUtil.java
@@ -0,0 +1,83 @@
+package com.azhon.basic.utils;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.DisplayMetrics;
+
+/**
+ * 文件名: DensityUtil
+ * 创建时间: 2018/3/18 on 13:32
+ * 描述: TODO
+ *
+ * @author xuhuixiang
+ */
+
+
+public class DensityUtil {
+
+ /**
+ * 获取屏幕宽度(像素)
+ *
+ * @param context 上下文
+ * @return px
+ */
+ public static int getWith(Context context) {
+ DisplayMetrics dm = context.getResources().getDisplayMetrics();
+ return dm.widthPixels;
+ }
+
+ /**
+ * 获取屏幕高度(像素)
+ *
+ * @param context 上下文
+ * @return px
+ */
+ public static int getHeight(Context context) {
+ DisplayMetrics dm = context.getResources().getDisplayMetrics();
+ return dm.heightPixels;
+ }
+
+ /**
+ * 获取状态栏的高度
+ *
+ * @param context 上下文
+ * @return px
+ */
+ public static int getStatusBarHeight(Context context) {
+ int statusBarHeight = 0;
+ int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android");
+ if (resourceId > 0) {
+ statusBarHeight = context.getResources().getDimensionPixelSize(resourceId);
+ }
+ return statusBarHeight;
+ }
+
+ /**
+ * 获取标题栏(ActionBar)的高度
+ *
+ * @param context 上下文
+ * @return px
+ */
+ public static int getActionBarHeight(Context context) {
+ TypedArray values = context.getTheme().obtainStyledAttributes(new int[]{android.R.attr.actionBarSize});
+ int actionBarHeight = values.getDimensionPixelSize(0, 0);
+ values.recycle();
+ return actionBarHeight;
+ }
+
+ /**
+ * 根据手机的分辨率从 dp 的单位 转成为 px(像素)
+ */
+ public static int dip2px(Context context, float dpValue) {
+ final float scale = context.getResources().getDisplayMetrics().density;
+ return (int) (dpValue * scale + 0.5f);
+ }
+
+ /**
+ * 根据手机的分辨率从 px(像素) 的单位 转成为 dp
+ */
+ public static int px2dip(Context context, float pxValue) {
+ final float scale = context.getResources().getDisplayMetrics().density;
+ return (int) (pxValue / scale + 0.5f);
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/SharePreUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/SharePreUtil.java
new file mode 100644
index 0000000..f2f71f5
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/SharePreUtil.java
@@ -0,0 +1,144 @@
+package com.azhon.basic.utils;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStreamReader;
+
+/**
+ * 文件名: SharePreUtil
+ * 创建者: xuhuixiang
+ * 创建时间: 2016/12/12 on 11:58
+ * 描述: SharedPreferences存取工具类
+ */
+
+public class SharePreUtil {
+ /**
+ * 配置文件,文件名
+ */
+ private static final String SHARE_NAME = "config";
+
+ /**
+ * 存字符串
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 值
+ */
+ public static void putString(Context context, String key, String values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ sp.edit().putString(key, values).apply();
+ }
+
+
+ /**
+ * 取字符串
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 默认值
+ * @return 取出的值
+ */
+ public static String getString(Context context, String key, String values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ return sp.getString(key, values);
+ }
+
+
+ /**
+ * 存布尔值
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 值
+ */
+ public static void putBoolean(Context context, String key, boolean values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ sp.edit().putBoolean(key, values).apply();
+ }
+
+ /**
+ * 取布尔值
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 默认值
+ * @return true/false
+ */
+ public static boolean getBoolean(Context context, String key, boolean values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ return sp.getBoolean(key, values);
+ }
+
+ /**
+ * 存int值
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 值
+ */
+ public static void putInt(Context context, String key, int values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ sp.edit().putInt(key, values).apply();
+ }
+
+ /**
+ * 取int值
+ *
+ * @param context 上下文
+ * @param key 键
+ * @param values 默认值
+ * @return
+ */
+ public static int getInt(Context context, String key, int values) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ return sp.getInt(key, values);
+ }
+
+ /**
+ * 删除一条字段
+ *
+ * @param context 上下文
+ * @param key 键
+ */
+ public static void deleShare(Context context, String key) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ //单个清理
+ sp.edit().remove(key).apply();
+ }
+
+ /**
+ * 删除全部数据
+ *
+ * @param context 上下文
+ */
+ public static void deleShareAll(Context context) {
+ SharedPreferences sp = context.getSharedPreferences(SHARE_NAME, Context.MODE_PRIVATE);
+ //全部清理
+ sp.edit().clear().apply();
+ }
+
+ /**
+ * 查看SharedPreferences的内容
+ */
+ public static String lookSharePre(Context context) {
+ try {
+ FileInputStream stream = new FileInputStream(new File("/data/data/" +
+ context.getPackageName() + "/shared_prefs", SHARE_NAME + ".xml"));
+ BufferedReader bff = new BufferedReader(new InputStreamReader(stream));
+ String line;
+ StringBuilder sb = new StringBuilder();
+ while ((line = bff.readLine()) != null) {
+ sb.append(line);
+ sb.append("\n");
+ }
+ return sb.toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "未找到当前配置文件!";
+ }
+}
\ No newline at end of file
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/SnackbarUtils.java b/basicLib/src/main/java/com/azhon/basic/utils/SnackbarUtils.java
new file mode 100644
index 0000000..0c2f831
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/SnackbarUtils.java
@@ -0,0 +1,475 @@
+package com.azhon.basic.utils;
+
+import android.annotation.TargetApi;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.GradientDrawable;
+import android.os.Build;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Button;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.Space;
+import android.widget.TextView;
+
+import androidx.annotation.ColorInt;
+import androidx.annotation.DrawableRes;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.StringRes;
+import androidx.coordinatorlayout.widget.CoordinatorLayout;
+
+import com.azhon.basic.R;
+import com.google.android.material.snackbar.Snackbar;
+
+public class SnackbarUtils {
+ //设置Snackbar背景颜色
+ private static final int color_info = 0XFF2094F3;
+ private static final int color_confirm = 0XFF4CB04E;
+ private static final int color_warning = 0XFFFEC005;
+ private static final int color_danger = 0XFFF44336;
+ //工具类当前持有的Snackbar实例
+ private static Snackbar mSnackbar = null;
+
+
+ private SnackbarUtils(){
+ throw new RuntimeException("禁止无参创建实例");
+ }
+
+ public SnackbarUtils(@NonNull Snackbar snackbar){
+ this.mSnackbar = snackbar;
+ }
+
+ /**
+ * 获取 mSnackbar
+ * @return
+ */
+ public Snackbar getSnackbar() {
+ return mSnackbar;
+ }
+
+ /**
+ * 初始化Snackbar实例
+ * 展示时间:Snackbar.LENGTH_SHORT
+ * @param view
+ * @param message
+ * @return
+ */
+ public static SnackbarUtils Short(View view, String message){
+ mSnackbar = Snackbar.make(view,message,Snackbar.LENGTH_SHORT);
+ return new SnackbarUtils(mSnackbar).backColor(0XCC1771E3);
+ }
+
+ /**
+ * 初始化Snackbar实例
+ * 展示时间:Snackbar.LENGTH_SHORT
+ * @param view
+ * @param message
+ * @return
+ */
+ public static SnackbarUtils Short(View view, String message,int Color){
+ mSnackbar = Snackbar.make(view,message,Snackbar.LENGTH_SHORT);
+ return new SnackbarUtils(mSnackbar).backColor(Color);
+ }
+
+
+ /**
+ * 初始化Snackbar实例
+ * 展示时间:Snackbar.LENGTH_LONG
+ * @param view
+ * @param message
+ * @return
+ */
+ public static SnackbarUtils Long(View view, String message){
+ mSnackbar = Snackbar.make(view,message,Snackbar.LENGTH_LONG);
+ return new SnackbarUtils(mSnackbar).backColor(0XCC1771E3);
+ }
+ /**
+ * 初始化Snackbar实例
+ * 展示时间:Snackbar.LENGTH_INDEFINITE
+ * @param view
+ * @param message
+ * @return
+ */
+ public static SnackbarUtils Indefinite(View view, String message){
+ mSnackbar = Snackbar.make(view,message,Snackbar.LENGTH_INDEFINITE);
+ return new SnackbarUtils(mSnackbar).backColor(0XCC1771E3);
+ }
+ /**
+ * 初始化Snackbar实例
+ * 展示时间:duration 毫秒
+ * @param view
+ * @param message
+ * @param duration 展示时长(毫秒)
+ * @return
+ */
+ public static SnackbarUtils Custom(View view, String message, int duration){
+ mSnackbar = Snackbar.make(view,message,Snackbar.LENGTH_SHORT);
+ mSnackbar.setDuration(duration);
+ return new SnackbarUtils(mSnackbar).backColor(0XCC1771E3);
+ }
+
+ /**
+ * 设置mSnackbar背景色为 color_info
+ */
+ public SnackbarUtils info(){
+ mSnackbar.getView().setBackgroundColor(color_info);
+ return new SnackbarUtils(mSnackbar);
+ }
+ /**
+ * 设置mSnackbar背景色为 color_confirm
+ */
+ public SnackbarUtils confirm(){
+ mSnackbar.getView().setBackgroundColor(color_confirm);
+ return new SnackbarUtils(mSnackbar);
+ }
+ /**
+ * 设置Snackbar背景色为 color_warning
+ */
+ public SnackbarUtils warning(){
+ mSnackbar.getView().setBackgroundColor(color_warning);
+ return new SnackbarUtils(mSnackbar);
+ }
+ /**
+ * 设置Snackbar背景色为 color_warning
+ */
+ public SnackbarUtils danger(){
+ mSnackbar.getView().setBackgroundColor(color_danger);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar背景色
+ * @param backgroundColor
+ */
+ public SnackbarUtils backColor(@ColorInt int backgroundColor){
+ mSnackbar.getView().setBackgroundColor(backgroundColor);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置TextView(@+id/snackbar_text)的文字颜色
+ * @param messageColor
+ */
+ public SnackbarUtils messageColor(@ColorInt int messageColor){
+ ((TextView)mSnackbar.getView().findViewById(R.id.snackbar_text)).setTextColor(messageColor);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Button(@+id/snackbar_action)的文字颜色
+ * @param actionTextColor
+ */
+ public SnackbarUtils actionColor(@ColorInt int actionTextColor){
+ ((Button)mSnackbar.getView().findViewById(R.id.snackbar_action)).setTextColor(actionTextColor);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置 Snackbar背景色 + TextView(@+id/snackbar_text)的文字颜色 + Button(@+id/snackbar_action)的文字颜色
+ * @param backgroundColor
+ * @param messageColor
+ * @param actionTextColor
+ */
+ public SnackbarUtils colors(@ColorInt int backgroundColor, @ColorInt int messageColor, @ColorInt int actionTextColor){
+ mSnackbar.getView().setBackgroundColor(backgroundColor);
+ ((TextView)mSnackbar.getView().findViewById(R.id.snackbar_text)).setTextColor(messageColor);
+ ((Button)mSnackbar.getView().findViewById(R.id.snackbar_action)).setTextColor(actionTextColor);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar 背景透明度
+ * @param alpha
+ * @return
+ */
+ public SnackbarUtils alpha(float alpha){
+ alpha = alpha>=1.0f?1.0f:(alpha<=0.0f?0.0f:alpha);
+ mSnackbar.getView().setAlpha(alpha);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar显示的位置
+ * @param gravity
+ */
+ public SnackbarUtils gravityFrameLayout(int gravity){
+ FrameLayout.LayoutParams params = new FrameLayout.LayoutParams(mSnackbar.getView().getLayoutParams().width,mSnackbar.getView().getLayoutParams().height);
+ params.gravity = gravity;
+ mSnackbar.getView().setLayoutParams(params);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar显示的位置,当Snackbar和CoordinatorLayout组合使用的时候
+ * @param gravity
+ */
+ public SnackbarUtils gravityCoordinatorLayout(int gravity){
+ CoordinatorLayout.LayoutParams params = new CoordinatorLayout.LayoutParams(mSnackbar.getView().getLayoutParams().width,mSnackbar.getView().getLayoutParams().height);
+ params.gravity = gravity;
+ mSnackbar.getView().setLayoutParams(params);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置按钮文字内容 及 点击监听
+ * {@link Snackbar#setAction(CharSequence, View.OnClickListener)}
+ * @param resId
+ * @param listener
+ * @return
+ */
+ public SnackbarUtils setAction(@StringRes int resId, View.OnClickListener listener){
+ return setAction(getSnackbar().getView().getResources().getText(resId), listener);
+ }
+
+ /**
+ * 设置按钮文字内容 及 点击监听
+ * {@link Snackbar#setAction(CharSequence, View.OnClickListener)}
+ * @param text
+ * @param listener
+ * @return
+ */
+ public SnackbarUtils setAction(CharSequence text, View.OnClickListener listener){
+ mSnackbar.setAction(text,listener);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置 mSnackbar 展示完成 及 隐藏完成 的监听
+ * @param setCallback
+ * @return
+ */
+ public SnackbarUtils setCallback(Snackbar.Callback setCallback){
+ mSnackbar.setCallback(setCallback);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置TextView(@+id/snackbar_text)左右两侧的图片
+ * @param leftDrawable
+ * @param rightDrawable
+ * @return
+ */
+ public SnackbarUtils leftAndRightDrawable(@Nullable @DrawableRes Integer leftDrawable, @Nullable @DrawableRes Integer rightDrawable){
+ Drawable drawableLeft = null;
+ Drawable drawableRight = null;
+ if(leftDrawable!=null){
+ try {
+ drawableLeft = getSnackbar().getView().getResources().getDrawable(leftDrawable.intValue());
+ }catch (Exception e){
+ Log.e("Jet","getSnackbar().getView().getResources().getDrawable(leftDrawable.intValue())");
+ }
+ }
+ if(rightDrawable!=null){
+ try {
+ drawableRight = getSnackbar().getView().getResources().getDrawable(rightDrawable.intValue());
+ }catch (Exception e){
+ Log.e("Jet","getSnackbar().getView().getResources().getDrawable(rightDrawable.intValue())");
+ }
+ }
+ return leftAndRightDrawable(drawableLeft,drawableRight);
+ }
+
+ /**
+ * 设置TextView(@+id/snackbar_text)左右两侧的图片
+ * @param leftDrawable
+ * @param rightDrawable
+ * @return
+ */
+ public SnackbarUtils leftAndRightDrawable(@Nullable Drawable leftDrawable, @Nullable Drawable rightDrawable){
+ TextView message = (TextView) mSnackbar.getView().findViewById(R.id.snackbar_text);
+ LinearLayout.LayoutParams paramsMessage = (LinearLayout.LayoutParams) message.getLayoutParams();
+ paramsMessage = new LinearLayout.LayoutParams(paramsMessage.width, paramsMessage.height,0.0f);
+ message.setLayoutParams(paramsMessage);
+ message.setCompoundDrawablePadding(message.getPaddingLeft());
+ int textSize = (int) message.getTextSize();
+ Log.e("Jet","textSize:"+textSize);
+ if(leftDrawable!=null){
+ leftDrawable.setBounds(0,0,textSize,textSize);
+ }
+ if(rightDrawable!=null){
+ rightDrawable.setBounds(0,0,textSize,textSize);
+ }
+ message.setCompoundDrawables(leftDrawable,null,rightDrawable,null);
+ LinearLayout.LayoutParams paramsSpace = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT,1.0f);
+ ((Snackbar.SnackbarLayout)mSnackbar.getView()).addView(new Space(mSnackbar.getView().getContext()),1,paramsSpace);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置TextView(@+id/snackbar_text)中文字的对齐方式 居中
+ * @return
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public SnackbarUtils messageCenter(){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
+ TextView message = (TextView) mSnackbar.getView().findViewById(R.id.snackbar_text);
+ //View.setTextAlignment需要SDK>=17
+ message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
+ message.setGravity(Gravity.CENTER);
+ }
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置TextView(@+id/snackbar_text)中文字的对齐方式 居右
+ * @return
+ */
+ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
+ public SnackbarUtils messageRight(){
+ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
+ TextView message = (TextView) mSnackbar.getView().findViewById(R.id.snackbar_text);
+ //View.setTextAlignment需要SDK>=17
+ message.setTextAlignment(View.TEXT_ALIGNMENT_GRAVITY);
+ message.setGravity(Gravity.CENTER_VERTICAL|Gravity.RIGHT);
+ }
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 向Snackbar布局中添加View(Google不建议,复杂的布局应该使用DialogFragment进行展示)
+ * @param layoutId 要添加的View的布局文件ID
+ * @param index
+ * @return
+ */
+ public SnackbarUtils addView(int layoutId, int index) {
+ //加载布局文件新建View
+ View addView = LayoutInflater.from(mSnackbar.getView().getContext()).inflate(layoutId,null);
+ return addView(addView,index);
+ }
+
+ /**
+ * 向Snackbar布局中添加View(Google不建议,复杂的布局应该使用DialogFragment进行展示)
+ * @param addView
+ * @param index
+ * @return
+ */
+ public SnackbarUtils addView(View addView, int index) {
+ LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.WRAP_CONTENT);//设置新建布局参数
+ //设置新建View在Snackbar内垂直居中显示
+ params.gravity= Gravity.CENTER_VERTICAL;
+ addView.setLayoutParams(params);
+ ((Snackbar.SnackbarLayout)mSnackbar.getView()).addView(addView,index);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar布局的外边距
+ * 注:经试验发现,调用margins后再调用 gravityFrameLayout,则margins无效.
+ * 为保证margins有效,应该先调用 gravityFrameLayout,在 show() 之前调用 margins
+ * @param margin
+ * @return
+ */
+ public SnackbarUtils margins(int margin){
+ return margins(margin,margin,margin,margin);
+ }
+
+ /**
+ * 设置Snackbar布局的外边距
+ * 注:经试验发现,调用margins后再调用 gravityFrameLayout,则margins无效.
+ * 为保证margins有效,应该先调用 gravityFrameLayout,在 show() 之前调用 margins
+ * @param left
+ * @param top
+ * @param right
+ * @param bottom
+ * @return
+ */
+ public SnackbarUtils margins(int left, int top, int right, int bottom){
+ ViewGroup.LayoutParams params = mSnackbar.getView().getLayoutParams();
+ ((ViewGroup.MarginLayoutParams) params).setMargins(left,top,right,bottom);
+ mSnackbar.getView().setLayoutParams(params);
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 经试验发现:
+ * 执行过{@link SnackbarUtils#backColor(int)}后:background instanceof ColorDrawable
+ * 未执行过{@link SnackbarUtils#backColor(int)}:background instanceof GradientDrawable
+ * @return
+ */
+ /*
+ public SnackbarUtils radius(){
+ Drawable background = mSnackbar.getView().getBackground();
+ if(background instanceof GradientDrawable){
+ Log.e("Jet","radius():GradientDrawable");
+ }
+ if(background instanceof ColorDrawable){
+ Log.e("Jet","radius():ColorDrawable");
+ }
+ if(background instanceof StateListDrawable){
+ Log.e("Jet","radius():StateListDrawable");
+ }
+ Log.e("Jet","radius()background:"+background.getClass().getSimpleName());
+ return new SnackbarUtils(mSnackbar);
+ }
+ */
+
+ /**
+ * 通过SnackBar现在的背景,获取其设置圆角值时候所需的GradientDrawable实例
+ * @param backgroundOri
+ * @return
+ */
+ private GradientDrawable getRadiusDrawable(Drawable backgroundOri){
+ GradientDrawable background = null;
+ if(backgroundOri instanceof GradientDrawable){
+ background = (GradientDrawable) backgroundOri;
+ }else if(backgroundOri instanceof ColorDrawable){
+ int backgroundColor = ((ColorDrawable)backgroundOri).getColor();
+ background = new GradientDrawable();
+ background.setColor(backgroundColor);
+ }else {
+ }
+ return background;
+ }
+ /**
+ * 设置Snackbar布局的圆角半径值
+ * @param radius 圆角半径
+ * @return
+ */
+ public SnackbarUtils radius(float radius){
+ //将要设置给mSnackbar的背景
+ GradientDrawable background = getRadiusDrawable(mSnackbar.getView().getBackground());
+ if(background != null){
+ radius = radius<=0?12:radius;
+ background.setCornerRadius(radius);
+ mSnackbar.getView().setBackgroundDrawable(background);
+ }
+ return new SnackbarUtils(mSnackbar);
+ }
+
+ /**
+ * 设置Snackbar布局的圆角半径值及边框颜色及边框宽度
+ * @param radius
+ * @param strokeWidth
+ * @param strokeColor
+ * @return
+ */
+ public SnackbarUtils radius(int radius, int strokeWidth, @ColorInt int strokeColor){
+ //将要设置给mSnackbar的背景
+ GradientDrawable background = getRadiusDrawable(mSnackbar.getView().getBackground());
+ if(background != null){
+ radius = radius<=0?12:radius;
+ strokeWidth = strokeWidth<=0?1:(strokeWidth>=mSnackbar.getView().findViewById(R.id.snackbar_text).getPaddingTop()?2:strokeWidth);
+ background.setCornerRadius(radius);
+ background.setStroke(strokeWidth,strokeColor);
+ mSnackbar.getView().setBackgroundDrawable(background);
+ }
+ return new SnackbarUtils(mSnackbar);
+ }
+
+
+
+
+ /**
+ * 显示 mSnackbar
+ */
+ public void show(){
+ if(mSnackbar!=null){
+ mSnackbar.show();
+ }
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/utils/TimeUtil.java b/basicLib/src/main/java/com/azhon/basic/utils/TimeUtil.java
new file mode 100644
index 0000000..2134554
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/utils/TimeUtil.java
@@ -0,0 +1,117 @@
+package com.azhon.basic.utils;
+
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * 创建者: xuhuixiang
+ * 创建时间: 2017/6/20 on 15:12
+ * 描述: TODO 时间格式化
+ */
+public class TimeUtil {
+
+ /**
+ * 将时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String yyyyMMdd(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String HHmmss(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("HH:mm:ss");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String yyyyMMddHHmm(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String yyyyMMddHHmmss(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String MMddHHmm(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String MMddHHmmss(long time) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat("MM-dd HH:mm:ss");
+ Date date = new Date(time);
+ return format.format(date);
+ }
+
+ /***
+ * 时间戳转换为时间
+ *
+ * @param time
+ * @return
+ */
+ public static String byPattern(long time, String pattern) {
+ if (time == 0) {
+ return "";
+ }
+ SimpleDateFormat format = new SimpleDateFormat(pattern);
+ Date date = new Date(time);
+ return format.format(date);
+ }
+}
diff --git a/basicLib/src/main/java/com/azhon/basic/view/LoadingDialog.java b/basicLib/src/main/java/com/azhon/basic/view/LoadingDialog.java
new file mode 100644
index 0000000..e6d57f7
--- /dev/null
+++ b/basicLib/src/main/java/com/azhon/basic/view/LoadingDialog.java
@@ -0,0 +1,57 @@
+package com.azhon.basic.view;
+
+
+import android.app.Dialog;
+import android.content.Context;
+import android.text.TextUtils;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.Window;
+import android.view.WindowManager;
+
+import androidx.annotation.NonNull;
+import androidx.databinding.DataBindingUtil;
+
+import com.azhon.basic.R;
+import com.azhon.basic.databinding.DialogLoadingBinding;
+import com.azhon.basic.utils.DensityUtil;
+
+/**
+ * 项目名: TODO-MVVM
+ * 包名 com.azhon.basic.view
+ * 文件名: LoadingDialog
+ * 创建时间: 2019-03-29 on 11:30
+ * 描述: TODO 等待对话框
+ *
+ * @author xuhuixiang
+ */
+
+public class LoadingDialog extends Dialog {
+
+ private DialogLoadingBinding binding;
+
+
+ public LoadingDialog(@NonNull Context context) {
+ super(context, R.style.LoadingDialog);
+ setCanceledOnTouchOutside(false);
+ binding = DataBindingUtil.inflate(LayoutInflater.from(context),
+ R.layout.dialog_loading, null, false);
+ setContentView(binding.getRoot());
+ Window window = getWindow();
+ WindowManager.LayoutParams lp = window.getAttributes();
+ lp.width = DensityUtil.dip2px(context, 150);
+ lp.height = DensityUtil.dip2px(context, 110);
+ lp.gravity = Gravity.CENTER;
+ window.setAttributes(lp);
+ }
+
+ /**
+ * 设置等待提示信息
+ */
+ public void setLoadingMsg(String msg) {
+ if (TextUtils.isEmpty(msg)) {
+ return;
+ }
+ binding.tvMsg.setText(msg);
+ }
+}
diff --git a/basicLib/src/main/res/drawable/dialog_loading_bg.xml b/basicLib/src/main/res/drawable/dialog_loading_bg.xml
new file mode 100644
index 0000000..f8053bc
--- /dev/null
+++ b/basicLib/src/main/res/drawable/dialog_loading_bg.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/basicLib/src/main/res/layout/dialog_loading.xml b/basicLib/src/main/res/layout/dialog_loading.xml
new file mode 100644
index 0000000..896f127
--- /dev/null
+++ b/basicLib/src/main/res/layout/dialog_loading.xml
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/basicLib/src/main/res/values/strings.xml b/basicLib/src/main/res/values/strings.xml
new file mode 100644
index 0000000..4767363
--- /dev/null
+++ b/basicLib/src/main/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ basicLib
+
diff --git a/basicLib/src/main/res/values/styles.xml b/basicLib/src/main/res/values/styles.xml
new file mode 100644
index 0000000..5aeef57
--- /dev/null
+++ b/basicLib/src/main/res/values/styles.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 0000000..c573848
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,69 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+apply plugin: 'io.codearte.nexus-staging'
+
+buildscript {
+ ext.kotlin_version = '1.5.20'
+
+ repositories {
+ maven {
+ url 'https://artifact.bytedance.com/repository/pangle'
+ }
+ maven { url'https://maven.aliyun.com/nexus/content/groups/public/' }
+ maven { url'https://maven.aliyun.com/nexus/content/repositories/jcenter'}
+ maven { url "https://jitpack.io" }
+ maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
+ google()
+// maven { url 'https://dl.bintray.com/umsdk/release' }
+ maven { url "https://plugins.gradle.org/m2" }
+// google()
+ jcenter()
+ mavenCentral()
+ maven { url 'https://repo1.maven.org/maven2/' }
+ maven { url "https://maven.aliyun.com/repository/public" }
+
+// maven { url "https://plugins.gradle.org/m2" }
+
+
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.0.1'
+ classpath 'org.greenrobot:greendao-gradle-plugin:3.3.0' // 添加GreenDao插件
+ classpath 'com.novoda:bintray-release:0.6.1'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath "com.github.dcendents:android-maven-gradle-plugin:2.0"
+ classpath 'io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.22.0'
+
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+
+
+}
+
+allprojects {
+ repositories {
+ maven {
+ url 'https://artifact.bytedance.com/repository/pangle'
+ }
+ maven { url'https://maven.aliyun.com/nexus/content/groups/public/' }
+ maven { url'https://maven.aliyun.com/nexus/content/repositories/jcenter'}
+ maven { url "https://jitpack.io" }
+ maven { url "https://oss.sonatype.org/content/repositories/snapshots/" }
+ google()
+// maven { url 'https://dl.bintray.com/umsdk/release' }
+// google()
+// jcenter()
+ maven { url "https://jitpack.io" }
+ jcenter()
+ mavenCentral()
+ maven { url 'https://repo1.maven.org/maven2/' }
+ maven { url "https://maven.aliyun.com/repository/public" }
+
+
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..2c57de0
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,28 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx512m
+
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app's APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+android.injected.testOnly = false
+android.enableR8=false
+
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..f6b961f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..12e45a2
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Tue Nov 17 14:36:01 CST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
diff --git a/matisse/.gitignore b/matisse/.gitignore
new file mode 100644
index 0000000..42afabf
--- /dev/null
+++ b/matisse/.gitignore
@@ -0,0 +1 @@
+/build
\ No newline at end of file
diff --git a/matisse/build.gradle b/matisse/build.gradle
new file mode 100644
index 0000000..fe35aa6
--- /dev/null
+++ b/matisse/build.gradle
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.
+ */
+apply plugin: 'com.android.library'
+apply plugin: 'checkstyle'
+
+android {
+ compileSdkVersion 29
+
+ defaultConfig {
+ minSdkVersion 23
+ targetSdkVersion 30
+ }
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+ lintOptions {
+ abortOnError false
+ }
+}
+
+dependencies {
+ implementation fileTree(dir: 'libs', include: ['*.jar'])
+
+ implementation "androidx.appcompat:appcompat:1.1.0"
+ implementation "androidx.annotation:annotation:1.1.0"
+ implementation "androidx.recyclerview:recyclerview:1.0.0"
+ implementation 'it.sephiroth.android.library.imagezoom:library:1.0.4'
+
+ compileOnly 'com.github.bumptech.glide:glide:4.9.0'
+ compileOnly 'com.squareup.picasso:picasso:2.5.2'
+}
+
+
+
+task javadoc(type: Javadoc) {
+ options.encoding = "utf-8"
+}
+
+checkstyle {
+ toolVersion = '7.6.1'
+}
+
+tasks.withType(Javadoc) {
+ options.addStringOption('Xdoclint:none', '-quiet')
+ options.addStringOption('encoding', 'UTF-8')
+}
+
+task checkstyle(type:Checkstyle) {
+ description 'Runs Checkstyle inspection against matisse sourcesets.'
+ group = 'Code Quality'
+ configFile rootProject.file('checkstyle.xml')
+ ignoreFailures = false
+ showViolations true
+ classpath = files()
+ source 'src/main/java'
+}
diff --git a/matisse/gradle.properties b/matisse/gradle.properties
new file mode 100644
index 0000000..e69de29
diff --git a/matisse/proguard-rules.pro b/matisse/proguard-rules.pro
new file mode 100644
index 0000000..f1ffffd
--- /dev/null
+++ b/matisse/proguard-rules.pro
@@ -0,0 +1,19 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in /Library/android-sdk-macosx/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the ProGuard
+# include property in project.properties.
+#
+# 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 *;
+#}
+
+-dontwarn com.squareup.okhttp.**
diff --git a/matisse/src/main/AndroidManifest.xml b/matisse/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..e92ee6b
--- /dev/null
+++ b/matisse/src/main/AndroidManifest.xml
@@ -0,0 +1,30 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/java/com/zhihu/matisse/Matisse.java b/matisse/src/main/java/com/zhihu/matisse/Matisse.java
new file mode 100644
index 0000000..4815bbc
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/Matisse.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.net.Uri;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+
+import com.zhihu.matisse.ui.MatisseActivity;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Entry for Matisse's media selection.
+ */
+public final class Matisse {
+
+ private final WeakReference mContext;
+ private final WeakReference mFragment;
+
+ private Matisse(Activity activity) {
+ this(activity, null);
+ }
+
+ private Matisse(Fragment fragment) {
+ this(fragment.getActivity(), fragment);
+ }
+
+ private Matisse(Activity activity, Fragment fragment) {
+ mContext = new WeakReference<>(activity);
+ mFragment = new WeakReference<>(fragment);
+ }
+
+ /**
+ * Start Matisse from an Activity.
+ *
+ * This Activity's {@link Activity#onActivityResult(int, int, Intent)} will be called when user
+ * finishes selecting.
+ *
+ * @param activity Activity instance.
+ * @return Matisse instance.
+ */
+ public static Matisse from(Activity activity) {
+ return new Matisse(activity);
+ }
+
+ /**
+ * Start Matisse from a Fragment.
+ *
+ * This Fragment's {@link Fragment#onActivityResult(int, int, Intent)} will be called when user
+ * finishes selecting.
+ *
+ * @param fragment Fragment instance.
+ * @return Matisse instance.
+ */
+ public static Matisse from(Fragment fragment) {
+ return new Matisse(fragment);
+ }
+
+ /**
+ * Obtain user selected media' {@link Uri} list in the starting Activity or Fragment.
+ *
+ * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or
+ * {@link Fragment#onActivityResult(int, int, Intent)}.
+ * @return User selected media' {@link Uri} list.
+ */
+ public static List obtainResult(Intent data) {
+ return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION);
+ }
+
+ /**
+ * Obtain user selected media path list in the starting Activity or Fragment.
+ *
+ * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or
+ * {@link Fragment#onActivityResult(int, int, Intent)}.
+ * @return User selected media path list.
+ */
+ public static boolean obtainPathTypeResult(Intent data) {
+ return data.getBooleanExtra(MatisseActivity.EXTRA_RESULT_SELECTION_TYPE,false);
+ }
+ /**
+ * Obtain user selected media path list in the starting Activity or Fragment.
+ *
+ * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or
+ * {@link Fragment#onActivityResult(int, int, Intent)}.
+ * @return User selected media path list.
+ */
+ public static List obtainPathResult(Intent data) {
+ return data.getStringArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH);
+ }
+
+ /**
+ * Obtain state whether user decide to use selected media in original
+ *
+ * @param data Intent passed by {@link Activity#onActivityResult(int, int, Intent)} or
+ * {@link Fragment#onActivityResult(int, int, Intent)}.
+ * @return Whether use original photo
+ */
+ public static boolean obtainOriginalState(Intent data) {
+ return data.getBooleanExtra(MatisseActivity.EXTRA_RESULT_ORIGINAL_ENABLE, false);
+ }
+
+ /**
+ * MIME types the selection constrains on.
+ *
+ * Types not included in the set will still be shown in the grid but can't be chosen.
+ *
+ * @param mimeTypes MIME types set user can choose from.
+ * @return {@link SelectionCreator} to build select specifications.
+ * @see MimeType
+ * @see SelectionCreator
+ */
+ public SelectionCreator choose(Set mimeTypes) {
+ return this.choose(mimeTypes, true);
+ }
+
+ /**
+ * MIME types the selection constrains on.
+ *
+ * Types not included in the set will still be shown in the grid but can't be chosen.
+ *
+ * @param mimeTypes MIME types set user can choose from.
+ * @param mediaTypeExclusive Whether can choose images and videos at the same time during one single choosing
+ * process. true corresponds to not being able to choose images and videos at the same
+ * time, and false corresponds to being able to do this.
+ * @return {@link SelectionCreator} to build select specifications.
+ * @see MimeType
+ * @see SelectionCreator
+ */
+ public SelectionCreator choose(Set mimeTypes, boolean mediaTypeExclusive) {
+ return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
+ }
+
+ @Nullable
+ Activity getActivity() {
+ return mContext.get();
+ }
+
+ @Nullable
+ Fragment getFragment() {
+ return mFragment != null ? mFragment.get() : null;
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/MimeType.java b/matisse/src/main/java/com/zhihu/matisse/MimeType.java
new file mode 100644
index 0000000..edbcced
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/MimeType.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse;
+
+import android.content.ContentResolver;
+import android.net.Uri;
+import android.text.TextUtils;
+
+import androidx.collection.ArraySet;
+
+import android.webkit.MimeTypeMap;
+
+import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;
+
+import java.util.Arrays;
+import java.util.EnumSet;
+import java.util.Locale;
+import java.util.Set;
+
+/**
+ * MIME Type enumeration to restrict selectable media on the selection activity. Matisse only supports images and
+ * videos.
+ *
+ * Good example of mime types Android supports:
+ * https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/media/java/android/media/MediaFile.java
+ */
+@SuppressWarnings("unused")
+public enum MimeType {
+
+ // ============== images ==============
+ JPEG("image/jpeg", arraySetOf(
+ "jpg",
+ "jpeg"
+ )),
+ PNG("image/png", arraySetOf(
+ "png"
+ )),
+ GIF("image/gif", arraySetOf(
+ "gif"
+ )),
+ BMP("image/x-ms-bmp", arraySetOf(
+ "bmp"
+ )),
+ WEBP("image/webp", arraySetOf(
+ "webp"
+ )),
+
+ // ============== videos ==============
+ MPEG("video/mpeg", arraySetOf(
+ "mpeg",
+ "mpg"
+ )),
+ MP4("video/mp4", arraySetOf(
+ "mp4",
+ "m4v"
+ )),
+ QUICKTIME("video/quicktime", arraySetOf(
+ "mov"
+ )),
+ THREEGPP("video/3gpp", arraySetOf(
+ "3gp",
+ "3gpp"
+ )),
+ THREEGPP2("video/3gpp2", arraySetOf(
+ "3g2",
+ "3gpp2"
+ )),
+ MKV("video/x-matroska", arraySetOf(
+ "mkv"
+ )),
+ WEBM("video/webm", arraySetOf(
+ "webm"
+ )),
+ TS("video/mp2ts", arraySetOf(
+ "ts"
+ )),
+ AVI("video/avi", arraySetOf(
+ "avi"
+ ));
+
+ private final String mMimeTypeName;
+ private final Set mExtensions;
+
+ MimeType(String mimeTypeName, Set extensions) {
+ mMimeTypeName = mimeTypeName;
+ mExtensions = extensions;
+ }
+
+ public static Set ofAll() {
+ return EnumSet.allOf(MimeType.class);
+ }
+
+ public static Set of(MimeType type, MimeType... rest) {
+ return EnumSet.of(type, rest);
+ }
+
+ public static Set ofImage() {
+ return EnumSet.of(JPEG, PNG, GIF, BMP, WEBP);
+ }
+
+ public static Set ofImage(boolean onlyGif) {
+ return EnumSet.of(GIF);
+ }
+
+ public static Set ofGif() {
+ return ofImage(true);
+ }
+
+ public static Set ofVideo() {
+ return EnumSet.of(MPEG, MP4, QUICKTIME, THREEGPP, THREEGPP2, MKV, WEBM, TS, AVI);
+ }
+
+ public static boolean isImage(String mimeType) {
+ if (mimeType == null) return false;
+ return mimeType.startsWith("image");
+ }
+
+ public static boolean isVideo(String mimeType) {
+ if (mimeType == null) return false;
+ return mimeType.startsWith("video");
+ }
+
+ public static boolean isGif(String mimeType) {
+ if (mimeType == null) return false;
+ return mimeType.equals(MimeType.GIF.toString());
+ }
+
+ private static Set arraySetOf(String... suffixes) {
+ return new ArraySet<>(Arrays.asList(suffixes));
+ }
+
+ @Override
+ public String toString() {
+ return mMimeTypeName;
+ }
+
+ public boolean checkType(ContentResolver resolver, Uri uri) {
+ MimeTypeMap map = MimeTypeMap.getSingleton();
+ if (uri == null) {
+ return false;
+ }
+ String type = map.getExtensionFromMimeType(resolver.getType(uri));
+ String path = null;
+ // lazy load the path and prevent resolve for multiple times
+ boolean pathParsed = false;
+ for (String extension : mExtensions) {
+ if (extension.equals(type)) {
+ return true;
+ }
+ if (!pathParsed) {
+ // we only resolve the path for one time
+ path = PhotoMetadataUtils.getPath(resolver, uri);
+ if (!TextUtils.isEmpty(path)) {
+ path = path.toLowerCase(Locale.US);
+ }
+ pathParsed = true;
+ }
+ if (path != null && path.endsWith(extension)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java b/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java
new file mode 100644
index 0000000..008cc22
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java
@@ -0,0 +1,377 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.os.Build;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.annotation.StyleRes;
+import androidx.fragment.app.Fragment;
+
+import com.zhihu.matisse.engine.ImageEngine;
+import com.zhihu.matisse.filter.Filter;
+import com.zhihu.matisse.internal.entity.CaptureStrategy;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.listener.OnCheckedListener;
+import com.zhihu.matisse.listener.OnSelectedListener;
+import com.zhihu.matisse.ui.MatisseActivity;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Set;
+
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_BEHIND;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_SENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_FULL_USER;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LOCKED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_NOSENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_LANDSCAPE;
+import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_USER_PORTRAIT;
+
+/**
+ * Fluent API for building media select specification.
+ */
+@SuppressWarnings("unused")
+public final class SelectionCreator {
+ private final Matisse mMatisse;
+ private final SelectionSpec mSelectionSpec;
+
+ @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
+ @IntDef({
+ SCREEN_ORIENTATION_UNSPECIFIED,
+ SCREEN_ORIENTATION_LANDSCAPE,
+ SCREEN_ORIENTATION_PORTRAIT,
+ SCREEN_ORIENTATION_USER,
+ SCREEN_ORIENTATION_BEHIND,
+ SCREEN_ORIENTATION_SENSOR,
+ SCREEN_ORIENTATION_NOSENSOR,
+ SCREEN_ORIENTATION_SENSOR_LANDSCAPE,
+ SCREEN_ORIENTATION_SENSOR_PORTRAIT,
+ SCREEN_ORIENTATION_REVERSE_LANDSCAPE,
+ SCREEN_ORIENTATION_REVERSE_PORTRAIT,
+ SCREEN_ORIENTATION_FULL_SENSOR,
+ SCREEN_ORIENTATION_USER_LANDSCAPE,
+ SCREEN_ORIENTATION_USER_PORTRAIT,
+ SCREEN_ORIENTATION_FULL_USER,
+ SCREEN_ORIENTATION_LOCKED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface ScreenOrientation {
+ }
+
+ /**
+ * Constructs a new specification builder on the context.
+ *
+ * @param matisse a requester context wrapper.
+ * @param mimeTypes MIME type set to select.
+ */
+ SelectionCreator(Matisse matisse, @NonNull Set mimeTypes, boolean mediaTypeExclusive) {
+ mMatisse = matisse;
+ mSelectionSpec = SelectionSpec.getCleanInstance();
+ mSelectionSpec.mimeTypeSet = mimeTypes;
+ mSelectionSpec.mediaTypeExclusive = mediaTypeExclusive;
+ mSelectionSpec.orientation = SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+
+ /**
+ * Whether to show only one media type if choosing medias are only images or videos.
+ *
+ * @param showSingleMediaType whether to show only one media type, either images or videos.
+ * @return {@link SelectionCreator} for fluent API.
+ * @see SelectionSpec#onlyShowImages()
+ * @see SelectionSpec#onlyShowVideos()
+ */
+ public SelectionCreator showSingleMediaType(boolean showSingleMediaType) {
+ mSelectionSpec.showSingleMediaType = showSingleMediaType;
+ return this;
+ }
+
+ /**
+ * Theme for media selecting Activity.
+ *
+ * There are two built-in themes:
+ * 1. com.zhihu.matisse.R.style.Matisse_Zhihu;
+ * 2. com.zhihu.matisse.R.style.Matisse_Dracula
+ * you can define a custom theme derived from the above ones or other themes.
+ *
+ * @param themeId theme resource id. Default value is com.zhihu.matisse.R.style.Matisse_Zhihu.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator theme(@StyleRes int themeId) {
+ mSelectionSpec.themeId = themeId;
+ return this;
+ }
+
+ /**
+ * Show a auto-increased number or a check mark when user select media.
+ *
+ * @param countable true for a auto-increased number from 1, false for a check mark. Default
+ * value is false.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator countable(boolean countable) {
+ mSelectionSpec.countable = countable;
+ return this;
+ }
+ public static int fistMaxNumber = -100;
+ /**
+ * Maximum selectable count.
+ *
+ * @param maxSelectable Maximum selectable count. Default value is 1.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator maxSelectable(int maxSelectable) {
+ if (maxSelectable < 1)
+ throw new IllegalArgumentException("maxSelectable must be greater than or equal to one");
+ if (mSelectionSpec.maxImageSelectable > 0 || mSelectionSpec.maxVideoSelectable > 0)
+ throw new IllegalStateException("already set maxImageSelectable and maxVideoSelectable");
+ mSelectionSpec.maxSelectable = maxSelectable;
+
+ if(fistMaxNumber == -100){
+ fistMaxNumber = maxSelectable;
+ }
+ return this;
+ }
+
+ /**
+ * Only useful when {@link SelectionSpec#mediaTypeExclusive} set true and you want to set different maximum
+ * selectable files for image and video media types.
+ *
+ * @param maxImageSelectable Maximum selectable count for image.
+ * @param maxVideoSelectable Maximum selectable count for video.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator maxSelectablePerMediaType(int maxImageSelectable, int maxVideoSelectable) {
+ if (maxImageSelectable < 1 || maxVideoSelectable < 1)
+ throw new IllegalArgumentException(("max selectable must be greater than or equal to one"));
+ mSelectionSpec.maxSelectable = -1;
+ mSelectionSpec.maxImageSelectable = maxImageSelectable;
+ mSelectionSpec.maxVideoSelectable = maxVideoSelectable;
+ return this;
+ }
+
+ /**
+ * Add filter to filter each selecting item.
+ *
+ * @param filter {@link Filter}
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator addFilter(@NonNull Filter filter) {
+ if (mSelectionSpec.filters == null) {
+ mSelectionSpec.filters = new ArrayList<>();
+ }
+ if (filter == null) throw new IllegalArgumentException("filter cannot be null");
+ mSelectionSpec.filters.add(filter);
+ return this;
+ }
+
+ /**
+ * Determines whether the photo capturing is enabled or not on the media grid view.
+ *
+ * If this value is set true, photo capturing entry will appear only on All Media's page.
+ *
+ * @param enable Whether to enable capturing or not. Default value is false;
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator capture(boolean enable) {
+ mSelectionSpec.capture = enable;
+ return this;
+ }
+
+ /**
+ * Show a original photo check options.Let users decide whether use original photo after select
+ *
+ * @param enable Whether to enable original photo or not
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator originalEnable(boolean enable) {
+ mSelectionSpec.originalable = enable;
+ return this;
+ }
+
+
+ /**
+ * Determines Whether to hide top and bottom toolbar in PreView mode ,when user tap the picture
+ * @param enable
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator autoHideToolbarOnSingleTap(boolean enable) {
+ mSelectionSpec.autoHideToobar = enable;
+ return this;
+ }
+
+ /**
+ * Maximum original size,the unit is MB. Only useful when {link@originalEnable} set true
+ *
+ * @param size Maximum original size. Default value is Integer.MAX_VALUE
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator maxOriginalSize(int size) {
+ mSelectionSpec.originalMaxSize = size;
+ return this;
+ }
+
+ /**
+ * Capture strategy provided for the location to save photos including internal and external
+ * storage and also a authority for {@link androidx.core.content.FileProvider}.
+ *
+ * @param captureStrategy {@link CaptureStrategy}, needed only when capturing is enabled.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator captureStrategy(CaptureStrategy captureStrategy) {
+ mSelectionSpec.captureStrategy = captureStrategy;
+ return this;
+ }
+
+ /**
+ * Set the desired orientation of this activity.
+ *
+ * @param orientation An orientation constant as used in {@link ScreenOrientation}.
+ * Default value is {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_PORTRAIT}.
+ * @return {@link SelectionCreator} for fluent API.
+ * @see Activity#setRequestedOrientation(int)
+ */
+ public SelectionCreator restrictOrientation(@ScreenOrientation int orientation) {
+ mSelectionSpec.orientation = orientation;
+ return this;
+ }
+
+ /**
+ * Set a fixed span count for the media grid. Same for different screen orientations.
+ *
+ * This will be ignored when {@link #gridExpectedSize(int)} is set.
+ *
+ * @param spanCount Requested span count.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator spanCount(int spanCount) {
+ if (spanCount < 1) throw new IllegalArgumentException("spanCount cannot be less than 1");
+ mSelectionSpec.spanCount = spanCount;
+ return this;
+ }
+
+ /**
+ * Set expected size for media grid to adapt to different screen sizes. This won't necessarily
+ * be applied cause the media grid should fill the view container. The measured media grid's
+ * size will be as close to this value as possible.
+ *
+ * @param size Expected media grid size in pixel.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator gridExpectedSize(int size) {
+ mSelectionSpec.gridExpectedSize = size;
+ return this;
+ }
+
+ /**
+ * Photo thumbnail's scale compared to the View's size. It should be a float value in (0.0,
+ * 1.0].
+ *
+ * @param scale Thumbnail's scale in (0.0, 1.0]. Default value is 0.5.
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator thumbnailScale(float scale) {
+ if (scale <= 0f || scale > 1f)
+ throw new IllegalArgumentException("Thumbnail scale must be between (0.0, 1.0]");
+ mSelectionSpec.thumbnailScale = scale;
+ return this;
+ }
+
+ /**
+ * Provide an image engine.
+ *
+ * There are two built-in image engines:
+ * 1. {@link com.zhihu.matisse.engine.impl.GlideEngine}
+ * 2. {@link com.zhihu.matisse.engine.impl.PicassoEngine}
+ * And you can implement your own image engine.
+ *
+ * @param imageEngine {@link ImageEngine}
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator imageEngine(ImageEngine imageEngine) {
+ mSelectionSpec.imageEngine = imageEngine;
+ return this;
+ }
+
+ /**
+ * Set listener for callback immediately when user select or unselect something.
+ *
+ * It's a redundant API with {@link Matisse#obtainResult(Intent)},
+ * we only suggest you to use this API when you need to do something immediately.
+ *
+ * @param listener {@link OnSelectedListener}
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ @NonNull
+ public SelectionCreator setOnSelectedListener(@Nullable OnSelectedListener listener) {
+ mSelectionSpec.onSelectedListener = listener;
+ return this;
+ }
+
+ /**
+ * Set listener for callback immediately when user check or uncheck original.
+ *
+ * @param listener {@link OnSelectedListener}
+ * @return {@link SelectionCreator} for fluent API.
+ */
+ public SelectionCreator setOnCheckedListener(@Nullable OnCheckedListener listener) {
+ mSelectionSpec.onCheckedListener = listener;
+ return this;
+ }
+
+ /**
+ * Start to select media and wait for result.
+ *
+ * @param requestCode Identity of the request Activity or Fragment.
+ */
+ public void forResult(int requestCode) {
+ Activity activity = mMatisse.getActivity();
+ if (activity == null) {
+ return;
+ }
+
+ Intent intent = new Intent(activity, MatisseActivity.class);
+
+ Fragment fragment = mMatisse.getFragment();
+ if (fragment != null) {
+ fragment.startActivityForResult(intent, requestCode);
+ } else {
+ activity.startActivityForResult(intent, requestCode);
+ }
+ }
+
+ public SelectionCreator showPreview(boolean showPreview) {
+ mSelectionSpec.showPreview = showPreview;
+ return this;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/engine/ImageEngine.java b/matisse/src/main/java/com/zhihu/matisse/engine/ImageEngine.java
new file mode 100644
index 0000000..84e5c96
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/engine/ImageEngine.java
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.engine;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+
+/**
+ * Image loader interface. There are predefined {@link com.zhihu.matisse.engine.impl.GlideEngine}
+ * and {@link com.zhihu.matisse.engine.impl.PicassoEngine}.
+ */
+@SuppressWarnings("unused")
+public interface ImageEngine {
+
+ /**
+ * Load thumbnail of a static image resource.
+ *
+ * @param context Context
+ * @param resize Desired size of the origin image
+ * @param placeholder Placeholder drawable when image is not loaded yet
+ * @param imageView ImageView widget
+ * @param uri Uri of the loaded image
+ */
+ void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri);
+
+ /**
+ * Load thumbnail of a gif image resource. You don't have to load an animated gif when it's only
+ * a thumbnail tile.
+ *
+ * @param context Context
+ * @param resize Desired size of the origin image
+ * @param placeholder Placeholder drawable when image is not loaded yet
+ * @param imageView ImageView widget
+ * @param uri Uri of the loaded image
+ */
+ void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri);
+
+ /**
+ * Load a static image resource.
+ *
+ * @param context Context
+ * @param resizeX Desired x-size of the origin image
+ * @param resizeY Desired y-size of the origin image
+ * @param imageView ImageView widget
+ * @param uri Uri of the loaded image
+ */
+ void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri);
+
+ /**
+ * Load a gif image resource.
+ *
+ * @param context Context
+ * @param resizeX Desired x-size of the origin image
+ * @param resizeY Desired y-size of the origin image
+ * @param imageView ImageView widget
+ * @param uri Uri of the loaded image
+ */
+ void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri);
+
+ /**
+ * Whether this implementation supports animated gif.
+ * Just knowledge of it, convenient for users.
+ *
+ * @return true support animated gif, false do not support animated gif.
+ */
+ boolean supportAnimatedGif();
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/engine/impl/GlideEngine.java b/matisse/src/main/java/com/zhihu/matisse/engine/impl/GlideEngine.java
new file mode 100644
index 0000000..51157ae
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/engine/impl/GlideEngine.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.engine.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.Priority;
+import com.bumptech.glide.request.RequestOptions;
+import com.zhihu.matisse.engine.ImageEngine;
+
+/**
+ * {@link ImageEngine} implementation using Glide.
+ */
+
+public class GlideEngine implements ImageEngine {
+
+ @Override
+ public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) {
+ Glide.with(context)
+ .asBitmap() // some .jpeg files are actually gif
+ .load(uri)
+ .apply(new RequestOptions()
+ .override(resize, resize)
+ .placeholder(placeholder)
+ .centerCrop())
+ .into(imageView);
+ }
+
+ @Override
+ public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView,
+ Uri uri) {
+ Glide.with(context)
+ .asBitmap() // some .jpeg files are actually gif
+ .load(uri)
+ .apply(new RequestOptions()
+ .override(resize, resize)
+ .placeholder(placeholder)
+ .centerCrop())
+ .into(imageView);
+ }
+
+ @Override
+ public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
+ Glide.with(context)
+ .load(uri)
+ .apply(new RequestOptions()
+ .override(resizeX, resizeY)
+ .priority(Priority.HIGH)
+ .fitCenter())
+ .into(imageView);
+ }
+
+ @Override
+ public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
+ Glide.with(context)
+ .asGif()
+ .load(uri)
+ .apply(new RequestOptions()
+ .override(resizeX, resizeY)
+ .priority(Priority.HIGH)
+ .fitCenter())
+ .into(imageView);
+ }
+
+ @Override
+ public boolean supportAnimatedGif() {
+ return true;
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/engine/impl/PicassoEngine.java b/matisse/src/main/java/com/zhihu/matisse/engine/impl/PicassoEngine.java
new file mode 100644
index 0000000..4ee33ea
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/engine/impl/PicassoEngine.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.engine.impl;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.widget.ImageView;
+
+import com.squareup.picasso.Picasso;
+import com.zhihu.matisse.engine.ImageEngine;
+
+/**
+ * {@link ImageEngine} implementation using Picasso.
+ */
+
+public class PicassoEngine implements ImageEngine {
+
+ @Override
+ public void loadThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, Uri uri) {
+ Picasso.with(context).load(uri).placeholder(placeholder)
+ .resize(resize, resize)
+ .centerCrop()
+ .into(imageView);
+ }
+
+ @Override
+ public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView,
+ Uri uri) {
+ loadThumbnail(context, resize, placeholder, imageView, uri);
+ }
+
+ @Override
+ public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
+ Picasso.with(context).load(uri).resize(resizeX, resizeY).priority(Picasso.Priority.HIGH)
+ .centerInside().into(imageView);
+ }
+
+ @Override
+ public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) {
+ loadImage(context, resizeX, resizeY, imageView, uri);
+ }
+
+ @Override
+ public boolean supportAnimatedGif() {
+ return false;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/filter/Filter.java b/matisse/src/main/java/com/zhihu/matisse/filter/Filter.java
new file mode 100644
index 0000000..0942364
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/filter/Filter.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.filter;
+
+import android.content.Context;
+
+import com.zhihu.matisse.MimeType;
+import com.zhihu.matisse.SelectionCreator;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.IncapableCause;
+
+import java.util.Set;
+
+/**
+ * Filter for choosing a {@link Item}. You can add multiple Filters through
+ * {@link SelectionCreator#addFilter(Filter)}.
+ */
+@SuppressWarnings("unused")
+public abstract class Filter {
+ /**
+ * Convenient constant for a minimum value.
+ */
+ public static final int MIN = 0;
+ /**
+ * Convenient constant for a maximum value.
+ */
+ public static final int MAX = Integer.MAX_VALUE;
+ /**
+ * Convenient constant for 1024.
+ */
+ public static final int K = 1024;
+
+ /**
+ * Against what mime types this filter applies.
+ */
+ protected abstract Set constraintTypes();
+
+ /**
+ * Invoked for filtering each item.
+ *
+ * @return null if selectable, {@link IncapableCause} if not selectable.
+ */
+ public abstract IncapableCause filter(Context context, Item item);
+
+ /**
+ * Whether an {@link Item} need filtering.
+ */
+ protected boolean needFiltering(Context context, Item item) {
+ for (MimeType type : constraintTypes()) {
+ if (type.checkType(context.getContentResolver(), item.getContentUri())) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java
new file mode 100644
index 0000000..35490e9
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.entity;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import androidx.annotation.Nullable;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.loader.AlbumLoader;
+
+public class Album implements Parcelable {
+ public static final Creator CREATOR = new Creator() {
+ @Nullable
+ @Override
+ public Album createFromParcel(Parcel source) {
+ return new Album(source);
+ }
+
+ @Override
+ public Album[] newArray(int size) {
+ return new Album[size];
+ }
+ };
+ public static final String ALBUM_ID_ALL = String.valueOf(-1);
+ public static final String ALBUM_NAME_ALL = "All";
+
+ private final String mId;
+ private final Uri mCoverUri;
+ private final String mDisplayName;
+ private long mCount;
+
+ public Album(String id, Uri coverUri, String albumName, long count) {
+ mId = id;
+ mCoverUri = coverUri;
+ mDisplayName = albumName;
+ mCount = count;
+ }
+
+ private Album(Parcel source) {
+ mId = source.readString();
+ mCoverUri = source.readParcelable(Uri.class.getClassLoader());
+ mDisplayName = source.readString();
+ mCount = source.readLong();
+ }
+
+ /**
+ * Constructs a new {@link Album} entity from the {@link Cursor}.
+ * This method is not responsible for managing cursor resource, such as close, iterate, and so on.
+ */
+ public static Album valueOf(Cursor cursor) {
+ String clumn = cursor.getString(cursor.getColumnIndex(AlbumLoader.COLUMN_URI));
+ return new Album(
+ cursor.getString(cursor.getColumnIndex("bucket_id")),
+ Uri.parse(clumn != null ? clumn : ""),
+ cursor.getString(cursor.getColumnIndex("bucket_display_name")),
+ cursor.getLong(cursor.getColumnIndex(AlbumLoader.COLUMN_COUNT)));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mId);
+ dest.writeParcelable(mCoverUri, 0);
+ dest.writeString(mDisplayName);
+ dest.writeLong(mCount);
+ }
+
+ public String getId() {
+ return mId;
+ }
+
+ public Uri getCoverUri() {
+ return mCoverUri;
+ }
+
+ public long getCount() {
+ return mCount;
+ }
+
+ public void addCaptureCount() {
+ mCount++;
+ }
+
+ public String getDisplayName(Context context) {
+ if (isAll()) {
+ return context.getString(R.string.album_name_all);
+ }
+ return mDisplayName;
+ }
+
+ public boolean isAll() {
+ return ALBUM_ID_ALL.equals(mId);
+ }
+
+ public boolean isEmpty() {
+ return mCount == 0;
+ }
+
+}
\ No newline at end of file
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/CaptureStrategy.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/CaptureStrategy.java
new file mode 100644
index 0000000..c4de7ca
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/CaptureStrategy.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.entity;
+
+public class CaptureStrategy {
+
+ public final boolean isPublic;
+ public final String authority;
+ public final String directory;
+
+ public CaptureStrategy(boolean isPublic, String authority) {
+ this(isPublic, authority, null);
+ }
+
+ public CaptureStrategy(boolean isPublic, String authority, String directory) {
+ this.isPublic = isPublic;
+ this.authority = authority;
+ this.directory = directory;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java
new file mode 100644
index 0000000..ff5ec46
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.entity;
+
+import android.content.Context;
+import androidx.annotation.IntDef;
+import androidx.fragment.app.FragmentActivity;
+import android.widget.Toast;
+
+import com.zhihu.matisse.internal.ui.widget.IncapableDialog;
+
+import java.lang.annotation.Retention;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+@SuppressWarnings("unused")
+public class IncapableCause {
+ public static final int TOAST = 0x00;
+ public static final int DIALOG = 0x01;
+ public static final int NONE = 0x02;
+
+ @Retention(SOURCE)
+ @IntDef({TOAST, DIALOG, NONE})
+ public @interface Form {
+ }
+
+ private int mForm = TOAST;
+ private String mTitle;
+ private String mMessage;
+
+ public IncapableCause(String message) {
+ mMessage = message;
+ }
+
+ public IncapableCause(String title, String message) {
+ mTitle = title;
+ mMessage = message;
+ }
+
+ public IncapableCause(@Form int form, String message) {
+ mForm = form;
+ mMessage = message;
+ }
+
+ public IncapableCause(@Form int form, String title, String message) {
+ mForm = form;
+ mTitle = title;
+ mMessage = message;
+ }
+
+ public static void handleCause(Context context, IncapableCause cause) {
+ if (cause == null)
+ return;
+
+ switch (cause.mForm) {
+ case NONE:
+ // do nothing.
+ break;
+ case DIALOG:
+ IncapableDialog incapableDialog = IncapableDialog.newInstance(cause.mTitle, cause.mMessage);
+ incapableDialog.show(((FragmentActivity) context).getSupportFragmentManager(),
+ IncapableDialog.class.getName());
+ break;
+ case TOAST:
+ default:
+ Toast.makeText(context, cause.mMessage, Toast.LENGTH_SHORT).show();
+ break;
+ }
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java
new file mode 100644
index 0000000..eda9bb5
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.entity;
+
+import android.content.ContentUris;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.provider.MediaStore;
+import androidx.annotation.Nullable;
+
+import com.zhihu.matisse.MimeType;
+
+public class Item implements Parcelable {
+ public static final Creator- CREATOR = new Creator
- () {
+ @Override
+ @Nullable
+ public Item createFromParcel(Parcel source) {
+ return new Item(source);
+ }
+
+ @Override
+ public Item[] newArray(int size) {
+ return new Item[size];
+ }
+ };
+ public static final long ITEM_ID_CAPTURE = -1;
+ public static final String ITEM_DISPLAY_NAME_CAPTURE = "Capture";
+ public final long id;
+ public final String mimeType;
+ public final Uri uri;
+ public final long size;
+ public final long duration; // only for video, in ms
+
+ private Item(long id, String mimeType, long size, long duration) {
+ this.id = id;
+ this.mimeType = mimeType;
+ Uri contentUri;
+ if (isImage()) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if (isVideo()) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ // ?
+ contentUri = MediaStore.Files.getContentUri("external");
+ }
+ this.uri = ContentUris.withAppendedId(contentUri, id);
+ this.size = size;
+ this.duration = duration;
+ }
+
+ private Item(Parcel source) {
+ id = source.readLong();
+ mimeType = source.readString();
+ uri = source.readParcelable(Uri.class.getClassLoader());
+ size = source.readLong();
+ duration = source.readLong();
+ }
+
+ public static Item valueOf(Cursor cursor) {
+ return new Item(cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID)),
+ cursor.getString(cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE)),
+ cursor.getLong(cursor.getColumnIndex(MediaStore.MediaColumns.SIZE)),
+ cursor.getLong(cursor.getColumnIndex("duration")));
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeLong(id);
+ dest.writeString(mimeType);
+ dest.writeParcelable(uri, 0);
+ dest.writeLong(size);
+ dest.writeLong(duration);
+ }
+
+ public Uri getContentUri() {
+ return uri;
+ }
+
+ public boolean isCapture() {
+ return id == ITEM_ID_CAPTURE;
+ }
+
+ public boolean isImage() {
+ return MimeType.isImage(mimeType);
+ }
+
+ public boolean isGif() {
+ return MimeType.isGif(mimeType);
+ }
+
+ public boolean isVideo() {
+ return MimeType.isVideo(mimeType);
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (!(obj instanceof Item)) {
+ return false;
+ }
+
+ Item other = (Item) obj;
+ return id == other.id
+ && (mimeType != null && mimeType.equals(other.mimeType)
+ || (mimeType == null && other.mimeType == null))
+ && (uri != null && uri.equals(other.uri)
+ || (uri == null && other.uri == null))
+ && size == other.size
+ && duration == other.duration;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = 1;
+ result = 31 * result + Long.valueOf(id).hashCode();
+ if (mimeType != null) {
+ result = 31 * result + mimeType.hashCode();
+ }
+ result = 31 * result + uri.hashCode();
+ result = 31 * result + Long.valueOf(size).hashCode();
+ result = 31 * result + Long.valueOf(duration).hashCode();
+ return result;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java
new file mode 100644
index 0000000..80b7d10
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.entity;
+
+import android.content.pm.ActivityInfo;
+
+import androidx.annotation.StyleRes;
+
+import com.zhihu.matisse.MimeType;
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.engine.ImageEngine;
+import com.zhihu.matisse.engine.impl.GlideEngine;
+import com.zhihu.matisse.filter.Filter;
+import com.zhihu.matisse.listener.OnCheckedListener;
+import com.zhihu.matisse.listener.OnSelectedListener;
+
+import java.util.List;
+import java.util.Set;
+
+public final class SelectionSpec {
+
+ public Set
mimeTypeSet;
+ public boolean mediaTypeExclusive;
+ public boolean showSingleMediaType;
+ @StyleRes
+ public int themeId;
+ public int orientation;
+ public boolean countable;
+ public int maxSelectable;
+ public int maxImageSelectable;
+ public int maxVideoSelectable;
+ public List filters;
+ public boolean capture;
+ public CaptureStrategy captureStrategy;
+ public int spanCount;
+ public int gridExpectedSize;
+ public float thumbnailScale;
+ public ImageEngine imageEngine;
+ public boolean hasInited;
+ public OnSelectedListener onSelectedListener;
+ public boolean originalable;
+ public boolean autoHideToobar;
+ public int originalMaxSize;
+ public OnCheckedListener onCheckedListener;
+ public boolean showPreview;
+
+ private SelectionSpec() {
+ }
+
+ public static SelectionSpec getInstance() {
+ return InstanceHolder.INSTANCE;
+ }
+
+ public static SelectionSpec getCleanInstance() {
+ SelectionSpec selectionSpec = getInstance();
+ selectionSpec.reset();
+ return selectionSpec;
+ }
+
+ private void reset() {
+ mimeTypeSet = null;
+ mediaTypeExclusive = true;
+ showSingleMediaType = false;
+ themeId = R.style.Matisse_Zhihu;
+ orientation = 0;
+ countable = false;
+ maxSelectable = 1;
+ maxImageSelectable = 0;
+ maxVideoSelectable = 0;
+ filters = null;
+ capture = false;
+ captureStrategy = null;
+ spanCount = 3;
+ gridExpectedSize = 0;
+ thumbnailScale = 0.5f;
+ imageEngine = new GlideEngine();
+ hasInited = true;
+ originalable = false;
+ autoHideToobar = false;
+ originalMaxSize = Integer.MAX_VALUE;
+ showPreview = true;
+ }
+
+ public boolean singleSelectionModeEnabled() {
+ return !countable && (maxSelectable == 1 || (maxImageSelectable == 1 && maxVideoSelectable == 1));
+ }
+
+ public boolean needOrientationRestriction() {
+ return orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
+ }
+
+ public boolean onlyShowImages() {
+ return showSingleMediaType && MimeType.ofImage().containsAll(mimeTypeSet);
+ }
+
+ public boolean onlyShowVideos() {
+ return showSingleMediaType && MimeType.ofVideo().containsAll(mimeTypeSet);
+ }
+
+ public boolean onlyShowGif() {
+ return showSingleMediaType && MimeType.ofGif().equals(mimeTypeSet);
+ }
+
+ private static final class InstanceHolder {
+ private static final SelectionSpec INSTANCE = new SelectionSpec();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java
new file mode 100644
index 0000000..157a563
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java
@@ -0,0 +1,293 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.loader;
+
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.os.Build;
+import android.provider.MediaStore;
+
+import androidx.loader.content.CursorLoader;
+
+import com.zhihu.matisse.MimeType;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Load all albums (grouped by bucket_id) into a single cursor.
+ */
+public class AlbumLoader extends CursorLoader {
+
+ private static final String COLUMN_BUCKET_ID = "bucket_id";
+ private static final String COLUMN_BUCKET_DISPLAY_NAME = "bucket_display_name";
+ public static final String COLUMN_URI = "uri";
+ public static final String COLUMN_COUNT = "count";
+ private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");
+
+ private static final String[] COLUMNS = {
+ MediaStore.Files.FileColumns._ID,
+ COLUMN_BUCKET_ID,
+ COLUMN_BUCKET_DISPLAY_NAME,
+ MediaStore.MediaColumns.MIME_TYPE,
+ COLUMN_URI,
+ COLUMN_COUNT};
+
+ private static final String[] PROJECTION = {
+ MediaStore.Files.FileColumns._ID,
+ COLUMN_BUCKET_ID,
+ COLUMN_BUCKET_DISPLAY_NAME,
+ MediaStore.MediaColumns.MIME_TYPE,
+ "COUNT(*) AS " + COLUMN_COUNT};
+
+ private static final String[] PROJECTION_29 = {
+ MediaStore.Files.FileColumns._ID,
+ COLUMN_BUCKET_ID,
+ COLUMN_BUCKET_DISPLAY_NAME,
+ MediaStore.MediaColumns.MIME_TYPE};
+
+ // === params for showSingleMediaType: false ===
+ private static final String SELECTION =
+ "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " OR "
+ + 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 + "=?"
+ + " OR "
+ + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+ private static final String[] SELECTION_ARGS = {
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
+ };
+ // =============================================
+
+ // === params for showSingleMediaType: true ===
+ private static final String SELECTION_FOR_SINGLE_MEDIA_TYPE =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0"
+ + ") GROUP BY (bucket_id";
+ private static final String SELECTION_FOR_SINGLE_MEDIA_TYPE_29 =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionArgsForSingleMediaType(int mediaType) {
+ return new String[]{String.valueOf(mediaType)};
+ }
+ // =============================================
+
+ // === params for showSingleMediaType: true ===
+ private static final String SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0"
+ + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?"
+ + ") GROUP BY (bucket_id";
+ private static final String SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE_29 =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0"
+ + " AND " + MediaStore.MediaColumns.MIME_TYPE + "=?";
+
+ private static String[] getSelectionArgsForSingleMediaGifType(int mediaType) {
+ return new String[]{String.valueOf(mediaType), "image/gif"};
+ }
+ // =============================================
+
+ private static final String BUCKET_ORDER_BY = "datetaken DESC";
+
+ private AlbumLoader(Context context, String selection, String[] selectionArgs) {
+ super(
+ context,
+ QUERY_URI,
+ beforeAndroidTen() ? PROJECTION : PROJECTION_29,
+ selection,
+ selectionArgs,
+ BUCKET_ORDER_BY
+ );
+ }
+
+ public static CursorLoader newInstance(Context context) {
+ String selection;
+ String[] selectionArgs;
+ if (SelectionSpec.getInstance().onlyShowGif()) {
+ selection = beforeAndroidTen()
+ ? SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE : SELECTION_FOR_SINGLE_MEDIA_GIF_TYPE_29;
+ selectionArgs = getSelectionArgsForSingleMediaGifType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ } else if (SelectionSpec.getInstance().onlyShowImages()) {
+ selection = beforeAndroidTen()
+ ? SELECTION_FOR_SINGLE_MEDIA_TYPE : SELECTION_FOR_SINGLE_MEDIA_TYPE_29;
+ selectionArgs = getSelectionArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ } else if (SelectionSpec.getInstance().onlyShowVideos()) {
+ selection = beforeAndroidTen()
+ ? SELECTION_FOR_SINGLE_MEDIA_TYPE : SELECTION_FOR_SINGLE_MEDIA_TYPE_29;
+ selectionArgs = getSelectionArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
+ } else {
+ selection = beforeAndroidTen() ? SELECTION : SELECTION_29;
+ selectionArgs = SELECTION_ARGS;
+ }
+ return new AlbumLoader(context, selection, selectionArgs);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ Cursor albums = super.loadInBackground();
+ MatrixCursor allAlbum = new MatrixCursor(COLUMNS);
+
+ if (beforeAndroidTen()) {
+ int totalCount = 0;
+ Uri allAlbumCoverUri = null;
+ MatrixCursor otherAlbums = new MatrixCursor(COLUMNS);
+ if (albums != null) {
+ while (albums.moveToNext()) {
+ long fileId = albums.getLong(
+ albums.getColumnIndex(MediaStore.Files.FileColumns._ID));
+ long bucketId = albums.getLong(
+ albums.getColumnIndex(COLUMN_BUCKET_ID));
+ String bucketDisplayName = albums.getString(
+ albums.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME));
+ String mimeType = albums.getString(
+ albums.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE));
+ Uri uri = getUri(albums);
+ int count = albums.getInt(albums.getColumnIndex(COLUMN_COUNT));
+
+ otherAlbums.addRow(new String[]{
+ Long.toString(fileId),
+ Long.toString(bucketId), bucketDisplayName, mimeType, uri.toString(),
+ String.valueOf(count)});
+ totalCount += count;
+ }
+ if (albums.moveToFirst()) {
+ allAlbumCoverUri = getUri(albums);
+ }
+ }
+
+ allAlbum.addRow(new String[]{
+ Album.ALBUM_ID_ALL, Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL, null,
+ allAlbumCoverUri == null ? null : allAlbumCoverUri.toString(),
+ String.valueOf(totalCount)});
+
+ return new MergeCursor(new Cursor[]{allAlbum, otherAlbums});
+ } else {
+ int totalCount = 0;
+ Uri allAlbumCoverUri = null;
+
+ // Pseudo GROUP BY
+ Map countMap = new HashMap<>();
+ if (albums != null) {
+ while (albums.moveToNext()) {
+ long bucketId = albums.getLong(albums.getColumnIndex(COLUMN_BUCKET_ID));
+
+ Long count = countMap.get(bucketId);
+ if (count == null) {
+ count = 1L;
+ } else {
+ count++;
+ }
+ countMap.put(bucketId, count);
+ }
+ }
+
+ MatrixCursor otherAlbums = new MatrixCursor(COLUMNS);
+ if (albums != null) {
+ if (albums.moveToFirst()) {
+ allAlbumCoverUri = getUri(albums);
+
+ Set done = new HashSet<>();
+
+ do {
+ long bucketId = albums.getLong(albums.getColumnIndex(COLUMN_BUCKET_ID));
+
+ if (done.contains(bucketId)) {
+ continue;
+ }
+
+ long fileId = albums.getLong(
+ albums.getColumnIndex(MediaStore.Files.FileColumns._ID));
+ String bucketDisplayName = albums.getString(
+ albums.getColumnIndex(COLUMN_BUCKET_DISPLAY_NAME));
+ String mimeType = albums.getString(
+ albums.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE));
+ Uri uri = getUri(albums);
+ long count = countMap.get(bucketId);
+
+ otherAlbums.addRow(new String[]{
+ Long.toString(fileId),
+ Long.toString(bucketId),
+ bucketDisplayName,
+ mimeType,
+ uri.toString(),
+ String.valueOf(count)});
+ done.add(bucketId);
+
+ totalCount += count;
+ } while (albums.moveToNext());
+ }
+ }
+
+ allAlbum.addRow(new String[]{
+ Album.ALBUM_ID_ALL,
+ Album.ALBUM_ID_ALL, Album.ALBUM_NAME_ALL, null,
+ allAlbumCoverUri == null ? null : allAlbumCoverUri.toString(),
+ String.valueOf(totalCount)});
+
+ return new MergeCursor(new Cursor[]{allAlbum, otherAlbums});
+ }
+ }
+
+ private static Uri getUri(Cursor cursor) {
+ long id = cursor.getLong(cursor.getColumnIndex(MediaStore.Files.FileColumns._ID));
+ String mimeType = cursor.getString(
+ cursor.getColumnIndex(MediaStore.MediaColumns.MIME_TYPE));
+ Uri contentUri;
+
+ if (MimeType.isImage(mimeType)) {
+ contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
+ } else if (MimeType.isVideo(mimeType)) {
+ contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
+ } else {
+ // ?
+ contentUri = MediaStore.Files.getContentUri("external");
+ }
+
+ Uri uri = ContentUris.withAppendedId(contentUri, id);
+ return uri;
+ }
+
+ @Override
+ public void onContentChanged() {
+ // FIXME a dirty way to fix loading multiple times
+ }
+
+ /**
+ * @return 是否是 Android 10 (Q) 之前的版本
+ */
+ private static boolean beforeAndroidTen() {
+ return android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.Q;
+ }
+}
\ No newline at end of file
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java
new file mode 100644
index 0000000..7bba0d6
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.loader;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.database.MergeCursor;
+import android.net.Uri;
+import android.provider.MediaStore;
+
+import androidx.loader.content.CursorLoader;
+
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.utils.MediaStoreCompat;
+
+/**
+ * Load images and videos into a single cursor.
+ */
+public class AlbumMediaLoader extends CursorLoader {
+ private static final Uri QUERY_URI = MediaStore.Files.getContentUri("external");
+ private static final String[] PROJECTION = {
+ MediaStore.Files.FileColumns._ID,
+ MediaStore.MediaColumns.DISPLAY_NAME,
+ MediaStore.MediaColumns.MIME_TYPE,
+ MediaStore.MediaColumns.SIZE,
+ "duration"};
+
+ // === params for album ALL && showSingleMediaType: false ===
+ private static final String SELECTION_ALL =
+ "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " OR "
+ + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+ private static final String[] SELECTION_ALL_ARGS = {
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
+ };
+ // ===========================================================
+
+ // === params for album ALL && showSingleMediaType: true ===
+ private static final String SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionArgsForSingleMediaType(int mediaType) {
+ return new String[]{String.valueOf(mediaType)};
+ }
+ // =========================================================
+
+ // === params for ordinary album && showSingleMediaType: false ===
+ private static final String SELECTION_ALBUM =
+ "(" + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " OR "
+ + MediaStore.Files.FileColumns.MEDIA_TYPE + "=?)"
+ + " AND "
+ + " bucket_id=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionAlbumArgs(String albumId) {
+ return new String[]{
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE),
+ String.valueOf(MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO),
+ albumId
+ };
+ }
+ // ===============================================================
+
+ // === params for ordinary album && showSingleMediaType: true ===
+ private static final String SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND "
+ + " bucket_id=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionAlbumArgsForSingleMediaType(int mediaType, String albumId) {
+ return new String[]{String.valueOf(mediaType), albumId};
+ }
+ // ===============================================================
+
+ // === params for album ALL && showSingleMediaType: true && MineType=="image/gif"
+ private static final String SELECTION_ALL_FOR_GIF =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND "
+ + MediaStore.MediaColumns.MIME_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionArgsForGifType(int mediaType) {
+ return new String[]{String.valueOf(mediaType), "image/gif"};
+ }
+ // ===============================================================
+
+ // === params for ordinary album && showSingleMediaType: true && MineType=="image/gif" ===
+ private static final String SELECTION_ALBUM_FOR_GIF =
+ MediaStore.Files.FileColumns.MEDIA_TYPE + "=?"
+ + " AND "
+ + " bucket_id=?"
+ + " AND "
+ + MediaStore.MediaColumns.MIME_TYPE + "=?"
+ + " AND " + MediaStore.MediaColumns.SIZE + ">0";
+
+ private static String[] getSelectionAlbumArgsForGifType(int mediaType, String albumId) {
+ return new String[]{String.valueOf(mediaType), albumId, "image/gif"};
+ }
+ // ===============================================================
+
+ private static final String ORDER_BY = MediaStore.Images.Media.DATE_TAKEN + " DESC";
+ private final boolean mEnableCapture;
+
+ private AlbumMediaLoader(Context context, String selection, String[] selectionArgs, boolean capture) {
+ super(context, QUERY_URI, PROJECTION, selection, selectionArgs, ORDER_BY);
+ mEnableCapture = capture;
+ }
+
+ public static CursorLoader newInstance(Context context, Album album, boolean capture) {
+ String selection;
+ String[] selectionArgs;
+ boolean enableCapture;
+
+ if (album.isAll()) {
+ if (SelectionSpec.getInstance().onlyShowGif()) {
+ selection = SELECTION_ALL_FOR_GIF;
+ selectionArgs = getSelectionArgsForGifType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ } else if (SelectionSpec.getInstance().onlyShowImages()) {
+ selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE;
+ selectionArgs =
+ getSelectionArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE);
+ } else if (SelectionSpec.getInstance().onlyShowVideos()) {
+ selection = SELECTION_ALL_FOR_SINGLE_MEDIA_TYPE;
+ selectionArgs =
+ getSelectionArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO);
+ } else {
+ selection = SELECTION_ALL;
+ selectionArgs = SELECTION_ALL_ARGS;
+ }
+ enableCapture = capture;
+ } else {
+ if (SelectionSpec.getInstance().onlyShowGif()) {
+ selection = SELECTION_ALBUM_FOR_GIF;
+ selectionArgs =
+ getSelectionAlbumArgsForGifType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE, album.getId());
+ } else if (SelectionSpec.getInstance().onlyShowImages()) {
+ selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE;
+ selectionArgs =
+ getSelectionAlbumArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_IMAGE,
+ album.getId());
+ } else if (SelectionSpec.getInstance().onlyShowVideos()) {
+ selection = SELECTION_ALBUM_FOR_SINGLE_MEDIA_TYPE;
+ selectionArgs = getSelectionAlbumArgsForSingleMediaType(
+ MediaStore.Files.FileColumns.MEDIA_TYPE_VIDEO,
+ album.getId());
+ } else {
+ selection = SELECTION_ALBUM;
+ selectionArgs = getSelectionAlbumArgs(album.getId());
+ }
+ enableCapture = false;
+ }
+ return new AlbumMediaLoader(context, selection, selectionArgs, enableCapture);
+ }
+
+ @Override
+ public Cursor loadInBackground() {
+ Cursor result = super.loadInBackground();
+ if (!mEnableCapture || !MediaStoreCompat.hasCameraFeature(getContext())) {
+ return result;
+ }
+ MatrixCursor dummy = new MatrixCursor(PROJECTION);
+ dummy.addRow(new Object[]{Item.ITEM_ID_CAPTURE, Item.ITEM_DISPLAY_NAME_CAPTURE, "", 0, 0});
+ return new MergeCursor(new Cursor[]{dummy, result});
+ }
+
+ @Override
+ public void onContentChanged() {
+ // FIXME a dirty way to fix loading multiple times
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java
new file mode 100644
index 0000000..ceabef6
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.model;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import androidx.fragment.app.FragmentActivity;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+
+import com.zhihu.matisse.internal.loader.AlbumLoader;
+
+import java.lang.ref.WeakReference;
+
+public class AlbumCollection implements LoaderManager.LoaderCallbacks {
+ private static final int LOADER_ID = 1;
+ private static final String STATE_CURRENT_SELECTION = "state_current_selection";
+ private WeakReference mContext;
+ private LoaderManager mLoaderManager;
+ private AlbumCallbacks mCallbacks;
+ private int mCurrentSelection;
+ private boolean mLoadFinished;
+
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ Context context = mContext.get();
+ if (context == null) {
+ return null;
+ }
+ mLoadFinished = false;
+ return AlbumLoader.newInstance(context);
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor data) {
+ Context context = mContext.get();
+ if (context == null) {
+ return;
+ }
+
+ if (!mLoadFinished) {
+ mLoadFinished = true;
+ mCallbacks.onAlbumLoad(data);
+ }
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ Context context = mContext.get();
+ if (context == null) {
+ return;
+ }
+
+ mCallbacks.onAlbumReset();
+ }
+
+ public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
+ mContext = new WeakReference(activity);
+ mLoaderManager = activity.getSupportLoaderManager();
+ mCallbacks = callbacks;
+ }
+
+ public void onRestoreInstanceState(Bundle savedInstanceState) {
+ if (savedInstanceState == null) {
+ return;
+ }
+
+ mCurrentSelection = savedInstanceState.getInt(STATE_CURRENT_SELECTION);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putInt(STATE_CURRENT_SELECTION, mCurrentSelection);
+ }
+
+ public void onDestroy() {
+ if (mLoaderManager != null) {
+ mLoaderManager.destroyLoader(LOADER_ID);
+ }
+ mCallbacks = null;
+ }
+
+ public void loadAlbums() {
+ mLoaderManager.initLoader(LOADER_ID, null, this);
+ }
+
+ public int getCurrentSelection() {
+ return mCurrentSelection;
+ }
+
+ public void setStateCurrentSelection(int currentSelection) {
+ mCurrentSelection = currentSelection;
+ }
+
+ public interface AlbumCallbacks {
+ void onAlbumLoad(Cursor cursor);
+
+ void onAlbumReset();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java
new file mode 100644
index 0000000..fa25939
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.model;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+import androidx.loader.app.LoaderManager;
+import androidx.loader.content.Loader;
+
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.loader.AlbumMediaLoader;
+
+import java.lang.ref.WeakReference;
+
+public class AlbumMediaCollection implements LoaderManager.LoaderCallbacks {
+ private static final int LOADER_ID = 2;
+ private static final String ARGS_ALBUM = "args_album";
+ private static final String ARGS_ENABLE_CAPTURE = "args_enable_capture";
+ private WeakReference mContext;
+ private LoaderManager mLoaderManager;
+ private AlbumMediaCallbacks mCallbacks;
+
+ @Override
+ public Loader onCreateLoader(int id, Bundle args) {
+ Context context = mContext.get();
+ if (context == null) {
+ return null;
+ }
+
+ Album album = args.getParcelable(ARGS_ALBUM);
+ if (album == null) {
+ return null;
+ }
+
+ return AlbumMediaLoader.newInstance(context, album,
+ album.isAll() && args.getBoolean(ARGS_ENABLE_CAPTURE, false));
+ }
+
+ @Override
+ public void onLoadFinished(Loader loader, Cursor data) {
+ Context context = mContext.get();
+ if (context == null) {
+ return;
+ }
+
+ mCallbacks.onAlbumMediaLoad(data);
+ }
+
+ @Override
+ public void onLoaderReset(Loader loader) {
+ Context context = mContext.get();
+ if (context == null) {
+ return;
+ }
+
+ mCallbacks.onAlbumMediaReset();
+ }
+
+ public void onCreate(@NonNull FragmentActivity context, @NonNull AlbumMediaCallbacks callbacks) {
+ mContext = new WeakReference(context);
+ mLoaderManager = context.getSupportLoaderManager();
+ mCallbacks = callbacks;
+ }
+
+ public void onDestroy() {
+ if (mLoaderManager != null) {
+ mLoaderManager.destroyLoader(LOADER_ID);
+ }
+ mCallbacks = null;
+ }
+
+ public void load(@Nullable Album target) {
+ load(target, false);
+ }
+
+ public void load(@Nullable Album target, boolean enableCapture) {
+ Bundle args = new Bundle();
+ args.putParcelable(ARGS_ALBUM, target);
+ args.putBoolean(ARGS_ENABLE_CAPTURE, enableCapture);
+ mLoaderManager.initLoader(LOADER_ID, args, this);
+ }
+
+ public interface AlbumMediaCallbacks {
+
+ void onAlbumMediaLoad(Cursor cursor);
+
+ void onAlbumMediaReset();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java
new file mode 100644
index 0000000..97d74cc
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.model;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.os.Bundle;
+import android.util.Log;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.SelectionCreator;
+import com.zhihu.matisse.internal.entity.IncapableCause;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.ui.widget.CheckView;
+import com.zhihu.matisse.internal.utils.PathUtils;
+import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+@SuppressWarnings("unused")
+public class SelectedItemCollection {
+
+ public static final String STATE_SELECTION = "state_selection";
+ public static final String STATE_COLLECTION_TYPE = "state_collection_type";
+ /**
+ * Empty collection
+ */
+ public static final int COLLECTION_UNDEFINED = 0x00;
+ /**
+ * Collection only with images
+ */
+ public static final int COLLECTION_IMAGE = 0x01;
+ /**
+ * Collection only with videos
+ */
+ public static final int COLLECTION_VIDEO = 0x01 << 1;
+ /**
+ * Collection with images and videos.
+ */
+ public static final int COLLECTION_MIXED = COLLECTION_IMAGE | COLLECTION_VIDEO;
+ private final Context mContext;
+ private Set- mItems;
+ private int mCollectionType = COLLECTION_UNDEFINED;
+
+ public SelectedItemCollection(Context context) {
+ mContext = context;
+ }
+
+ public void onCreate(Bundle bundle) {
+ if (bundle == null) {
+ mItems = new LinkedHashSet<>();
+ } else {
+ List
- saved = bundle.getParcelableArrayList(STATE_SELECTION);
+ mItems = new LinkedHashSet<>(saved);
+ mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED);
+ }
+ }
+
+ public void setDefaultSelection(List
- uris) {
+ mItems.addAll(uris);
+ }
+
+ public void onSaveInstanceState(Bundle outState) {
+ outState.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems));
+ outState.putInt(STATE_COLLECTION_TYPE, mCollectionType);
+ }
+
+ public Bundle getDataWithBundle() {
+ Bundle bundle = new Bundle();
+ bundle.putParcelableArrayList(STATE_SELECTION, new ArrayList<>(mItems));
+ bundle.putInt(STATE_COLLECTION_TYPE, mCollectionType);
+ return bundle;
+ }
+
+ public boolean add(Item item) {
+ if (typeConflict(item)) {
+ throw new IllegalArgumentException("Can't select images and videos at the same time.");
+ }
+ boolean added = mItems.add(item);
+ if (added) {
+ if (mCollectionType == COLLECTION_UNDEFINED) {
+ if (item.isImage()) {
+ mCollectionType = COLLECTION_IMAGE;
+ } else if (item.isVideo()) {
+ mCollectionType = COLLECTION_VIDEO;
+ }
+ } else if (mCollectionType == COLLECTION_IMAGE) {
+ if (item.isVideo()) {
+ mCollectionType = COLLECTION_MIXED;
+ }
+ } else if (mCollectionType == COLLECTION_VIDEO) {
+ if (item.isImage()) {
+ mCollectionType = COLLECTION_MIXED;
+ }
+ }
+ }
+
+ if(item.isVideo()&&mItems.size()>0){
+ SelectionSpec.getInstance().maxSelectable = 1;
+ }else{
+ SelectionSpec.getInstance().maxSelectable = SelectionCreator.fistMaxNumber;
+ }
+
+ return added;
+ }
+
+ public boolean remove(Item item) {
+ boolean removed = mItems.remove(item);
+ if (removed) {
+ if (mItems.size() == 0) {
+ mCollectionType = COLLECTION_UNDEFINED;
+ } else {
+ if (mCollectionType == COLLECTION_MIXED) {
+ refineCollectionType();
+ }
+ }
+ }
+ return removed;
+ }
+
+ public void overwrite(ArrayList
- items, int collectionType) {
+ if (items.size() == 0) {
+ mCollectionType = COLLECTION_UNDEFINED;
+ } else {
+ mCollectionType = collectionType;
+ }
+ mItems.clear();
+ mItems.addAll(items);
+ }
+
+
+ public List
- asList() {
+ return new ArrayList<>(mItems);
+ }
+
+ public List
asListOfUri() {
+ List uris = new ArrayList<>();
+ for (Item item : mItems) {
+ uris.add(item.getContentUri());
+ }
+ return uris;
+ }
+
+ public List asListOfString() {
+ List paths = new ArrayList<>();
+ for (Item item : mItems) {
+ paths.add(PathUtils.getPath(mContext, item.getContentUri()));
+ }
+ return paths;
+ }
+
+ public boolean asListOfType() {
+ boolean isVideo = false;
+ for (Item item : mItems) {
+ if(item.isVideo()){
+ return true;
+ }
+ }
+ return isVideo;
+ }
+
+ public boolean isEmpty() {
+ return mItems == null || mItems.isEmpty();
+ }
+
+ public boolean isSelected(Item item) {
+ return mItems.contains(item);
+ }
+
+ public IncapableCause isAcceptable(Item item) {
+ if (maxSelectableReached()) {
+ int maxSelectable = currentMaxSelectable();
+ String cause;
+
+// try {
+// cause = mContext.getResources().getQuantityString(
+// R.string.error_over_count,
+// maxSelectable,
+// maxSelectable
+// );
+// } catch (Resources.NotFoundException e) {
+ cause = mContext.getString(
+ R.string.error_over_count,
+ maxSelectable
+ );
+// } catch (NoClassDefFoundError e) {
+// cause = mContext.getString(
+// R.string.error_over_count,
+// maxSelectable
+// );
+// }
+
+ return new IncapableCause(cause);
+ } else if (typeConflict(item)) {
+ return new IncapableCause(mContext.getString(R.string.error_type_conflict));
+ }
+
+ return PhotoMetadataUtils.isAcceptable(mContext, item);
+ }
+
+ public boolean maxSelectableReached() {
+ return mItems.size() == currentMaxSelectable();
+ }
+
+ public int maxSize;
+ // depends
+ private int currentMaxSelectable() {
+ SelectionSpec spec = SelectionSpec.getInstance();
+ if (spec.maxSelectable > 0) {
+ maxSize =spec.maxSelectable;
+ return spec.maxSelectable;
+ } else if (mCollectionType == COLLECTION_IMAGE) {
+ return spec.maxImageSelectable;
+ } else if (mCollectionType == COLLECTION_VIDEO) {
+ return spec.maxVideoSelectable;
+ } else {
+ return spec.maxSelectable;
+ }
+ }
+
+ public int getCollectionType() {
+ return mCollectionType;
+ }
+
+ private void refineCollectionType() {
+ boolean hasImage = false;
+ boolean hasVideo = false;
+ for (Item i : mItems) {
+ if (i.isImage() && !hasImage) hasImage = true;
+ if (i.isVideo() && !hasVideo) hasVideo = true;
+ }
+ if (hasImage && hasVideo) {
+ mCollectionType = COLLECTION_MIXED;
+ } else if (hasImage) {
+ mCollectionType = COLLECTION_IMAGE;
+ } else if (hasVideo) {
+ mCollectionType = COLLECTION_VIDEO;
+ }
+ }
+
+ /**
+ * Determine whether there will be conflict media types. A user can only select images and videos at the same time
+ * while {@link SelectionSpec#mediaTypeExclusive} is set to false.
+ */
+ public boolean typeConflict(Item item) {
+ return SelectionSpec.getInstance().mediaTypeExclusive
+ && ((item.isImage() && (mCollectionType == COLLECTION_VIDEO || mCollectionType == COLLECTION_MIXED))
+ || (item.isVideo() && (mCollectionType == COLLECTION_IMAGE || mCollectionType == COLLECTION_MIXED)));
+ }
+
+ public int count() {
+ return mItems.size();
+ }
+
+ public int checkedNumOf(Item item) {
+ int index = new ArrayList<>(mItems).indexOf(item);
+ return index == -1 ? CheckView.UNCHECKED : index + 1;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java
new file mode 100644
index 0000000..01c82e3
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui;
+
+import android.database.Cursor;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.model.AlbumMediaCollection;
+import com.zhihu.matisse.internal.ui.adapter.PreviewPagerAdapter;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AlbumPreviewActivity extends BasePreviewActivity implements
+ AlbumMediaCollection.AlbumMediaCallbacks {
+
+ public static final String EXTRA_ALBUM = "extra_album";
+ public static final String EXTRA_ITEM = "extra_item";
+
+ private AlbumMediaCollection mCollection = new AlbumMediaCollection();
+
+ private boolean mIsAlreadySetPosition;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!SelectionSpec.getInstance().hasInited) {
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ mCollection.onCreate(this, this);
+ Album album = getIntent().getParcelableExtra(EXTRA_ALBUM);
+ mCollection.load(album);
+
+ Item item = getIntent().getParcelableExtra(EXTRA_ITEM);
+ if (mSpec.countable) {
+ mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
+ } else {
+ mCheckView.setChecked(mSelectedCollection.isSelected(item));
+ }
+ updateSize(item);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mCollection.onDestroy();
+ }
+
+ @Override
+ public void onAlbumMediaLoad(Cursor cursor) {
+ List- items = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ items.add(Item.valueOf(cursor));
+ }
+// cursor.close();
+
+ if (items.isEmpty()) {
+ return;
+ }
+
+ PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
+ adapter.addAll(items);
+ adapter.notifyDataSetChanged();
+ if (!mIsAlreadySetPosition) {
+ //onAlbumMediaLoad is called many times..
+ mIsAlreadySetPosition = true;
+ Item selected = getIntent().getParcelableExtra(EXTRA_ITEM);
+ int selectedIndex = items.indexOf(selected);
+ mPager.setCurrentItem(selectedIndex, false);
+ mPreviousPos = selectedIndex;
+ }
+ }
+
+ @Override
+ public void onAlbumMediaReset() {
+
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java
new file mode 100644
index 0000000..de9948b
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java
@@ -0,0 +1,351 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.graphics.Color;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.viewpager.widget.ViewPager;
+import androidx.interpolator.view.animation.FastOutSlowInInterpolator;
+import androidx.appcompat.app.AppCompatActivity;
+import android.view.View;
+import android.view.WindowManager;
+import android.widget.FrameLayout;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.IncapableCause;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.model.SelectedItemCollection;
+import com.zhihu.matisse.internal.ui.adapter.PreviewPagerAdapter;
+import com.zhihu.matisse.internal.ui.widget.CheckRadioView;
+import com.zhihu.matisse.internal.ui.widget.CheckView;
+import com.zhihu.matisse.internal.ui.widget.IncapableDialog;
+import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;
+import com.zhihu.matisse.internal.utils.Platform;
+import com.zhihu.matisse.listener.OnFragmentInteractionListener;
+
+public abstract class BasePreviewActivity extends AppCompatActivity implements View.OnClickListener,
+ ViewPager.OnPageChangeListener, OnFragmentInteractionListener {
+
+ public static final String EXTRA_DEFAULT_BUNDLE = "extra_default_bundle";
+ public static final String EXTRA_RESULT_BUNDLE = "extra_result_bundle";
+ public static final String EXTRA_RESULT_APPLY = "extra_result_apply";
+ public static final String EXTRA_RESULT_ORIGINAL_ENABLE = "extra_result_original_enable";
+ public static final String CHECK_STATE = "checkState";
+
+ protected final SelectedItemCollection mSelectedCollection = new SelectedItemCollection(this);
+ protected SelectionSpec mSpec;
+ protected ViewPager mPager;
+
+ protected PreviewPagerAdapter mAdapter;
+
+ protected CheckView mCheckView;
+ protected TextView mButtonBack;
+ protected TextView mButtonApply;
+ protected TextView mSize;
+
+ protected int mPreviousPos = -1;
+
+ private LinearLayout mOriginalLayout;
+ private CheckRadioView mOriginal;
+ protected boolean mOriginalEnable;
+
+ private FrameLayout mBottomToolbar;
+ private FrameLayout mTopToolbar;
+ private boolean mIsToolbarHide = false;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ setTheme(SelectionSpec.getInstance().themeId);
+ super.onCreate(savedInstanceState);
+ if (!SelectionSpec.getInstance().hasInited) {
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ setContentView(R.layout.activity_media_preview);
+ if (Platform.hasKitKat()) {
+ getWindow().addFlags(WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS);
+ }
+
+ mSpec = SelectionSpec.getInstance();
+ if (mSpec.needOrientationRestriction()) {
+ setRequestedOrientation(mSpec.orientation);
+ }
+
+ if (savedInstanceState == null) {
+ mSelectedCollection.onCreate(getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE));
+ mOriginalEnable = getIntent().getBooleanExtra(EXTRA_RESULT_ORIGINAL_ENABLE, false);
+ } else {
+ mSelectedCollection.onCreate(savedInstanceState);
+ mOriginalEnable = savedInstanceState.getBoolean(CHECK_STATE);
+ }
+ mButtonBack = (TextView) findViewById(R.id.button_back);
+ mButtonApply = (TextView) findViewById(R.id.button_apply);
+ mSize = (TextView) findViewById(R.id.size);
+ mButtonBack.setOnClickListener(this);
+ mButtonApply.setOnClickListener(this);
+
+ mPager = (ViewPager) findViewById(R.id.pager);
+ mPager.addOnPageChangeListener(this);
+ mAdapter = new PreviewPagerAdapter(getSupportFragmentManager(), null);
+ mPager.setAdapter(mAdapter);
+ mCheckView = (CheckView) findViewById(R.id.check_view);
+ mCheckView.setCountable(mSpec.countable);
+ mBottomToolbar = findViewById(R.id.bottom_toolbar);
+ mTopToolbar = findViewById(R.id.top_toolbar);
+
+ mCheckView.setOnClickListener(v -> {
+ Item item = mAdapter.getMediaItem(mPager.getCurrentItem());
+ if (mSelectedCollection.isSelected(item)) {
+ mSelectedCollection.remove(item);
+ if (mSpec.countable) {
+ mCheckView.setCheckedNum(CheckView.UNCHECKED);
+ } else {
+ mCheckView.setChecked(false);
+ }
+ } else {
+ if (assertAddSelection(item)) {
+ mSelectedCollection.add(item);
+ if (mSpec.countable) {
+ mCheckView.setCheckedNum(mSelectedCollection.checkedNumOf(item));
+ } else {
+ mCheckView.setChecked(true);
+ }
+ }
+ }
+ updateApplyButton();
+
+ if (mSpec.onSelectedListener != null) {
+ mSpec.onSelectedListener.onSelected(
+ mSelectedCollection.asListOfUri(), mSelectedCollection.asListOfString());
+ }
+ });
+
+
+ mOriginalLayout = findViewById(R.id.originalLayout);
+ mOriginal = findViewById(R.id.original);
+ mOriginalLayout.setOnClickListener(v -> {
+
+ int count = countOverMaxSize();
+ if (count > 0) {
+ IncapableDialog incapableDialog = IncapableDialog.newInstance("",
+ getString(R.string.error_over_original_count, count, mSpec.originalMaxSize));
+ incapableDialog.show(getSupportFragmentManager(),
+ IncapableDialog.class.getName());
+ return;
+ }
+
+ mOriginalEnable = !mOriginalEnable;
+ mOriginal.setChecked(mOriginalEnable);
+ if (!mOriginalEnable) {
+ mOriginal.setColor(Color.WHITE);
+ }
+
+
+ if (mSpec.onCheckedListener != null) {
+ mSpec.onCheckedListener.onCheck(mOriginalEnable);
+ }
+ });
+
+ updateApplyButton();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ mSelectedCollection.onSaveInstanceState(outState);
+ outState.putBoolean("checkState", mOriginalEnable);
+ super.onSaveInstanceState(outState);
+ }
+
+ @Override
+ public void onBackPressed() {
+ sendBackResult(false);
+ super.onBackPressed();
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.button_back) {
+ onBackPressed();
+ } else if (v.getId() == R.id.button_apply) {
+ sendBackResult(true);
+ finish();
+ }
+ }
+
+ @Override
+ public void onClick() {
+ if (!mSpec.autoHideToobar) {
+ return;
+ }
+
+ if (mIsToolbarHide) {
+ mTopToolbar.animate()
+ .setInterpolator(new FastOutSlowInInterpolator())
+ .translationYBy(mTopToolbar.getMeasuredHeight())
+ .start();
+ mBottomToolbar.animate()
+ .translationYBy(-mBottomToolbar.getMeasuredHeight())
+ .setInterpolator(new FastOutSlowInInterpolator())
+ .start();
+ } else {
+ mTopToolbar.animate()
+ .setInterpolator(new FastOutSlowInInterpolator())
+ .translationYBy(-mTopToolbar.getMeasuredHeight())
+ .start();
+ mBottomToolbar.animate()
+ .setInterpolator(new FastOutSlowInInterpolator())
+ .translationYBy(mBottomToolbar.getMeasuredHeight())
+ .start();
+ }
+
+ mIsToolbarHide = !mIsToolbarHide;
+
+ }
+
+ @Override
+ public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+
+ }
+
+ @Override
+ public void onPageSelected(int position) {
+ PreviewPagerAdapter adapter = (PreviewPagerAdapter) mPager.getAdapter();
+ if (mPreviousPos != -1 && mPreviousPos != position) {
+ ((PreviewItemFragment) adapter.instantiateItem(mPager, mPreviousPos)).resetView();
+
+ Item item = adapter.getMediaItem(position);
+ if (mSpec.countable) {
+ int checkedNum = mSelectedCollection.checkedNumOf(item);
+ mCheckView.setCheckedNum(checkedNum);
+ if (checkedNum > 0) {
+ mCheckView.setEnabled(true);
+ } else {
+ mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
+ }
+ } else {
+ boolean checked = mSelectedCollection.isSelected(item);
+ mCheckView.setChecked(checked);
+ if (checked) {
+ mCheckView.setEnabled(true);
+ } else {
+ mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached());
+ }
+ }
+ updateSize(item);
+ }
+ mPreviousPos = position;
+ }
+
+ @Override
+ public void onPageScrollStateChanged(int state) {
+
+ }
+
+ private void updateApplyButton() {
+ int selectedCount = mSelectedCollection.count();
+ if (selectedCount == 0) {
+ mButtonApply.setText(R.string.button_apply_default);
+ mButtonApply.setEnabled(false);
+ } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) {
+ mButtonApply.setText(R.string.button_apply_default);
+ mButtonApply.setEnabled(true);
+ } else {
+ mButtonApply.setEnabled(true);
+ mButtonApply.setText(getString(R.string.button_apply, selectedCount));
+ }
+
+ if (mSpec.originalable) {
+ mOriginalLayout.setVisibility(View.VISIBLE);
+ updateOriginalState();
+ } else {
+ mOriginalLayout.setVisibility(View.GONE);
+ }
+ }
+
+
+ private void updateOriginalState() {
+ mOriginal.setChecked(mOriginalEnable);
+ if (!mOriginalEnable) {
+ mOriginal.setColor(Color.WHITE);
+ }
+
+ if (countOverMaxSize() > 0) {
+
+ if (mOriginalEnable) {
+ IncapableDialog incapableDialog = IncapableDialog.newInstance("",
+ getString(R.string.error_over_original_size, mSpec.originalMaxSize));
+ incapableDialog.show(getSupportFragmentManager(),
+ IncapableDialog.class.getName());
+
+ mOriginal.setChecked(false);
+ mOriginal.setColor(Color.WHITE);
+ mOriginalEnable = false;
+ }
+ }
+ }
+
+
+ private int countOverMaxSize() {
+ int count = 0;
+ int selectedCount = mSelectedCollection.count();
+ for (int i = 0; i < selectedCount; i++) {
+ Item item = mSelectedCollection.asList().get(i);
+ if (item.isImage()) {
+ float size = PhotoMetadataUtils.getSizeInMB(item.size);
+ if (size > mSpec.originalMaxSize) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ protected void updateSize(Item item) {
+ if (item.isGif()) {
+ mSize.setVisibility(View.VISIBLE);
+ mSize.setText(PhotoMetadataUtils.getSizeInMB(item.size) + "M");
+ } else {
+ mSize.setVisibility(View.GONE);
+ }
+
+ if (item.isVideo()) {
+ mOriginalLayout.setVisibility(View.GONE);
+ } else if (mSpec.originalable) {
+ mOriginalLayout.setVisibility(View.VISIBLE);
+ }
+ }
+
+ protected void sendBackResult(boolean apply) {
+ Intent intent = new Intent();
+ intent.putExtra(EXTRA_RESULT_BUNDLE, mSelectedCollection.getDataWithBundle());
+ intent.putExtra(EXTRA_RESULT_APPLY, apply);
+ intent.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
+ setResult(Activity.RESULT_OK, intent);
+ }
+
+ private boolean assertAddSelection(Item item) {
+ IncapableCause cause = mSelectedCollection.isAcceptable(item);
+ IncapableCause.handleCause(this, cause);
+ return cause == null;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java
new file mode 100644
index 0000000..86c46e2
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.model.AlbumMediaCollection;
+import com.zhihu.matisse.internal.model.SelectedItemCollection;
+import com.zhihu.matisse.internal.ui.adapter.AlbumMediaAdapter;
+import com.zhihu.matisse.internal.ui.widget.MediaGridInset;
+import com.zhihu.matisse.internal.utils.UIUtils;
+
+public class MediaSelectionFragment extends Fragment implements
+ AlbumMediaCollection.AlbumMediaCallbacks, AlbumMediaAdapter.CheckStateListener,
+ AlbumMediaAdapter.OnMediaClickListener {
+
+ public static final String EXTRA_ALBUM = "extra_album";
+
+ private final AlbumMediaCollection mAlbumMediaCollection = new AlbumMediaCollection();
+ private RecyclerView mRecyclerView;
+ private AlbumMediaAdapter mAdapter;
+ private SelectionProvider mSelectionProvider;
+ private AlbumMediaAdapter.CheckStateListener mCheckStateListener;
+ private AlbumMediaAdapter.OnMediaClickListener mOnMediaClickListener;
+
+ public static MediaSelectionFragment newInstance(Album album) {
+ MediaSelectionFragment fragment = new MediaSelectionFragment();
+ Bundle args = new Bundle();
+ args.putParcelable(EXTRA_ALBUM, album);
+ fragment.setArguments(args);
+ return fragment;
+ }
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof SelectionProvider) {
+ mSelectionProvider = (SelectionProvider) context;
+ } else {
+ throw new IllegalStateException("Context must implement SelectionProvider.");
+ }
+ if (context instanceof AlbumMediaAdapter.CheckStateListener) {
+ mCheckStateListener = (AlbumMediaAdapter.CheckStateListener) context;
+ }
+ if (context instanceof AlbumMediaAdapter.OnMediaClickListener) {
+ mOnMediaClickListener = (AlbumMediaAdapter.OnMediaClickListener) context;
+ }
+ }
+
+ @Nullable
+ @Override
+ public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container,
+ @Nullable Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_media_selection, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ mRecyclerView = (RecyclerView) view.findViewById(R.id.recyclerview);
+ }
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Album album = getArguments().getParcelable(EXTRA_ALBUM);
+
+ mAdapter = new AlbumMediaAdapter(getContext(),
+ mSelectionProvider.provideSelectedItemCollection(), mRecyclerView);
+ mAdapter.registerCheckStateListener(this);
+ mAdapter.registerOnMediaClickListener(this);
+ mRecyclerView.setHasFixedSize(true);
+
+ int spanCount;
+ SelectionSpec selectionSpec = SelectionSpec.getInstance();
+ if (selectionSpec.gridExpectedSize > 0) {
+ spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize);
+ } else {
+ spanCount = selectionSpec.spanCount;
+ }
+ mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount));
+
+ int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing);
+ mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false));
+ mRecyclerView.setAdapter(mAdapter);
+ mAlbumMediaCollection.onCreate(getActivity(), this);
+ mAlbumMediaCollection.load(album, selectionSpec.capture);
+ }
+
+ @Override
+ public void onDestroyView() {
+ super.onDestroyView();
+ mAlbumMediaCollection.onDestroy();
+ }
+
+ public void refreshMediaGrid() {
+ mAdapter.notifyDataSetChanged();
+ }
+
+ public void refreshSelection() {
+ mAdapter.refreshSelection();
+ }
+
+ @Override
+ public void onAlbumMediaLoad(Cursor cursor) {
+ mAdapter.swapCursor(cursor);
+ }
+
+ @Override
+ public void onAlbumMediaReset() {
+ mAdapter.swapCursor(null);
+ }
+
+ @Override
+ public void onUpdate() {
+ // notify outer Activity that check state changed
+ if (mCheckStateListener != null) {
+ mCheckStateListener.onUpdate();
+ }
+ }
+
+ @Override
+ public void onMediaClick(Album album, Item item, int adapterPosition) {
+ if (mOnMediaClickListener != null) {
+ mOnMediaClickListener.onMediaClick((Album) getArguments().getParcelable(EXTRA_ALBUM),
+ item, adapterPosition);
+ }
+ }
+
+ public interface SelectionProvider {
+ SelectedItemCollection provideSelectedItemCollection();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java
new file mode 100644
index 0000000..9827d84
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui;
+
+import android.content.ActivityNotFoundException;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+import androidx.fragment.app.Fragment;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.Toast;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;
+import com.zhihu.matisse.listener.OnFragmentInteractionListener;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+import it.sephiroth.android.library.imagezoom.ImageViewTouchBase;
+
+public class PreviewItemFragment extends Fragment {
+
+ private static final String ARGS_ITEM = "args_item";
+ private OnFragmentInteractionListener mListener;
+
+ public static PreviewItemFragment newInstance(Item item) {
+ PreviewItemFragment fragment = new PreviewItemFragment();
+ Bundle bundle = new Bundle();
+ bundle.putParcelable(ARGS_ITEM, item);
+ fragment.setArguments(bundle);
+ return fragment;
+ }
+
+ @Override
+ public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ return inflater.inflate(R.layout.fragment_preview_item, container, false);
+ }
+
+ @Override
+ public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
+ super.onViewCreated(view, savedInstanceState);
+ final Item item = getArguments().getParcelable(ARGS_ITEM);
+ if (item == null) {
+ return;
+ }
+
+ View videoPlayButton = view.findViewById(R.id.video_play_button);
+ if (item.isVideo()) {
+ videoPlayButton.setVisibility(View.VISIBLE);
+ videoPlayButton.setOnClickListener(v -> {
+ Intent intent = new Intent(Intent.ACTION_VIEW);
+ intent.setDataAndType(item.uri, "video/*");
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(getContext(), R.string.error_no_video_activity, Toast.LENGTH_SHORT).show();
+ }
+ });
+ } else {
+ videoPlayButton.setVisibility(View.GONE);
+ }
+
+ ImageViewTouch image = (ImageViewTouch) view.findViewById(R.id.image_view);
+ image.setDisplayType(ImageViewTouchBase.DisplayType.FIT_TO_SCREEN);
+
+ image.setSingleTapListener(() -> {
+ if (mListener != null) {
+ mListener.onClick();
+ }
+ });
+
+ Point size = PhotoMetadataUtils.getBitmapSize(item.getContentUri(), getActivity());
+ if (item.isGif()) {
+ SelectionSpec.getInstance().imageEngine.loadGifImage(getContext(), size.x, size.y, image,
+ item.getContentUri());
+ } else {
+ SelectionSpec.getInstance().imageEngine.loadImage(getContext(), size.x, size.y, image,
+ item.getContentUri());
+ }
+ }
+
+ public void resetView() {
+ if (getView() != null) {
+ ((ImageViewTouch) getView().findViewById(R.id.image_view)).resetMatrix();
+ }
+ }
+
+
+ @Override
+ public void onAttach(Context context) {
+ super.onAttach(context);
+ if (context instanceof OnFragmentInteractionListener) {
+ mListener = (OnFragmentInteractionListener) context;
+ } else {
+ throw new RuntimeException(context.toString()
+ + " must implement OnFragmentInteractionListener");
+ }
+ }
+
+ @Override
+ public void onDetach() {
+ super.onDetach();
+ mListener = null;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java
new file mode 100644
index 0000000..8b934c7
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java
@@ -0,0 +1,51 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui;
+
+import android.os.Bundle;
+import androidx.annotation.Nullable;
+
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.model.SelectedItemCollection;
+
+import java.util.List;
+
+public class SelectedPreviewActivity extends BasePreviewActivity {
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (!SelectionSpec.getInstance().hasInited) {
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+
+ Bundle bundle = getIntent().getBundleExtra(EXTRA_DEFAULT_BUNDLE);
+ List
- selected = bundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
+ mAdapter.addAll(selected);
+ mAdapter.notifyDataSetChanged();
+ if (mSpec.countable) {
+ mCheckView.setCheckedNum(1);
+ } else {
+ mCheckView.setChecked(true);
+ }
+ mPreviousPos = 0;
+ updateSize(selected.get(0));
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java
new file mode 100644
index 0000000..e90e371
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java
@@ -0,0 +1,297 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import androidx.recyclerview.widget.GridLayoutManager;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.entity.IncapableCause;
+import com.zhihu.matisse.internal.model.SelectedItemCollection;
+import com.zhihu.matisse.internal.ui.widget.CheckView;
+import com.zhihu.matisse.internal.ui.widget.MediaGrid;
+
+public class AlbumMediaAdapter extends
+ RecyclerViewCursorAdapter
implements
+ MediaGrid.OnMediaGridClickListener {
+
+ private static final int VIEW_TYPE_CAPTURE = 0x01;
+ private static final int VIEW_TYPE_MEDIA = 0x02;
+ private final SelectedItemCollection mSelectedCollection;
+ private final Drawable mPlaceholder;
+ private SelectionSpec mSelectionSpec;
+ private CheckStateListener mCheckStateListener;
+ private OnMediaClickListener mOnMediaClickListener;
+ private RecyclerView mRecyclerView;
+ private int mImageResize;
+
+ public AlbumMediaAdapter(Context context, SelectedItemCollection selectedCollection, RecyclerView recyclerView) {
+ super(null);
+ mSelectionSpec = SelectionSpec.getInstance();
+ mSelectedCollection = selectedCollection;
+
+ TypedArray ta = context.getTheme().obtainStyledAttributes(new int[]{R.attr.item_placeholder});
+ mPlaceholder = ta.getDrawable(0);
+ ta.recycle();
+
+ mRecyclerView = recyclerView;
+ }
+
+ @Override
+ public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
+ if (viewType == VIEW_TYPE_CAPTURE) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.photo_capture_item, parent, false);
+ CaptureViewHolder holder = new CaptureViewHolder(v);
+ holder.itemView.setOnClickListener(v1 -> {
+ if (v1.getContext() instanceof OnPhotoCapture) {
+ ((OnPhotoCapture) v1.getContext()).capture();
+ }
+ });
+ return holder;
+ } else if (viewType == VIEW_TYPE_MEDIA) {
+ View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.media_grid_item, parent, false);
+ return new MediaViewHolder(v);
+ }
+ return null;
+ }
+
+ @Override
+ protected void onBindViewHolder(final RecyclerView.ViewHolder holder, Cursor cursor) {
+ if (holder instanceof CaptureViewHolder) {
+ CaptureViewHolder captureViewHolder = (CaptureViewHolder) holder;
+ Drawable[] drawables = captureViewHolder.mHint.getCompoundDrawables();
+ TypedArray ta = holder.itemView.getContext().getTheme().obtainStyledAttributes(
+ new int[]{R.attr.capture_textColor});
+ int color = ta.getColor(0, 0);
+ ta.recycle();
+
+ for (int i = 0; i < drawables.length; i++) {
+ Drawable drawable = drawables[i];
+ if (drawable != null) {
+ final Drawable.ConstantState state = drawable.getConstantState();
+ if (state == null) {
+ continue;
+ }
+
+ Drawable newDrawable = state.newDrawable().mutate();
+ newDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ newDrawable.setBounds(drawable.getBounds());
+ drawables[i] = newDrawable;
+ }
+ }
+ captureViewHolder.mHint.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawables[3]);
+ } else if (holder instanceof MediaViewHolder) {
+ MediaViewHolder mediaViewHolder = (MediaViewHolder) holder;
+
+ final Item item = Item.valueOf(cursor);
+ mediaViewHolder.mMediaGrid.preBindMedia(new MediaGrid.PreBindInfo(
+ getImageResize(mediaViewHolder.mMediaGrid.getContext()),
+ mPlaceholder,
+ mSelectionSpec.countable,
+ holder
+ ));
+ mediaViewHolder.mMediaGrid.bindMedia(item);
+ mediaViewHolder.mMediaGrid.setOnMediaGridClickListener(this);
+ setCheckStatus(item, mediaViewHolder.mMediaGrid);
+ }
+ }
+
+ private void setCheckStatus(Item item, MediaGrid mediaGrid) {
+ if (mSelectionSpec.countable) {
+ int checkedNum = mSelectedCollection.checkedNumOf(item);
+ if (checkedNum > 0) {
+ mediaGrid.setCheckEnabled(true);
+ mediaGrid.setCheckedNum(checkedNum);
+ } else {
+ if (mSelectedCollection.maxSelectableReached()) {
+ mediaGrid.setCheckEnabled(false);
+ mediaGrid.setCheckedNum(CheckView.UNCHECKED);
+ } else {
+ mediaGrid.setCheckEnabled(true);
+ mediaGrid.setCheckedNum(checkedNum);
+ }
+ }
+ } else {
+ boolean selected = mSelectedCollection.isSelected(item);
+ if (selected) {
+ mediaGrid.setCheckEnabled(true);
+ mediaGrid.setChecked(true);
+ } else {
+ if (mSelectedCollection.maxSelectableReached()) {
+ mediaGrid.setCheckEnabled(false);
+ mediaGrid.setChecked(false);
+ } else {
+ mediaGrid.setCheckEnabled(true);
+ mediaGrid.setChecked(false);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder) {
+ if (mSelectionSpec.showPreview) {
+ if (mOnMediaClickListener != null) {
+ mOnMediaClickListener.onMediaClick(null, item, holder.getAdapterPosition());
+ }
+ } else {
+ updateSelectedItem(item, holder);
+ }
+ }
+
+ @Override
+ public void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder) {
+ updateSelectedItem(item, holder);
+ }
+
+ private void updateSelectedItem(Item item, RecyclerView.ViewHolder holder) {
+ if (mSelectionSpec.countable) {
+ int checkedNum = mSelectedCollection.checkedNumOf(item);
+ if (checkedNum == CheckView.UNCHECKED) {
+ if (assertAddSelection(holder.itemView.getContext(), item)) {
+ mSelectedCollection.add(item);
+ notifyCheckStateChanged();
+ }
+ } else {
+ mSelectedCollection.remove(item);
+ notifyCheckStateChanged();
+ }
+ } else {
+ if (mSelectedCollection.isSelected(item)) {
+ mSelectedCollection.remove(item);
+ notifyCheckStateChanged();
+ } else {
+ if (assertAddSelection(holder.itemView.getContext(), item)) {
+ mSelectedCollection.add(item);
+ notifyCheckStateChanged();
+ }
+ }
+ }
+ }
+
+ private void notifyCheckStateChanged() {
+ notifyDataSetChanged();
+ if (mCheckStateListener != null) {
+ mCheckStateListener.onUpdate();
+ }
+ }
+
+ @Override
+ public int getItemViewType(int position, Cursor cursor) {
+ return Item.valueOf(cursor).isCapture() ? VIEW_TYPE_CAPTURE : VIEW_TYPE_MEDIA;
+ }
+
+ private boolean assertAddSelection(Context context, Item item) {
+ IncapableCause cause = mSelectedCollection.isAcceptable(item);
+ IncapableCause.handleCause(context, cause);
+ return cause == null;
+ }
+
+
+ public void registerCheckStateListener(CheckStateListener listener) {
+ mCheckStateListener = listener;
+ }
+
+ public void unregisterCheckStateListener() {
+ mCheckStateListener = null;
+ }
+
+ public void registerOnMediaClickListener(OnMediaClickListener listener) {
+ mOnMediaClickListener = listener;
+ }
+
+ public void unregisterOnMediaClickListener() {
+ mOnMediaClickListener = null;
+ }
+
+ public void refreshSelection() {
+ GridLayoutManager layoutManager = (GridLayoutManager) mRecyclerView.getLayoutManager();
+ int first = layoutManager.findFirstVisibleItemPosition();
+ int last = layoutManager.findLastVisibleItemPosition();
+ if (first == -1 || last == -1) {
+ return;
+ }
+ Cursor cursor = getCursor();
+ for (int i = first; i <= last; i++) {
+ RecyclerView.ViewHolder holder = mRecyclerView.findViewHolderForAdapterPosition(first);
+ if (holder instanceof MediaViewHolder) {
+ if (cursor.moveToPosition(i)) {
+ setCheckStatus(Item.valueOf(cursor), ((MediaViewHolder) holder).mMediaGrid);
+ }
+ }
+ }
+ }
+
+ private int getImageResize(Context context) {
+ if (mImageResize == 0) {
+ RecyclerView.LayoutManager lm = mRecyclerView.getLayoutManager();
+ int spanCount = ((GridLayoutManager) lm).getSpanCount();
+ int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ int availableWidth = screenWidth - context.getResources().getDimensionPixelSize(
+ R.dimen.media_grid_spacing) * (spanCount - 1);
+ mImageResize = availableWidth / spanCount;
+ mImageResize = (int) (mImageResize * mSelectionSpec.thumbnailScale);
+ }
+ return mImageResize;
+ }
+
+ public interface CheckStateListener {
+ void onUpdate();
+ }
+
+ public interface OnMediaClickListener {
+ void onMediaClick(Album album, Item item, int adapterPosition);
+ }
+
+ public interface OnPhotoCapture {
+ void capture();
+ }
+
+ private static class MediaViewHolder extends RecyclerView.ViewHolder {
+
+ private MediaGrid mMediaGrid;
+
+ MediaViewHolder(View itemView) {
+ super(itemView);
+ mMediaGrid = (MediaGrid) itemView;
+ }
+ }
+
+ private static class CaptureViewHolder extends RecyclerView.ViewHolder {
+
+ private TextView mHint;
+
+ CaptureViewHolder(View itemView) {
+ super(itemView);
+
+ mHint = (TextView) itemView.findViewById(R.id.hint);
+ }
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumsAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumsAdapter.java
new file mode 100644
index 0000000..8e809c9
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumsAdapter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.adapter;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.CursorAdapter;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+
+public class AlbumsAdapter extends CursorAdapter {
+
+ private final Drawable mPlaceholder;
+
+ public AlbumsAdapter(Context context, Cursor c, boolean autoRequery) {
+ super(context, c, autoRequery);
+
+ TypedArray ta = context.getTheme().obtainStyledAttributes(
+ new int[]{R.attr.album_thumbnail_placeholder});
+ mPlaceholder = ta.getDrawable(0);
+ ta.recycle();
+ }
+
+ public AlbumsAdapter(Context context, Cursor c, int flags) {
+ super(context, c, flags);
+
+ TypedArray ta = context.getTheme().obtainStyledAttributes(
+ new int[]{R.attr.album_thumbnail_placeholder});
+ mPlaceholder = ta.getDrawable(0);
+ ta.recycle();
+ }
+
+ @Override
+ public View newView(Context context, Cursor cursor, ViewGroup parent) {
+ return LayoutInflater.from(context).inflate(R.layout.album_list_item, parent, false);
+ }
+
+ @Override
+ public void bindView(View view, Context context, Cursor cursor) {
+ Album album = Album.valueOf(cursor);
+ ((TextView) view.findViewById(R.id.album_name)).setText(album.getDisplayName(context));
+ ((TextView) view.findViewById(R.id.album_media_count)).setText(String.valueOf(album.getCount()));
+
+ // do not need to load animated Gif
+ SelectionSpec.getInstance().imageEngine.loadThumbnail(context, context.getResources().getDimensionPixelSize(R
+ .dimen.media_grid_size), mPlaceholder,
+ (ImageView) view.findViewById(R.id.album_cover), album.getCoverUri());
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java
new file mode 100644
index 0000000..b75abc1
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.adapter;
+
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentPagerAdapter;
+import android.view.ViewGroup;
+
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.ui.PreviewItemFragment;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PreviewPagerAdapter extends FragmentPagerAdapter {
+
+ private ArrayList- mItems = new ArrayList<>();
+ private OnPrimaryItemSetListener mListener;
+
+ public PreviewPagerAdapter(FragmentManager manager, OnPrimaryItemSetListener listener) {
+ super(manager);
+ mListener = listener;
+ }
+
+ @Override
+ public Fragment getItem(int position) {
+ return PreviewItemFragment.newInstance(mItems.get(position));
+ }
+
+ @Override
+ public int getCount() {
+ return mItems.size();
+ }
+
+ @Override
+ public void setPrimaryItem(ViewGroup container, int position, Object object) {
+ super.setPrimaryItem(container, position, object);
+ if (mListener != null) {
+ mListener.onPrimaryItemSet(position);
+ }
+ }
+
+ public Item getMediaItem(int position) {
+ return mItems.get(position);
+ }
+
+ public void addAll(List
- items) {
+ mItems.addAll(items);
+ }
+
+ interface OnPrimaryItemSetListener {
+
+ void onPrimaryItemSet(int position);
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java
new file mode 100644
index 0000000..48203b0
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.adapter;
+
+import android.database.Cursor;
+import android.provider.MediaStore;
+import androidx.recyclerview.widget.RecyclerView;
+
+public abstract class RecyclerViewCursorAdapter
extends
+ RecyclerView.Adapter {
+
+ private Cursor mCursor;
+ private int mRowIDColumn;
+
+ RecyclerViewCursorAdapter(Cursor c) {
+ setHasStableIds(true);
+ swapCursor(c);
+ }
+
+ protected abstract void onBindViewHolder(VH holder, Cursor cursor);
+
+ @Override
+ public void onBindViewHolder(VH holder, int position) {
+ if (!isDataValid(mCursor)) {
+ throw new IllegalStateException("Cannot bind view holder when cursor is in invalid state.");
+ }
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("Could not move cursor to position " + position
+ + " when trying to bind view holder");
+ }
+
+ onBindViewHolder(holder, mCursor);
+ }
+
+ @Override
+ public int getItemViewType(int position) {
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("Could not move cursor to position " + position
+ + " when trying to get item view type.");
+ }
+ return getItemViewType(position, mCursor);
+ }
+
+ protected abstract int getItemViewType(int position, Cursor cursor);
+
+ @Override
+ public int getItemCount() {
+ if (isDataValid(mCursor)) {
+ return mCursor.getCount();
+ } else {
+ return 0;
+ }
+ }
+
+ @Override
+ public long getItemId(int position) {
+ if (!isDataValid(mCursor)) {
+ throw new IllegalStateException("Cannot lookup item id when cursor is in invalid state.");
+ }
+ if (!mCursor.moveToPosition(position)) {
+ throw new IllegalStateException("Could not move cursor to position " + position
+ + " when trying to get an item id");
+ }
+
+ return mCursor.getLong(mRowIDColumn);
+ }
+
+ public void swapCursor(Cursor newCursor) {
+ if (newCursor == mCursor) {
+ return;
+ }
+
+ if (newCursor != null) {
+ mCursor = newCursor;
+ mRowIDColumn = mCursor.getColumnIndexOrThrow(MediaStore.Files.FileColumns._ID);
+ // notify the observers about the new cursor
+ notifyDataSetChanged();
+ } else {
+ notifyItemRangeRemoved(0, getItemCount());
+ mCursor = null;
+ mRowIDColumn = -1;
+ }
+ }
+
+ public Cursor getCursor() {
+ return mCursor;
+ }
+
+ private boolean isDataValid(Cursor cursor) {
+ return cursor != null && !cursor.isClosed();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java
new file mode 100644
index 0000000..c8c88af
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import androidx.annotation.NonNull;
+import androidx.appcompat.widget.ListPopupWindow;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.CursorAdapter;
+import android.widget.TextView;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.utils.Platform;
+
+public class AlbumsSpinner {
+
+ private static final int MAX_SHOWN_COUNT = 6;
+ private CursorAdapter mAdapter;
+ private TextView mSelected;
+ private ListPopupWindow mListPopupWindow;
+ private AdapterView.OnItemSelectedListener mOnItemSelectedListener;
+
+ public AlbumsSpinner(@NonNull Context context) {
+ mListPopupWindow = new ListPopupWindow(context, null, R.attr.listPopupWindowStyle);
+ mListPopupWindow.setModal(true);
+ float density = context.getResources().getDisplayMetrics().density;
+ mListPopupWindow.setContentWidth(ListPopupWindow.MATCH_PARENT);
+ mListPopupWindow.setHorizontalOffset((int) (0 * density));
+ mListPopupWindow.setVerticalOffset((int) (0 * density));
+ mListPopupWindow.setOnItemClickListener((parent, view, position, id) -> {
+ AlbumsSpinner.this.onItemSelected(parent.getContext(), position);
+ if (mOnItemSelectedListener != null) {
+ mOnItemSelectedListener.onItemSelected(parent, view, position, id);
+ }
+ });
+ }
+
+ public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener listener) {
+ mOnItemSelectedListener = listener;
+ }
+
+ public void setSelection(Context context, int position) {
+ mListPopupWindow.setSelection(position);
+ onItemSelected(context, position);
+ }
+
+ private void onItemSelected(Context context, int position) {
+ mListPopupWindow.dismiss();
+ Cursor cursor = mAdapter.getCursor();
+ cursor.moveToPosition(position);
+ Album album = Album.valueOf(cursor);
+ String displayName = album.getDisplayName(context);
+ if (mSelected.getVisibility() == View.VISIBLE) {
+ mSelected.setText(displayName);
+ } else {
+ if (Platform.hasICS()) {
+ mSelected.setAlpha(0.0f);
+ mSelected.setVisibility(View.VISIBLE);
+ mSelected.setText(displayName);
+ mSelected.animate().alpha(1.0f).setDuration(context.getResources().getInteger(
+ android.R.integer.config_longAnimTime)).start();
+ } else {
+ mSelected.setVisibility(View.VISIBLE);
+ mSelected.setText(displayName);
+ }
+
+ }
+ }
+
+ public void setAdapter(CursorAdapter adapter) {
+ mListPopupWindow.setAdapter(adapter);
+ mAdapter = adapter;
+ }
+
+ public void setSelectedTextView(TextView textView) {
+ mSelected = textView;
+ // tint dropdown arrow icon
+ Drawable[] drawables = mSelected.getCompoundDrawables();
+ Drawable right = drawables[2];
+ TypedArray ta = mSelected.getContext().getTheme().obtainStyledAttributes(
+ new int[]{R.attr.album_element_color});
+ int color = ta.getColor(0, 0);
+ ta.recycle();
+ right.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+
+ mSelected.setVisibility(View.GONE);
+ mSelected.setOnClickListener(v -> {
+ int itemHeight = v.getResources().getDimensionPixelSize(R.dimen.album_item_height);
+ mListPopupWindow.setHeight(
+ mAdapter.getCount() > MAX_SHOWN_COUNT ? itemHeight * MAX_SHOWN_COUNT
+ : itemHeight * mAdapter.getCount());
+ mListPopupWindow.show();
+ });
+ mSelected.setOnTouchListener(mListPopupWindow.createDragToOpenListener(mSelected));
+ }
+
+ public void setPopupAnchorView(View view) {
+ mListPopupWindow.setAnchorView(view);
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java
new file mode 100644
index 0000000..1d71b72
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java
@@ -0,0 +1,60 @@
+package com.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import androidx.core.content.res.ResourcesCompat;
+import androidx.appcompat.widget.AppCompatImageView;
+import android.util.AttributeSet;
+
+import com.zhihu.matisse.R;
+
+public class CheckRadioView extends AppCompatImageView {
+
+ private Drawable mDrawable;
+
+ private int mSelectedColor;
+ private int mUnSelectUdColor;
+
+ public CheckRadioView(Context context) {
+ super(context);
+ init();
+ }
+
+
+
+ public CheckRadioView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init();
+ }
+
+ private void init() {
+ mSelectedColor = ResourcesCompat.getColor(
+ getResources(), R.color.zhihu_item_checkCircle_backgroundColor,
+ getContext().getTheme());
+ mUnSelectUdColor = ResourcesCompat.getColor(
+ getResources(), R.color.zhihu_check_original_radio_disable,
+ getContext().getTheme());
+ setChecked(false);
+ }
+
+ public void setChecked(boolean enable) {
+ if (enable) {
+ setImageResource(R.drawable.ic_preview_radio_on);
+ mDrawable = getDrawable();
+ mDrawable.setColorFilter(mSelectedColor, PorterDuff.Mode.SRC_IN);
+ } else {
+ setImageResource(R.drawable.ic_preview_radio_off);
+ mDrawable = getDrawable();
+ mDrawable.setColorFilter(mUnSelectUdColor, PorterDuff.Mode.SRC_IN);
+ }
+ }
+
+
+ public void setColor(int color) {
+ if (mDrawable == null) {
+ mDrawable = getDrawable();
+ }
+ mDrawable.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java
new file mode 100644
index 0000000..7407325
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.RadialGradient;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import androidx.core.content.res.ResourcesCompat;
+import android.text.TextPaint;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.zhihu.matisse.R;
+
+public class CheckView extends View {
+
+ public static final int UNCHECKED = Integer.MIN_VALUE;
+ private static final float STROKE_WIDTH = 3.0f; // dp
+ private static final float SHADOW_WIDTH = 6.0f; // dp
+ private static final int SIZE = 48; // dp
+ private static final float STROKE_RADIUS = 11.5f; // dp
+ private static final float BG_RADIUS = 11.0f; // dp
+ private static final int CONTENT_SIZE = 16; // dp
+ private boolean mCountable;
+ private boolean mChecked;
+ private int mCheckedNum;
+ private Paint mStrokePaint;
+ private Paint mBackgroundPaint;
+ private TextPaint mTextPaint;
+ private Paint mShadowPaint;
+ private Drawable mCheckDrawable;
+ private float mDensity;
+ private Rect mCheckRect;
+ private boolean mEnabled = true;
+
+ public CheckView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public CheckView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public CheckView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ // fixed size 48dp x 48dp
+ int sizeSpec = MeasureSpec.makeMeasureSpec((int) (SIZE * mDensity), MeasureSpec.EXACTLY);
+ super.onMeasure(sizeSpec, sizeSpec);
+ }
+
+ private void init(Context context) {
+ mDensity = context.getResources().getDisplayMetrics().density;
+
+ mStrokePaint = new Paint();
+ mStrokePaint.setAntiAlias(true);
+ mStrokePaint.setStyle(Paint.Style.STROKE);
+ mStrokePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_OVER));
+ mStrokePaint.setStrokeWidth(STROKE_WIDTH * mDensity);
+ TypedArray ta = getContext().getTheme().obtainStyledAttributes(new int[]{R.attr.item_checkCircle_borderColor});
+ int defaultColor = ResourcesCompat.getColor(
+ getResources(), R.color.zhihu_item_checkCircle_borderColor,
+ getContext().getTheme());
+ int color = ta.getColor(0, defaultColor);
+ ta.recycle();
+ mStrokePaint.setColor(color);
+
+ mCheckDrawable = ResourcesCompat.getDrawable(context.getResources(),
+ R.drawable.ic_check_white_18dp, context.getTheme());
+ }
+
+ public void setChecked(boolean checked) {
+ if (mCountable) {
+ throw new IllegalStateException("CheckView is countable, call setCheckedNum() instead.");
+ }
+ mChecked = checked;
+ invalidate();
+ }
+
+ public void setCountable(boolean countable) {
+ mCountable = countable;
+ }
+
+ public void setCheckedNum(int checkedNum) {
+ if (!mCountable) {
+ throw new IllegalStateException("CheckView is not countable, call setChecked() instead.");
+ }
+ if (checkedNum != UNCHECKED && checkedNum <= 0) {
+ throw new IllegalArgumentException("checked num can't be negative.");
+ }
+ mCheckedNum = checkedNum;
+ invalidate();
+ }
+
+ public void setEnabled(boolean enabled) {
+ if (mEnabled != enabled) {
+ mEnabled = enabled;
+ invalidate();
+ }
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ super.onDraw(canvas);
+
+ // draw outer and inner shadow
+ initShadowPaint();
+ canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
+ (STROKE_RADIUS + STROKE_WIDTH / 2 + SHADOW_WIDTH) * mDensity, mShadowPaint);
+
+ // draw white stroke
+ canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
+ STROKE_RADIUS * mDensity, mStrokePaint);
+
+ // draw content
+ if (mCountable) {
+ if (mCheckedNum != UNCHECKED) {
+ initBackgroundPaint();
+ canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
+ BG_RADIUS * mDensity, mBackgroundPaint);
+ initTextPaint();
+ String text = String.valueOf(mCheckedNum);
+ int baseX = (int) (canvas.getWidth() - mTextPaint.measureText(text)) / 2;
+ int baseY = (int) (canvas.getHeight() - mTextPaint.descent() - mTextPaint.ascent()) / 2;
+ canvas.drawText(text, baseX, baseY, mTextPaint);
+ }
+ } else {
+ if (mChecked) {
+ initBackgroundPaint();
+ canvas.drawCircle((float) SIZE * mDensity / 2, (float) SIZE * mDensity / 2,
+ BG_RADIUS * mDensity, mBackgroundPaint);
+
+ mCheckDrawable.setBounds(getCheckRect());
+ mCheckDrawable.draw(canvas);
+ }
+ }
+
+ // enable hint
+ setAlpha(mEnabled ? 1.0f : 0.5f);
+ }
+
+ private void initShadowPaint() {
+ if (mShadowPaint == null) {
+ mShadowPaint = new Paint();
+ mShadowPaint.setAntiAlias(true);
+ // all in dp
+ float outerRadius = STROKE_RADIUS + STROKE_WIDTH / 2;
+ float innerRadius = outerRadius - STROKE_WIDTH;
+ float gradientRadius = outerRadius + SHADOW_WIDTH;
+ float stop0 = (innerRadius - SHADOW_WIDTH) / gradientRadius;
+ float stop1 = innerRadius / gradientRadius;
+ float stop2 = outerRadius / gradientRadius;
+ float stop3 = 1.0f;
+ mShadowPaint.setShader(
+ new RadialGradient((float) SIZE * mDensity / 2,
+ (float) SIZE * mDensity / 2,
+ gradientRadius * mDensity,
+ new int[]{Color.parseColor("#00000000"), Color.parseColor("#0D000000"),
+ Color.parseColor("#0D000000"), Color.parseColor("#00000000")},
+ new float[]{stop0, stop1, stop2, stop3},
+ Shader.TileMode.CLAMP));
+ }
+ }
+
+ private void initBackgroundPaint() {
+ if (mBackgroundPaint == null) {
+ mBackgroundPaint = new Paint();
+ mBackgroundPaint.setAntiAlias(true);
+ mBackgroundPaint.setStyle(Paint.Style.FILL);
+ TypedArray ta = getContext().getTheme()
+ .obtainStyledAttributes(new int[]{R.attr.item_checkCircle_backgroundColor});
+ int defaultColor = ResourcesCompat.getColor(
+ getResources(), R.color.zhihu_item_checkCircle_backgroundColor,
+ getContext().getTheme());
+ int color = ta.getColor(0, defaultColor);
+ ta.recycle();
+ mBackgroundPaint.setColor(color);
+ }
+ }
+
+ private void initTextPaint() {
+ if (mTextPaint == null) {
+ mTextPaint = new TextPaint();
+ mTextPaint.setAntiAlias(true);
+ mTextPaint.setColor(Color.WHITE);
+ mTextPaint.setTypeface(Typeface.create(Typeface.DEFAULT, Typeface.BOLD));
+ mTextPaint.setTextSize(12.0f * mDensity);
+ }
+ }
+
+ // rect for drawing checked number or mark
+ private Rect getCheckRect() {
+ if (mCheckRect == null) {
+ int rectPadding = (int) (SIZE * mDensity / 2 - CONTENT_SIZE * mDensity / 2);
+ mCheckRect = new Rect(rectPadding, rectPadding,
+ (int) (SIZE * mDensity - rectPadding), (int) (SIZE * mDensity - rectPadding));
+ }
+
+ return mCheckRect;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java
new file mode 100644
index 0000000..e640487
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.app.Dialog;
+import android.content.DialogInterface;
+import android.os.Bundle;
+import androidx.annotation.NonNull;
+import androidx.fragment.app.DialogFragment;
+import androidx.appcompat.app.AlertDialog;
+import android.text.TextUtils;
+
+import com.zhihu.matisse.R;
+
+public class IncapableDialog extends DialogFragment {
+
+ public static final String EXTRA_TITLE = "extra_title";
+ public static final String EXTRA_MESSAGE = "extra_message";
+
+ public static IncapableDialog newInstance(String title, String message) {
+ IncapableDialog dialog = new IncapableDialog();
+ Bundle args = new Bundle();
+ args.putString(EXTRA_TITLE, title);
+ args.putString(EXTRA_MESSAGE, message);
+ dialog.setArguments(args);
+ return dialog;
+ }
+
+ @NonNull
+ @Override
+ public Dialog onCreateDialog(Bundle savedInstanceState) {
+ String title = getArguments().getString(EXTRA_TITLE);
+ String message = getArguments().getString(EXTRA_MESSAGE);
+
+ AlertDialog.Builder builder = new AlertDialog.Builder(getActivity());
+ if (!TextUtils.isEmpty(title)) {
+ builder.setTitle(title);
+ }
+ if (!TextUtils.isEmpty(message)) {
+ builder.setMessage(message);
+ }
+ builder.setPositiveButton(R.string.button_ok, (dialog, which) -> dialog.dismiss());
+
+ return builder.create();
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java
new file mode 100644
index 0000000..0abb367
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java
@@ -0,0 +1,161 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import androidx.recyclerview.widget.RecyclerView;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.ImageView;
+import android.widget.TextView;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+
+public class MediaGrid extends SquareFrameLayout implements View.OnClickListener {
+
+ private ImageView mThumbnail;
+ private CheckView mCheckView;
+ private ImageView mGifTag;
+ private TextView mVideoDuration;
+
+ private Item mMedia;
+ private PreBindInfo mPreBindInfo;
+ private OnMediaGridClickListener mListener;
+
+ public MediaGrid(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public MediaGrid(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ private void init(Context context) {
+ LayoutInflater.from(context).inflate(R.layout.media_grid_content, this, true);
+
+ mThumbnail = (ImageView) findViewById(R.id.media_thumbnail);
+ mCheckView = (CheckView) findViewById(R.id.check_view);
+ mGifTag = (ImageView) findViewById(R.id.gif);
+ mVideoDuration = (TextView) findViewById(R.id.video_duration);
+
+ mThumbnail.setOnClickListener(this);
+ mCheckView.setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (mListener != null) {
+ if (v == mThumbnail) {
+ mListener.onThumbnailClicked(mThumbnail, mMedia, mPreBindInfo.mViewHolder);
+ } else if (v == mCheckView) {
+ mListener.onCheckViewClicked(mCheckView, mMedia, mPreBindInfo.mViewHolder);
+ }
+ }
+ }
+
+ public void preBindMedia(PreBindInfo info) {
+ mPreBindInfo = info;
+ }
+
+ public void bindMedia(Item item) {
+ mMedia = item;
+ setGifTag();
+ initCheckView();
+ setImage();
+ setVideoDuration();
+ }
+
+ public Item getMedia() {
+ return mMedia;
+ }
+
+ private void setGifTag() {
+ mGifTag.setVisibility(mMedia.isGif() ? View.VISIBLE : View.GONE);
+ }
+
+ private void initCheckView() {
+ mCheckView.setCountable(mPreBindInfo.mCheckViewCountable);
+ }
+
+ public void setCheckEnabled(boolean enabled) {
+ mCheckView.setEnabled(enabled);
+ }
+
+ public void setCheckedNum(int checkedNum) {
+ mCheckView.setCheckedNum(checkedNum);
+ }
+
+ public void setChecked(boolean checked) {
+ mCheckView.setChecked(checked);
+ }
+
+ private void setImage() {
+ if (mMedia.isGif()) {
+ SelectionSpec.getInstance().imageEngine.loadGifThumbnail(getContext(), mPreBindInfo.mResize,
+ mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri());
+ } else {
+ SelectionSpec.getInstance().imageEngine.loadThumbnail(getContext(), mPreBindInfo.mResize,
+ mPreBindInfo.mPlaceholder, mThumbnail, mMedia.getContentUri());
+ }
+ }
+
+ private void setVideoDuration() {
+ if (mMedia.isVideo()) {
+ mVideoDuration.setVisibility(VISIBLE);
+ mVideoDuration.setText(DateUtils.formatElapsedTime(mMedia.duration / 1000));
+ } else {
+ mVideoDuration.setVisibility(GONE);
+ }
+ }
+
+ public void setOnMediaGridClickListener(OnMediaGridClickListener listener) {
+ mListener = listener;
+ }
+
+ public void removeOnMediaGridClickListener() {
+ mListener = null;
+ }
+
+ public interface OnMediaGridClickListener {
+
+ void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder);
+
+ void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder);
+ }
+
+ public static class PreBindInfo {
+ int mResize;
+ Drawable mPlaceholder;
+ boolean mCheckViewCountable;
+ RecyclerView.ViewHolder mViewHolder;
+
+ public PreBindInfo(int resize, Drawable placeholder, boolean checkViewCountable,
+ RecyclerView.ViewHolder viewHolder) {
+ mResize = resize;
+ mPlaceholder = placeholder;
+ mCheckViewCountable = checkViewCountable;
+ mViewHolder = viewHolder;
+ }
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java
new file mode 100644
index 0000000..dca609c
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.graphics.Rect;
+import androidx.recyclerview.widget.RecyclerView;
+import android.view.View;
+
+public class MediaGridInset extends RecyclerView.ItemDecoration {
+
+ private int mSpanCount;
+ private int mSpacing;
+ private boolean mIncludeEdge;
+
+ public MediaGridInset(int spanCount, int spacing, boolean includeEdge) {
+ this.mSpanCount = spanCount;
+ this.mSpacing = spacing;
+ this.mIncludeEdge = includeEdge;
+ }
+
+ @Override
+ public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
+ RecyclerView.State state) {
+ int position = parent.getChildAdapterPosition(view); // item position
+ int column = position % mSpanCount; // item column
+
+ if (mIncludeEdge) {
+ // spacing - column * ((1f / spanCount) * spacing)
+ outRect.left = mSpacing - column * mSpacing / mSpanCount;
+ // (column + 1) * ((1f / spanCount) * spacing)
+ outRect.right = (column + 1) * mSpacing / mSpanCount;
+
+ if (position < mSpanCount) { // top edge
+ outRect.top = mSpacing;
+ }
+ outRect.bottom = mSpacing; // item bottom
+ } else {
+ // column * ((1f / spanCount) * spacing)
+ outRect.left = column * mSpacing / mSpanCount;
+ // spacing - (column + 1) * ((1f / spanCount) * spacing)
+ outRect.right = mSpacing - (column + 1) * mSpacing / mSpanCount;
+ if (position >= mSpanCount) {
+ outRect.top = mSpacing; // item top
+ }
+ }
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java
new file mode 100644
index 0000000..b3e66c3
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import androidx.viewpager.widget.ViewPager;
+import android.util.AttributeSet;
+import android.view.View;
+
+import it.sephiroth.android.library.imagezoom.ImageViewTouch;
+
+public class PreviewViewPager extends ViewPager {
+
+ public PreviewViewPager(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected boolean canScroll(View v, boolean checkV, int dx, int x, int y) {
+ if (v instanceof ImageViewTouch) {
+ return ((ImageViewTouch) v).canScroll(dx) || super.canScroll(v, checkV, dx, x, y);
+ }
+ return super.canScroll(v, checkV, dx, x, y);
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java
new file mode 100644
index 0000000..b1a5220
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Path;
+import android.graphics.RectF;
+import androidx.appcompat.widget.AppCompatImageView;
+import android.util.AttributeSet;
+
+public class RoundedRectangleImageView extends AppCompatImageView {
+
+ private float mRadius; // dp
+ private Path mRoundedRectPath;
+ private RectF mRectF;
+
+ public RoundedRectangleImageView(Context context) {
+ super(context);
+ init(context);
+ }
+
+ public RoundedRectangleImageView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ init(context);
+ }
+
+ public RoundedRectangleImageView(Context context, AttributeSet attrs, int defStyleAttr) {
+ super(context, attrs, defStyleAttr);
+ init(context);
+ }
+
+ private void init(Context context) {
+ float density = context.getResources().getDisplayMetrics().density;
+ mRadius = 2.0f * density;
+ mRoundedRectPath = new Path();
+ mRectF = new RectF();
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ mRectF.set(0.0f, 0.0f, getMeasuredWidth(), getMeasuredHeight());
+ mRoundedRectPath.addRoundRect(mRectF, mRadius, mRadius, Path.Direction.CW);
+ }
+
+ @Override
+ protected void onDraw(Canvas canvas) {
+ canvas.clipPath(mRoundedRectPath);
+ super.onDraw(canvas);
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareFrameLayout.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareFrameLayout.java
new file mode 100644
index 0000000..1f95518
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareFrameLayout.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.ui.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.FrameLayout;
+
+public class SquareFrameLayout extends FrameLayout {
+
+ public SquareFrameLayout(Context context) {
+ super(context);
+ }
+
+ public SquareFrameLayout(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec);
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/ExifInterfaceCompat.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/ExifInterfaceCompat.java
new file mode 100644
index 0000000..b2be0e4
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/ExifInterfaceCompat.java
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.utils;
+
+import android.media.ExifInterface;
+import android.text.TextUtils;
+import android.util.Log;
+
+import java.io.IOException;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.TimeZone;
+
+/**
+ * Bug fixture for ExifInterface constructor.
+ */
+final class ExifInterfaceCompat {
+ private static final String TAG = ExifInterfaceCompat.class.getSimpleName();
+ private static final int EXIF_DEGREE_FALLBACK_VALUE = -1;
+
+ /**
+ * Do not instantiate this class.
+ */
+ private ExifInterfaceCompat() {
+ }
+
+ /**
+ * Creates new instance of {@link ExifInterface}.
+ * Original constructor won't check filename value, so if null value has been passed,
+ * the process will be killed because of SIGSEGV.
+ * Google Play crash report system cannot perceive this crash, so this method will throw
+ * {@link NullPointerException} when the filename is null.
+ *
+ * @param filename a JPEG filename.
+ * @return {@link ExifInterface} instance.
+ * @throws IOException something wrong with I/O.
+ */
+ public static ExifInterface newInstance(String filename) throws IOException {
+ if (filename == null) throw new NullPointerException("filename should not be null");
+ return new ExifInterface(filename);
+ }
+
+ private static Date getExifDateTime(String filepath) {
+ ExifInterface exif;
+ try {
+ // ExifInterface does not check whether file path is null or not,
+ // so passing null file path argument to its constructor causing SIGSEGV.
+ // We should avoid such a situation by checking file path string.
+ exif = newInstance(filepath);
+ } catch (IOException ex) {
+ Log.e(TAG, "cannot read exif", ex);
+ return null;
+ }
+
+ String date = exif.getAttribute(ExifInterface.TAG_DATETIME);
+ if (TextUtils.isEmpty(date)) {
+ return null;
+ }
+ try {
+ SimpleDateFormat formatter = new SimpleDateFormat("yyyy:MM:dd HH:mm:ss");
+ formatter.setTimeZone(TimeZone.getTimeZone("UTC"));
+ return formatter.parse(date);
+ } catch (ParseException e) {
+ Log.d(TAG, "failed to parse date taken", e);
+ }
+ return null;
+ }
+
+ /**
+ * Read exif info and get datetime value of the photo.
+ *
+ * @param filepath to get datetime
+ * @return when a photo taken.
+ */
+ public static long getExifDateTimeInMillis(String filepath) {
+ Date datetime = getExifDateTime(filepath);
+ if (datetime == null) {
+ return -1;
+ }
+ return datetime.getTime();
+ }
+
+ /**
+ * Read exif info and get orientation value of the photo.
+ *
+ * @param filepath to get exif.
+ * @return exif orientation value
+ */
+ public static int getExifOrientation(String filepath) {
+ ExifInterface exif;
+ try {
+ // ExifInterface does not check whether file path is null or not,
+ // so passing null file path argument to its constructor causing SIGSEGV.
+ // We should avoid such a situation by checking file path string.
+ exif = newInstance(filepath);
+ } catch (IOException ex) {
+ Log.e(TAG, "cannot read exif", ex);
+ return EXIF_DEGREE_FALLBACK_VALUE;
+ }
+
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, EXIF_DEGREE_FALLBACK_VALUE);
+ if (orientation == EXIF_DEGREE_FALLBACK_VALUE) {
+ return 0;
+ }
+ // We only recognize a subset of orientation tag values.
+ switch (orientation) {
+ case ExifInterface.ORIENTATION_ROTATE_90:
+ return 90;
+ case ExifInterface.ORIENTATION_ROTATE_180:
+ return 180;
+ case ExifInterface.ORIENTATION_ROTATE_270:
+ return 270;
+ default:
+ return 0;
+ }
+ }
+}
\ No newline at end of file
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java
new file mode 100644
index 0000000..592c77e
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.utils;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.MediaStore;
+import androidx.fragment.app.Fragment;
+import androidx.core.content.FileProvider;
+import androidx.core.os.EnvironmentCompat;
+
+import com.zhihu.matisse.internal.entity.CaptureStrategy;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.ref.WeakReference;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+
+public class MediaStoreCompat {
+
+ private final WeakReference mContext;
+ private final WeakReference mFragment;
+ private CaptureStrategy mCaptureStrategy;
+ private Uri mCurrentPhotoUri;
+ private String mCurrentPhotoPath;
+
+ public MediaStoreCompat(Activity activity) {
+ mContext = new WeakReference<>(activity);
+ mFragment = null;
+ }
+
+ public MediaStoreCompat(Activity activity, Fragment fragment) {
+ mContext = new WeakReference<>(activity);
+ mFragment = new WeakReference<>(fragment);
+ }
+
+ /**
+ * Checks whether the device has a camera feature or not.
+ *
+ * @param context a context to check for camera feature.
+ * @return true if the device has a camera feature. false otherwise.
+ */
+ public static boolean hasCameraFeature(Context context) {
+ PackageManager pm = context.getApplicationContext().getPackageManager();
+ return pm.hasSystemFeature(PackageManager.FEATURE_CAMERA);
+ }
+
+ public void setCaptureStrategy(CaptureStrategy strategy) {
+ mCaptureStrategy = strategy;
+ }
+
+ public void dispatchCaptureIntent(Context context, int requestCode) {
+ Intent captureIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
+ if (captureIntent.resolveActivity(context.getPackageManager()) != null) {
+ File photoFile = null;
+ try {
+ photoFile = createImageFile();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+
+ if (photoFile != null) {
+ mCurrentPhotoPath = photoFile.getAbsolutePath();
+ mCurrentPhotoUri = FileProvider.getUriForFile(mContext.get(),
+ mCaptureStrategy.authority, photoFile);
+ captureIntent.putExtra(MediaStore.EXTRA_OUTPUT, mCurrentPhotoUri);
+ captureIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ List resInfoList = context.getPackageManager()
+ .queryIntentActivities(captureIntent, PackageManager.MATCH_DEFAULT_ONLY);
+ for (ResolveInfo resolveInfo : resInfoList) {
+ String packageName = resolveInfo.activityInfo.packageName;
+ context.grantUriPermission(packageName, mCurrentPhotoUri,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
+ if (mFragment != null) {
+ mFragment.get().startActivityForResult(captureIntent, requestCode);
+ } else {
+ mContext.get().startActivityForResult(captureIntent, requestCode);
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("ResultOfMethodCallIgnored")
+ private File createImageFile() throws IOException {
+ // Create an image file name
+ String timeStamp =
+ new SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(new Date());
+ String imageFileName = String.format("JPEG_%s.jpg", timeStamp);
+ File storageDir;
+ if (mCaptureStrategy.isPublic) {
+ storageDir = Environment.getExternalStoragePublicDirectory(
+ Environment.DIRECTORY_PICTURES);
+ if (!storageDir.exists()) storageDir.mkdirs();
+ } else {
+ storageDir = mContext.get().getExternalFilesDir(Environment.DIRECTORY_PICTURES);
+ }
+ if (mCaptureStrategy.directory != null) {
+ storageDir = new File(storageDir, mCaptureStrategy.directory);
+ if (!storageDir.exists()) storageDir.mkdirs();
+ }
+
+ // Avoid joining path components manually
+ File tempFile = new File(storageDir, imageFileName);
+
+ // Handle the situation that user's external storage is not ready
+ if (!Environment.MEDIA_MOUNTED.equals(EnvironmentCompat.getStorageState(tempFile))) {
+ return null;
+ }
+
+ return tempFile;
+ }
+
+ public Uri getCurrentPhotoUri() {
+ return mCurrentPhotoUri;
+ }
+
+ public String getCurrentPhotoPath() {
+ return mCurrentPhotoPath;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/PathUtils.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/PathUtils.java
new file mode 100644
index 0000000..de64322
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/PathUtils.java
@@ -0,0 +1,135 @@
+package com.zhihu.matisse.internal.utils;
+
+import android.annotation.TargetApi;
+import android.content.ContentUris;
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Environment;
+import android.provider.DocumentsContract;
+import android.provider.MediaStore;
+
+/**
+ * http://stackoverflow.com/a/27271131/4739220
+ */
+
+public class PathUtils {
+ /**
+ * 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.
+ *
+ * @param context The context.
+ * @param uri The Uri to query.
+ * @author paulburke
+ */
+ @TargetApi(Build.VERSION_CODES.KITKAT)
+ public static String getPath(final Context context, final Uri uri) {
+ // DocumentProvider
+ if (Platform.hasKitKat() && DocumentsContract.isDocumentUri(context, uri)) {
+ // ExternalStorageProvider
+ if (isExternalStorageDocument(uri)) {
+ final String docId = DocumentsContract.getDocumentId(uri);
+ final String[] split = docId.split(":");
+ final String type = split[0];
+
+ if ("primary".equalsIgnoreCase(type)) {
+ return Environment.getExternalStorageDirectory() + "/" + split[1];
+ }
+
+ // TODO handle non-primary volumes
+ } else if (isDownloadsDocument(uri)) { // DownloadsProvider
+
+ 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);
+ } else if (isMediaDocument(uri)) { // MediaProvider
+ 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);
+ }
+ } else if ("content".equalsIgnoreCase(uri.getScheme())) { // MediaStore (and general)
+ return getDataColumn(context, uri, null, null);
+ } else if ("file".equalsIgnoreCase(uri.getScheme())) { // File
+ return uri.getPath();
+ }
+
+ return null;
+ }
+
+ /**
+ * 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.
+ */
+ 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 columnIndex = cursor.getColumnIndexOrThrow(column);
+ return cursor.getString(columnIndex);
+ }
+ } finally {
+ if (cursor != null)
+ cursor.close();
+ }
+ return null;
+ }
+
+
+ /**
+ * @param uri The Uri to check.
+ * @return Whether the Uri authority is ExternalStorageProvider.
+ */
+ 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.
+ */
+ 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.
+ */
+ public static boolean isMediaDocument(Uri uri) {
+ return "com.android.providers.media.documents".equals(uri.getAuthority());
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/PhotoMetadataUtils.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/PhotoMetadataUtils.java
new file mode 100644
index 0000000..59c1c8c
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/PhotoMetadataUtils.java
@@ -0,0 +1,178 @@
+/*
+ * Copyright (C) 2014 nohana, Inc.
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.utils;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.database.Cursor;
+import android.graphics.BitmapFactory;
+import android.graphics.Point;
+import android.media.ExifInterface;
+import android.net.Uri;
+import android.provider.MediaStore;
+import android.util.DisplayMetrics;
+import android.util.Log;
+
+import com.zhihu.matisse.MimeType;
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.filter.Filter;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.entity.IncapableCause;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+public final class PhotoMetadataUtils {
+ private static final String TAG = PhotoMetadataUtils.class.getSimpleName();
+ private static final int MAX_WIDTH = 1600;
+ private static final String SCHEME_CONTENT = "content";
+
+ private PhotoMetadataUtils() {
+ throw new AssertionError("oops! the utility class is about to be instantiated...");
+ }
+
+ public static int getPixelsCount(ContentResolver resolver, Uri uri) {
+ Point size = getBitmapBound(resolver, uri);
+ return size.x * size.y;
+ }
+
+ public static Point getBitmapSize(Uri uri, Activity activity) {
+ ContentResolver resolver = activity.getContentResolver();
+ Point imageSize = getBitmapBound(resolver, uri);
+ int w = imageSize.x;
+ int h = imageSize.y;
+ if (PhotoMetadataUtils.shouldRotate(resolver, uri)) {
+ w = imageSize.y;
+ h = imageSize.x;
+ }
+ if (h == 0) return new Point(MAX_WIDTH, MAX_WIDTH);
+ DisplayMetrics metrics = new DisplayMetrics();
+ activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
+ float screenWidth = (float) metrics.widthPixels;
+ float screenHeight = (float) metrics.heightPixels;
+ float widthScale = screenWidth / w;
+ float heightScale = screenHeight / h;
+ if (widthScale > heightScale) {
+ return new Point((int) (w * widthScale), (int) (h * heightScale));
+ }
+ return new Point((int) (w * widthScale), (int) (h * heightScale));
+ }
+
+ public static Point getBitmapBound(ContentResolver resolver, Uri uri) {
+ InputStream is = null;
+ try {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ is = resolver.openInputStream(uri);
+ BitmapFactory.decodeStream(is, null, options);
+ int width = options.outWidth;
+ int height = options.outHeight;
+ return new Point(width, height);
+ } catch (FileNotFoundException e) {
+ return new Point(0, 0);
+ } finally {
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+
+ public static String getPath(ContentResolver resolver, Uri uri) {
+ if (uri == null) {
+ return null;
+ }
+
+ if (SCHEME_CONTENT.equals(uri.getScheme())) {
+ Cursor cursor = null;
+ try {
+ cursor = resolver.query(uri, new String[]{MediaStore.Images.ImageColumns.DATA},
+ null, null, null);
+ if (cursor == null || !cursor.moveToFirst()) {
+ return null;
+ }
+ return cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA));
+ } finally {
+ if (cursor != null) {
+ cursor.close();
+ }
+ }
+ }
+ return uri.getPath();
+ }
+
+ public static IncapableCause isAcceptable(Context context, Item item) {
+ if (!isSelectableType(context, item)) {
+ return new IncapableCause(context.getString(R.string.error_file_type));
+ }
+
+ if (SelectionSpec.getInstance().filters != null) {
+ for (Filter filter : SelectionSpec.getInstance().filters) {
+ IncapableCause incapableCause = filter.filter(context, item);
+ if (incapableCause != null) {
+ return incapableCause;
+ }
+ }
+ }
+ return null;
+ }
+
+ private static boolean isSelectableType(Context context, Item item) {
+ if (context == null) {
+ return false;
+ }
+
+ ContentResolver resolver = context.getContentResolver();
+ for (MimeType type : SelectionSpec.getInstance().mimeTypeSet) {
+ if (type.checkType(resolver, item.getContentUri())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private static boolean shouldRotate(ContentResolver resolver, Uri uri) {
+ ExifInterface exif;
+ try {
+ exif = ExifInterfaceCompat.newInstance(getPath(resolver, uri));
+ } catch (IOException e) {
+ Log.e(TAG, "could not read exif info of the image: " + uri);
+ return false;
+ }
+ int orientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, -1);
+ return orientation == ExifInterface.ORIENTATION_ROTATE_90
+ || orientation == ExifInterface.ORIENTATION_ROTATE_270;
+ }
+
+ public static float getSizeInMB(long sizeInBytes) {
+ DecimalFormat df = (DecimalFormat) NumberFormat.getNumberInstance(Locale.US);
+ df.applyPattern("0.0");
+ String result = df.format((float) sizeInBytes / 1024 / 1024);
+ Log.e(TAG, "getSizeInMB: " + result);
+ result = result.replaceAll(",", "."); // in some case , 0.0 will be 0,0
+ return Float.valueOf(result);
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/Platform.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/Platform.java
new file mode 100644
index 0000000..df41989
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/Platform.java
@@ -0,0 +1,16 @@
+package com.zhihu.matisse.internal.utils;
+
+import android.os.Build;
+
+/**
+ * @author JoongWon Baik
+ */
+public class Platform {
+ public static boolean hasICS() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH;
+ }
+
+ public static boolean hasKitKat() {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/SingleMediaScanner.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/SingleMediaScanner.java
new file mode 100644
index 0000000..81370df
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/SingleMediaScanner.java
@@ -0,0 +1,44 @@
+package com.zhihu.matisse.internal.utils;
+
+import android.content.Context;
+import android.media.MediaScannerConnection;
+import android.net.Uri;
+
+/**
+ * @author 工藤
+ * @email gougou@16fan.com
+ * create at 2018年10月23日12:17:59
+ * description:媒体扫描
+ */
+public class SingleMediaScanner implements MediaScannerConnection.MediaScannerConnectionClient {
+
+ private MediaScannerConnection mMsc;
+ private String mPath;
+ private ScanListener mListener;
+
+ public interface ScanListener {
+
+ /**
+ * scan finish
+ */
+ void onScanFinish();
+ }
+
+ public SingleMediaScanner(Context context, String mPath, ScanListener mListener) {
+ this.mPath = mPath;
+ this.mListener = mListener;
+ this.mMsc = new MediaScannerConnection(context, this);
+ this.mMsc.connect();
+ }
+
+ @Override public void onMediaScannerConnected() {
+ mMsc.scanFile(mPath, null);
+ }
+
+ @Override public void onScanCompleted(String mPath, Uri mUri) {
+ mMsc.disconnect();
+ if (mListener != null) {
+ mListener.onScanFinish();
+ }
+ }
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java
new file mode 100644
index 0000000..129b599
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.internal.utils;
+
+import android.content.Context;
+
+public class UIUtils {
+
+ public static int spanCount(Context context, int gridExpectedSize) {
+ int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
+ float expected = (float) screenWidth / (float) gridExpectedSize;
+ int spanCount = Math.round(expected);
+ if (spanCount == 0) {
+ spanCount = 1;
+ }
+ return spanCount;
+ }
+
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/listener/OnCheckedListener.java b/matisse/src/main/java/com/zhihu/matisse/listener/OnCheckedListener.java
new file mode 100644
index 0000000..4a0ebae
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/listener/OnCheckedListener.java
@@ -0,0 +1,9 @@
+package com.zhihu.matisse.listener;
+
+
+/**
+ * when original is enabled , callback immediately when user check or uncheck original.
+ */
+public interface OnCheckedListener {
+ void onCheck(boolean isChecked);
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/listener/OnFragmentInteractionListener.java b/matisse/src/main/java/com/zhihu/matisse/listener/OnFragmentInteractionListener.java
new file mode 100644
index 0000000..79f5536
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/listener/OnFragmentInteractionListener.java
@@ -0,0 +1,11 @@
+package com.zhihu.matisse.listener;
+
+/**
+ * PreViewItemFragment 和 BasePreViewActivity 通信的接口 ,为了方便拿到 ImageViewTouch 的点击事件
+ */
+public interface OnFragmentInteractionListener {
+ /**
+ * ImageViewTouch 被点击了
+ */
+ void onClick();
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java b/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java
new file mode 100644
index 0000000..b7d6d22
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java
@@ -0,0 +1,30 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.listener;
+
+import android.net.Uri;
+import androidx.annotation.NonNull;
+
+import java.util.List;
+
+public interface OnSelectedListener {
+ /**
+ * @param uriList the selected item {@link Uri} list.
+ * @param pathList the selected item file path list.
+ */
+ void onSelected(@NonNull List uriList, @NonNull List pathList);
+}
diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java b/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java
new file mode 100644
index 0000000..477df85
--- /dev/null
+++ b/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java
@@ -0,0 +1,456 @@
+/*
+ * Copyright 2017 Zhihu Inc.
+ *
+ * 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.zhihu.matisse.ui;
+
+import android.app.Activity;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.Looper;
+import android.util.Log;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.AdapterView;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.annotation.Nullable;
+import androidx.annotation.RequiresApi;
+import androidx.appcompat.app.ActionBar;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.appcompat.widget.Toolbar;
+import androidx.core.content.ContextCompat;
+import androidx.fragment.app.Fragment;
+
+import com.zhihu.matisse.R;
+import com.zhihu.matisse.internal.entity.Album;
+import com.zhihu.matisse.internal.entity.Item;
+import com.zhihu.matisse.internal.entity.SelectionSpec;
+import com.zhihu.matisse.internal.model.AlbumCollection;
+import com.zhihu.matisse.internal.model.SelectedItemCollection;
+import com.zhihu.matisse.internal.ui.AlbumPreviewActivity;
+import com.zhihu.matisse.internal.ui.BasePreviewActivity;
+import com.zhihu.matisse.internal.ui.MediaSelectionFragment;
+import com.zhihu.matisse.internal.ui.SelectedPreviewActivity;
+import com.zhihu.matisse.internal.ui.adapter.AlbumMediaAdapter;
+import com.zhihu.matisse.internal.ui.adapter.AlbumsAdapter;
+import com.zhihu.matisse.internal.ui.widget.AlbumsSpinner;
+import com.zhihu.matisse.internal.ui.widget.CheckRadioView;
+import com.zhihu.matisse.internal.ui.widget.IncapableDialog;
+import com.zhihu.matisse.internal.utils.MediaStoreCompat;
+import com.zhihu.matisse.internal.utils.PathUtils;
+import com.zhihu.matisse.internal.utils.PhotoMetadataUtils;
+import com.zhihu.matisse.internal.utils.SingleMediaScanner;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+
+/**
+ * Main Activity to display albums and media content (images/videos) in each album
+ * and also support media selecting operations.
+ */
+public class MatisseActivity extends AppCompatActivity implements
+ AlbumCollection.AlbumCallbacks, AdapterView.OnItemSelectedListener,
+ MediaSelectionFragment.SelectionProvider, View.OnClickListener,
+ AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener,
+ AlbumMediaAdapter.OnPhotoCapture {
+
+ public static final String EXTRA_RESULT_SELECTION = "extra_result_selection";
+ public static final String EXTRA_RESULT_SELECTION_PATH = "extra_result_selection_path";
+ public static final String EXTRA_RESULT_SELECTION_TYPE = "extra_result_selection_type";
+
+ public static final String EXTRA_RESULT_ORIGINAL_ENABLE = "extra_result_original_enable";
+ private static final int REQUEST_CODE_PREVIEW = 23;
+ private static final int REQUEST_CODE_CAPTURE = 24;
+ public static final String CHECK_STATE = "checkState";
+ private final AlbumCollection mAlbumCollection = new AlbumCollection();
+ private MediaStoreCompat mMediaStoreCompat;
+ private SelectedItemCollection mSelectedCollection = new SelectedItemCollection(this);
+ private SelectionSpec mSpec;
+
+ private AlbumsSpinner mAlbumsSpinner;
+ private AlbumsAdapter mAlbumsAdapter;
+ private TextView mButtonPreview;
+ private TextView mButtonApply;
+ private View mContainer;
+ private View mEmptyView;
+
+ private LinearLayout mOriginalLayout;
+ private CheckRadioView mOriginal;
+ private boolean mOriginalEnable;
+
+ private WeakReference fragmentRef;
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ // programmatically set theme before super.onCreate()
+// getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE);//实现状态栏文字颜色为暗色
+ getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR);
+ getWindow().setStatusBarColor(getResources().getColor(R.color.dracula_capture));
+ mSpec = SelectionSpec.getInstance();
+ setTheme(mSpec.themeId);
+ super.onCreate(savedInstanceState);
+ if (!mSpec.hasInited) {
+ setResult(RESULT_CANCELED);
+ finish();
+ return;
+ }
+ setContentView(R.layout.activity_matisse);
+
+ if (mSpec.needOrientationRestriction()) {
+ setRequestedOrientation(mSpec.orientation);
+ }
+
+ if (mSpec.capture) {
+ mMediaStoreCompat = new MediaStoreCompat(this);
+ if (mSpec.captureStrategy == null)
+ throw new RuntimeException("Don't forget to set CaptureStrategy.");
+ mMediaStoreCompat.setCaptureStrategy(mSpec.captureStrategy);
+ }
+
+ Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
+ setSupportActionBar(toolbar);
+ ActionBar actionBar = getSupportActionBar();
+ actionBar.setDisplayShowTitleEnabled(false);
+ actionBar.setDisplayHomeAsUpEnabled(true);
+ Drawable navigationIcon = toolbar.getNavigationIcon();
+ TypedArray ta = getTheme().obtainStyledAttributes(new int[]{R.attr.album_element_color});
+ int color = ta.getColor(0, 0);
+ ta.recycle();
+ navigationIcon.setColorFilter(color, PorterDuff.Mode.SRC_IN);
+
+ mButtonPreview = (TextView) findViewById(R.id.button_preview);
+ mButtonApply = (TextView) findViewById(R.id.button_apply);
+ mButtonPreview.setOnClickListener(this);
+ mButtonApply.setOnClickListener(this);
+ mContainer = findViewById(R.id.container);
+ mEmptyView = findViewById(R.id.empty_view);
+ mOriginalLayout = findViewById(R.id.originalLayout);
+ mOriginal = findViewById(R.id.original);
+ mOriginalLayout.setOnClickListener(this);
+
+ mSelectedCollection.onCreate(savedInstanceState);
+ if (savedInstanceState != null) {
+ mOriginalEnable = savedInstanceState.getBoolean(CHECK_STATE);
+ }
+ updateBottomToolbar();
+
+ mAlbumsAdapter = new AlbumsAdapter(this, null, false);
+ mAlbumsSpinner = new AlbumsSpinner(this);
+ mAlbumsSpinner.setOnItemSelectedListener(this);
+ mAlbumsSpinner.setSelectedTextView((TextView) findViewById(R.id.selected_album));
+ mAlbumsSpinner.setPopupAnchorView(findViewById(R.id.toolbar));
+ mAlbumsSpinner.setAdapter(mAlbumsAdapter);
+ mAlbumCollection.onCreate(this, this);
+ mAlbumCollection.onRestoreInstanceState(savedInstanceState);
+ mAlbumCollection.loadAlbums();
+ }
+
+ @Override
+ protected void onSaveInstanceState(Bundle outState) {
+ super.onSaveInstanceState(outState);
+ mSelectedCollection.onSaveInstanceState(outState);
+ mAlbumCollection.onSaveInstanceState(outState);
+ outState.putBoolean("checkState", mOriginalEnable);
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ mAlbumCollection.onDestroy();
+ mSpec.onCheckedListener = null;
+ mSpec.onSelectedListener = null;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ if (item.getItemId() == android.R.id.home) {
+ onBackPressed();
+ return true;
+ }
+ return super.onOptionsItemSelected(item);
+ }
+
+ @Override
+ public void onBackPressed() {
+ setResult(Activity.RESULT_CANCELED);
+ super.onBackPressed();
+ }
+
+ @Override
+ protected void onActivityResult(int requestCode, int resultCode, Intent data) {
+ super.onActivityResult(requestCode, resultCode, data);
+ if (resultCode != RESULT_OK)
+ return;
+
+ if (requestCode == REQUEST_CODE_PREVIEW) {
+ Log.i("尴尬","尴尬1:");
+ Bundle resultBundle = data.getBundleExtra(BasePreviewActivity.EXTRA_RESULT_BUNDLE);
+ ArrayList- selected = resultBundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
+ mOriginalEnable = data.getBooleanExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, false);
+ int collectionType = resultBundle.getInt(SelectedItemCollection.STATE_COLLECTION_TYPE,
+ SelectedItemCollection.COLLECTION_UNDEFINED);
+ if (data.getBooleanExtra(BasePreviewActivity.EXTRA_RESULT_APPLY, false)) {
+ Intent result = new Intent();
+ ArrayList
selectedUris = new ArrayList<>();
+ ArrayList selectedPaths = new ArrayList<>();
+ boolean isVideos = false;
+
+ if (selected != null) {
+ for (Item item : selected) {
+ selectedUris.add(item.getContentUri());
+ selectedPaths.add(PathUtils.getPath(this, item.getContentUri()));
+ isVideos = item.isVideo();
+ }
+ }
+ result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris);
+ result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths);
+ result.putExtra(EXTRA_RESULT_SELECTION_TYPE, isVideos);
+ result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
+ setResult(RESULT_OK, result);
+ finish();
+ } else {
+ mSelectedCollection.overwrite(selected, collectionType);
+ Fragment mediaSelectionFragment = getSupportFragmentManager().findFragmentByTag(
+ MediaSelectionFragment.class.getSimpleName());
+ if (mediaSelectionFragment instanceof MediaSelectionFragment) {
+ ((MediaSelectionFragment) mediaSelectionFragment).refreshMediaGrid();
+ }
+ updateBottomToolbar();
+ }
+ } else if (requestCode == REQUEST_CODE_CAPTURE) {
+ Log.i("尴尬","尴尬2:");
+
+ // Just pass the data back to previous calling Activity.
+ Uri contentUri = mMediaStoreCompat.getCurrentPhotoUri();
+ String path = mMediaStoreCompat.getCurrentPhotoPath();
+ ArrayList selected = new ArrayList<>();
+ selected.add(contentUri);
+ ArrayList selectedPath = new ArrayList<>();
+ selectedPath.add(path);
+ Intent result = new Intent();
+ result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selected);
+ result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPath);
+ setResult(RESULT_OK, result);
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
+ MatisseActivity.this.revokeUriPermission(contentUri,
+ Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ new SingleMediaScanner(this.getApplicationContext(), path, () -> Log.i("SingleMediaScanner", "scan finish!"));
+ finish();
+ }
+ }
+
+ private void updateBottomToolbar() {
+
+ int selectedCount = mSelectedCollection.count();
+ if (selectedCount == 0) {
+ mButtonPreview.setEnabled(false);
+ mButtonApply.setEnabled(false);
+ mButtonApply.setText(getString(R.string.button_apply_default));
+ } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) {
+ mButtonPreview.setEnabled(true);
+ mButtonApply.setText(R.string.button_apply_default);
+ mButtonApply.setEnabled(true);
+ } else {
+ mButtonPreview.setEnabled(true);
+ mButtonApply.setEnabled(true);
+ mButtonApply.setText(getString(R.string.button_apply, selectedCount));
+ }
+
+
+ if (mSpec.originalable) {
+ mOriginalLayout.setVisibility(View.VISIBLE);
+ updateOriginalState();
+ } else {
+ mOriginalLayout.setVisibility(View.INVISIBLE);
+ }
+
+
+ }
+
+
+ private void updateOriginalState() {
+ mOriginal.setChecked(mOriginalEnable);
+ if (countOverMaxSize() > 0) {
+
+ if (mOriginalEnable) {
+ IncapableDialog incapableDialog = IncapableDialog.newInstance("",
+ getString(R.string.error_over_original_size, mSpec.originalMaxSize));
+ incapableDialog.show(getSupportFragmentManager(),
+ IncapableDialog.class.getName());
+
+ mOriginal.setChecked(false);
+ mOriginalEnable = false;
+ }
+ }
+ }
+
+
+ private int countOverMaxSize() {
+ int count = 0;
+ int selectedCount = mSelectedCollection.count();
+ for (int i = 0; i < selectedCount; i++) {
+ Item item = mSelectedCollection.asList().get(i);
+
+ if (item.isImage()) {
+ float size = PhotoMetadataUtils.getSizeInMB(item.size);
+ if (size > mSpec.originalMaxSize) {
+ count++;
+ }
+ }
+ }
+ return count;
+ }
+
+ @Override
+ public void onClick(View v) {
+ if (v.getId() == R.id.button_preview) {
+ Intent intent = new Intent(this, SelectedPreviewActivity.class);
+ intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle());
+ intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
+ startActivityForResult(intent, REQUEST_CODE_PREVIEW);
+ } else if (v.getId() == R.id.button_apply) {
+ Log.i("尴尬","尴尬3:");
+ Intent result = new Intent();
+ ArrayList selectedUris = (ArrayList) mSelectedCollection.asListOfUri();
+ result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris);
+ ArrayList selectedPaths = (ArrayList) mSelectedCollection.asListOfString();
+ result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths);
+ result.putExtra(EXTRA_RESULT_SELECTION_TYPE,mSelectedCollection.asListOfType());
+
+ result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
+ setResult(RESULT_OK, result);
+ finish();
+ } else if (v.getId() == R.id.originalLayout) {
+ int count = countOverMaxSize();
+ if (count > 0) {
+ IncapableDialog incapableDialog = IncapableDialog.newInstance("",
+ getString(R.string.error_over_original_count, count, mSpec.originalMaxSize));
+ incapableDialog.show(getSupportFragmentManager(),
+ IncapableDialog.class.getName());
+ return;
+ }
+
+ mOriginalEnable = !mOriginalEnable;
+ mOriginal.setChecked(mOriginalEnable);
+
+ if (mSpec.onCheckedListener != null) {
+ mSpec.onCheckedListener.onCheck(mOriginalEnable);
+ }
+ }
+ }
+
+ @Override
+ public void onItemSelected(AdapterView> parent, View view, int position, long id) {
+ mAlbumCollection.setStateCurrentSelection(position);
+ mAlbumsAdapter.getCursor().moveToPosition(position);
+ Album album = Album.valueOf(mAlbumsAdapter.getCursor());
+ if (album.isAll() && SelectionSpec.getInstance().capture) {
+ album.addCaptureCount();
+ }
+ onAlbumSelected(album);
+ }
+
+ @Override
+ public void onNothingSelected(AdapterView> parent) {
+
+ }
+
+ @Override
+ public void onAlbumLoad(final Cursor cursor) {
+ mAlbumsAdapter.swapCursor(cursor);
+ // select default album.
+ Handler handler = new Handler(Looper.getMainLooper());
+ handler.post(() -> {
+ cursor.moveToPosition(mAlbumCollection.getCurrentSelection());
+ mAlbumsSpinner.setSelection(MatisseActivity.this,
+ mAlbumCollection.getCurrentSelection());
+ Album album = Album.valueOf(cursor);
+ if (album.isAll() && SelectionSpec.getInstance().capture) {
+ album.addCaptureCount();
+ }
+ onAlbumSelected(album);
+ });
+ }
+
+ @Override
+ public void onAlbumReset() {
+ mAlbumsAdapter.swapCursor(null);
+ }
+
+ private Fragment fragment;
+ private void onAlbumSelected(Album album) {
+ if (album.isAll() && album.isEmpty()) {
+ mContainer.setVisibility(View.GONE);
+ mEmptyView.setVisibility(View.VISIBLE);
+ } else {
+ mContainer.setVisibility(View.VISIBLE);
+ mEmptyView.setVisibility(View.GONE);
+ if (fragment != null) {
+ getSupportFragmentManager()
+ .beginTransaction()
+ .remove(fragment)
+ .commitAllowingStateLoss();
+ }
+ fragment = MediaSelectionFragment.newInstance(album);
+ getSupportFragmentManager()
+ .beginTransaction()
+ .add(R.id.container, fragment, MediaSelectionFragment.class.getSimpleName())
+ .commitAllowingStateLoss();
+ }
+ }
+
+ @Override
+ public void onUpdate() {
+ // notify bottom toolbar that check state changed.
+ updateBottomToolbar();
+
+ if (mSpec.onSelectedListener != null) {
+ mSpec.onSelectedListener.onSelected(
+ mSelectedCollection.asListOfUri(), mSelectedCollection.asListOfString());
+ }
+ }
+
+ @Override
+ public void onMediaClick(Album album, Item item, int adapterPosition) {
+ Intent intent = new Intent(this, AlbumPreviewActivity.class);
+ intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album);
+ intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item);
+ intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle());
+ intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable);
+ startActivityForResult(intent, REQUEST_CODE_PREVIEW);
+ }
+
+ @Override
+ public SelectedItemCollection provideSelectedItemCollection() {
+ return mSelectedCollection;
+ }
+
+ @Override
+ public void capture() {
+ if (mMediaStoreCompat != null) {
+ mMediaStoreCompat.dispatchCaptureIntent(this, REQUEST_CODE_CAPTURE);
+ }
+ }
+
+}
diff --git a/matisse/src/main/res/color/dracula_bottom_toolbar_apply.xml b/matisse/src/main/res/color/dracula_bottom_toolbar_apply.xml
new file mode 100644
index 0000000..84e13af
--- /dev/null
+++ b/matisse/src/main/res/color/dracula_bottom_toolbar_apply.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/color/dracula_bottom_toolbar_preview.xml b/matisse/src/main/res/color/dracula_bottom_toolbar_preview.xml
new file mode 100644
index 0000000..56e04b7
--- /dev/null
+++ b/matisse/src/main/res/color/dracula_bottom_toolbar_preview.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/color/dracula_preview_bottom_toolbar_apply.xml b/matisse/src/main/res/color/dracula_preview_bottom_toolbar_apply.xml
new file mode 100644
index 0000000..9920c27
--- /dev/null
+++ b/matisse/src/main/res/color/dracula_preview_bottom_toolbar_apply.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/color/zhihu_bottom_toolbar_apply.xml b/matisse/src/main/res/color/zhihu_bottom_toolbar_apply.xml
new file mode 100644
index 0000000..d12c968
--- /dev/null
+++ b/matisse/src/main/res/color/zhihu_bottom_toolbar_apply.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/color/zhihu_bottom_toolbar_preview.xml b/matisse/src/main/res/color/zhihu_bottom_toolbar_preview.xml
new file mode 100644
index 0000000..fef8d16
--- /dev/null
+++ b/matisse/src/main/res/color/zhihu_bottom_toolbar_preview.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/color/zhihu_preview_bottom_toolbar_apply.xml b/matisse/src/main/res/color/zhihu_preview_bottom_toolbar_apply.xml
new file mode 100644
index 0000000..0bec454
--- /dev/null
+++ b/matisse/src/main/res/color/zhihu_preview_bottom_toolbar_apply.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/matisse/src/main/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.png b/matisse/src/main/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.png
new file mode 100644
index 0000000..4c6076d
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_check_white_18dp.png b/matisse/src/main/res/drawable-hdpi/ic_check_white_18dp.png
new file mode 100644
index 0000000..08dbac9
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_check_white_18dp.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_empty_dracula.png b/matisse/src/main/res/drawable-hdpi/ic_empty_dracula.png
new file mode 100644
index 0000000..0920df4
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_empty_dracula.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_empty_zhihu.png b/matisse/src/main/res/drawable-hdpi/ic_empty_zhihu.png
new file mode 100644
index 0000000..86ffe1c
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_empty_zhihu.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_gif.png b/matisse/src/main/res/drawable-hdpi/ic_gif.png
new file mode 100644
index 0000000..d4085f5
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_gif.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_play_circle_outline_white_48dp.png b/matisse/src/main/res/drawable-hdpi/ic_play_circle_outline_white_48dp.png
new file mode 100644
index 0000000..6e1b578
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_play_circle_outline_white_48dp.png differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_preview_radio_off.webp b/matisse/src/main/res/drawable-hdpi/ic_preview_radio_off.webp
new file mode 100644
index 0000000..80930b9
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_preview_radio_off.webp differ
diff --git a/matisse/src/main/res/drawable-hdpi/ic_preview_radio_on.webp b/matisse/src/main/res/drawable-hdpi/ic_preview_radio_on.webp
new file mode 100644
index 0000000..89ff580
Binary files /dev/null and b/matisse/src/main/res/drawable-hdpi/ic_preview_radio_on.webp differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_arrow_drop_down_white_24dp.png b/matisse/src/main/res/drawable-mdpi/ic_arrow_drop_down_white_24dp.png
new file mode 100644
index 0000000..4046a74
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_arrow_drop_down_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_check_white_18dp.png b/matisse/src/main/res/drawable-mdpi/ic_check_white_18dp.png
new file mode 100644
index 0000000..ad14847
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_check_white_18dp.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_empty_dracula.png b/matisse/src/main/res/drawable-mdpi/ic_empty_dracula.png
new file mode 100644
index 0000000..34937ef
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_empty_dracula.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_empty_zhihu.png b/matisse/src/main/res/drawable-mdpi/ic_empty_zhihu.png
new file mode 100644
index 0000000..f5628ee
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_empty_zhihu.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_gif.png b/matisse/src/main/res/drawable-mdpi/ic_gif.png
new file mode 100644
index 0000000..aeeb0d4
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_gif.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_play_circle_outline_white_48dp.png b/matisse/src/main/res/drawable-mdpi/ic_play_circle_outline_white_48dp.png
new file mode 100644
index 0000000..615b80d
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_play_circle_outline_white_48dp.png differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_preview_radio_off.webp b/matisse/src/main/res/drawable-mdpi/ic_preview_radio_off.webp
new file mode 100644
index 0000000..1a8f6c1
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_preview_radio_off.webp differ
diff --git a/matisse/src/main/res/drawable-mdpi/ic_preview_radio_on.webp b/matisse/src/main/res/drawable-mdpi/ic_preview_radio_on.webp
new file mode 100644
index 0000000..b336c9c
Binary files /dev/null and b/matisse/src/main/res/drawable-mdpi/ic_preview_radio_on.webp differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.png b/matisse/src/main/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.png
new file mode 100644
index 0000000..da239e4
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_check_white_18dp.png b/matisse/src/main/res/drawable-xhdpi/ic_check_white_18dp.png
new file mode 100644
index 0000000..729f290
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_check_white_18dp.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_empty_dracula.png b/matisse/src/main/res/drawable-xhdpi/ic_empty_dracula.png
new file mode 100644
index 0000000..2cd36f6
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_empty_dracula.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_empty_zhihu.png b/matisse/src/main/res/drawable-xhdpi/ic_empty_zhihu.png
new file mode 100644
index 0000000..c223b56
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_empty_zhihu.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_gif.png b/matisse/src/main/res/drawable-xhdpi/ic_gif.png
new file mode 100644
index 0000000..ed2a70b
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_gif.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.png b/matisse/src/main/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.png
new file mode 100644
index 0000000..516f643
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.png differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_off.webp b/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_off.webp
new file mode 100644
index 0000000..b13bb1b
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_off.webp differ
diff --git a/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_on.webp b/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_on.webp
new file mode 100644
index 0000000..632b2a7
Binary files /dev/null and b/matisse/src/main/res/drawable-xhdpi/ic_preview_radio_on.webp differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.png b/matisse/src/main/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.png
new file mode 100644
index 0000000..c19c19d
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_black_back.png b/matisse/src/main/res/drawable-xxhdpi/ic_black_back.png
new file mode 100644
index 0000000..e8993f7
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_black_back.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_check_white_18dp.png b/matisse/src/main/res/drawable-xxhdpi/ic_check_white_18dp.png
new file mode 100644
index 0000000..9e3f948
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_check_white_18dp.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_empty_dracula.png b/matisse/src/main/res/drawable-xxhdpi/ic_empty_dracula.png
new file mode 100644
index 0000000..daaedc8
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_empty_dracula.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_empty_zhihu.png b/matisse/src/main/res/drawable-xxhdpi/ic_empty_zhihu.png
new file mode 100644
index 0000000..c7b14e4
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_empty_zhihu.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_gif.png b/matisse/src/main/res/drawable-xxhdpi/ic_gif.png
new file mode 100644
index 0000000..2803825
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_gif.png differ
diff --git a/matisse/src/main/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.png b/matisse/src/main/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.png
new file mode 100644
index 0000000..0311f89
Binary files /dev/null and b/matisse/src/main/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.png b/matisse/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.png
new file mode 100644
index 0000000..452e502
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_check_white_18dp.png b/matisse/src/main/res/drawable-xxxhdpi/ic_check_white_18dp.png
new file mode 100644
index 0000000..2c2ad77
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_check_white_18dp.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_empty_dracula.png b/matisse/src/main/res/drawable-xxxhdpi/ic_empty_dracula.png
new file mode 100644
index 0000000..4fc34d8
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_empty_dracula.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_empty_zhihu.png b/matisse/src/main/res/drawable-xxxhdpi/ic_empty_zhihu.png
new file mode 100644
index 0000000..9b3c3f1
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_empty_zhihu.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_gif.png b/matisse/src/main/res/drawable-xxxhdpi/ic_gif.png
new file mode 100644
index 0000000..d9d6433
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_gif.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.png b/matisse/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.png
new file mode 100644
index 0000000..777658e
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.png differ
diff --git a/matisse/src/main/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.png b/matisse/src/main/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.png
new file mode 100644
index 0000000..7a5a168
Binary files /dev/null and b/matisse/src/main/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.png differ
diff --git a/matisse/src/main/res/layout/activity_matisse.xml b/matisse/src/main/res/layout/activity_matisse.xml
new file mode 100644
index 0000000..c690a40
--- /dev/null
+++ b/matisse/src/main/res/layout/activity_matisse.xml
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/layout/activity_media_preview.xml b/matisse/src/main/res/layout/activity_media_preview.xml
new file mode 100644
index 0000000..e9fc349
--- /dev/null
+++ b/matisse/src/main/res/layout/activity_media_preview.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/layout/album_list_item.xml b/matisse/src/main/res/layout/album_list_item.xml
new file mode 100644
index 0000000..1fc8d2f
--- /dev/null
+++ b/matisse/src/main/res/layout/album_list_item.xml
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/layout/fragment_media_selection.xml b/matisse/src/main/res/layout/fragment_media_selection.xml
new file mode 100644
index 0000000..79393ee
--- /dev/null
+++ b/matisse/src/main/res/layout/fragment_media_selection.xml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
diff --git a/matisse/src/main/res/layout/fragment_preview_item.xml b/matisse/src/main/res/layout/fragment_preview_item.xml
new file mode 100644
index 0000000..2cfc58d
--- /dev/null
+++ b/matisse/src/main/res/layout/fragment_preview_item.xml
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
diff --git a/matisse/src/main/res/layout/media_grid_content.xml b/matisse/src/main/res/layout/media_grid_content.xml
new file mode 100644
index 0000000..711557c
--- /dev/null
+++ b/matisse/src/main/res/layout/media_grid_content.xml
@@ -0,0 +1,50 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/layout/media_grid_item.xml b/matisse/src/main/res/layout/media_grid_item.xml
new file mode 100644
index 0000000..392e38b
--- /dev/null
+++ b/matisse/src/main/res/layout/media_grid_item.xml
@@ -0,0 +1,21 @@
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/layout/photo_capture_item.xml b/matisse/src/main/res/layout/photo_capture_item.xml
new file mode 100644
index 0000000..9c30942
--- /dev/null
+++ b/matisse/src/main/res/layout/photo_capture_item.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values-zh-rTW/strings.xml b/matisse/src/main/res/values-zh-rTW/strings.xml
new file mode 100644
index 0000000..fc77408
--- /dev/null
+++ b/matisse/src/main/res/values-zh-rTW/strings.xml
@@ -0,0 +1,38 @@
+
+
+
+ 全部
+
+ 預覽
+ 使用
+ 使用(%1$d)
+ 返回
+ 拍一張
+ 還沒有圖片或影片
+ 我知道了
+
+ 您已經達到最大選擇數量
+ 最多只能選擇 %1$d 個文件
+ 圖片質量太低
+ 圖片質量太高
+ 不支援的文件類型
+ 不能同時選擇圖片和影片
+ 沒有支持影片預覽的應用程式
+ 原圖
+ 确定
+ 确定(%1$d)
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values-zh/strings.xml b/matisse/src/main/res/values-zh/strings.xml
new file mode 100644
index 0000000..853ea42
--- /dev/null
+++ b/matisse/src/main/res/values-zh/strings.xml
@@ -0,0 +1,40 @@
+
+
+
+ 全部
+
+ 预览
+ 使用
+ 使用(%1$d)
+ 返回
+ 拍一张
+ 还没有图片或视频
+ 我知道了
+
+ 您已经达到最大选择数量
+ 最多只能选择 %1$d 个文件
+ 图片质量太低
+ 图片质量太高
+ 不支持的文件类型
+ 不能同时选择图片和视频
+ 没有支持视频预览的应用
+ "该照片大于 %1$d M,无法上传将取消勾选原图"
+ "有 %1$d 张照片大于 %2$d M\n无法上传,将取消勾选原图"
+ 原图
+ 确定
+ 确定(%1$d)
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/attrs.xml b/matisse/src/main/res/values/attrs.xml
new file mode 100644
index 0000000..852a3e3
--- /dev/null
+++ b/matisse/src/main/res/values/attrs.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/colors.xml b/matisse/src/main/res/values/colors.xml
new file mode 100644
index 0000000..24ec1f9
--- /dev/null
+++ b/matisse/src/main/res/values/colors.xml
@@ -0,0 +1,22 @@
+
+
+
+
+ #CC000000
+ #61FFFFFF
+
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/colors_dracula.xml b/matisse/src/main/res/values/colors_dracula.xml
new file mode 100644
index 0000000..68af821
--- /dev/null
+++ b/matisse/src/main/res/values/colors_dracula.xml
@@ -0,0 +1,43 @@
+
+
+
+ #000000
+ #000000
+
+ #000000
+ #FFFFFF
+ #89FFFFFF
+ #000000
+ #4DFFFFFF
+
+ #000000
+ #263237
+ #FFFFFF
+ #FFFFFF
+
+ #000000
+ #000000
+
+ #DEFFFFFF
+ #4DFFFFFF
+ #03A9F4
+ #4D03A9F4
+
+ #FFFFFF
+ #03A9F4
+ #4D03A9F4
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/colors_zhihu.xml b/matisse/src/main/res/values/colors_zhihu.xml
new file mode 100644
index 0000000..15c19d2
--- /dev/null
+++ b/matisse/src/main/res/values/colors_zhihu.xml
@@ -0,0 +1,45 @@
+
+
+
+ #343EEF
+ #343EEF
+
+ #FFFFFF
+ #DE000000
+ #999999
+ #EAEEF4
+ #4D000000
+
+ #EAEEF4
+ #1E8AE8
+ #FFFFFF
+ #424242
+
+ #FFFFFF
+ #FFFFFF
+
+ #DE000000
+ #4D000000
+ #0077D9
+ #4D0077D9
+
+ #FFFFFF
+ #0077D9
+ #4D0077D9
+
+ #808080
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/dimens.xml b/matisse/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..f32d77a
--- /dev/null
+++ b/matisse/src/main/res/values/dimens.xml
@@ -0,0 +1,22 @@
+
+
+
+ 48dp
+ 4dp
+
+ 72dp
+
\ No newline at end of file
diff --git a/matisse/src/main/res/values/strings.xml b/matisse/src/main/res/values/strings.xml
new file mode 100644
index 0000000..fa6bebd
--- /dev/null
+++ b/matisse/src/main/res/values/strings.xml
@@ -0,0 +1,43 @@
+
+
+
+ All Media
+
+ Preview
+ Apply
+ Apply(%1$d)
+ Back
+ Camera
+ No media yet
+ OK
+
+ You have reached max selectable
+ You can only select up to %1$d media files
+ Under quality
+ Over quality
+ Unsupported file type
+ Can\'t select images and videos at the same time
+ No App found supporting video preview
+ Can\'t select the images larger than %1$d MB
+ %1$d images over %2$d MB. Original will be unchecked
+ Original
+ Sure
+ Sure(%1$d)
+
+
diff --git a/matisse/src/main/res/values/styles.xml b/matisse/src/main/res/values/styles.xml
new file mode 100644
index 0000000..17a0fd4
--- /dev/null
+++ b/matisse/src/main/res/values/styles.xml
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+ //====================================== Theme Zhihu ===========================================
+
+
+
+
+
+
+
+ //===================================== Theme Dracula ==========================================
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..3f658d9
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,5 @@
+include ':basicLib'
+include ':app'
+include ':cili'
+include ':youyou'
+include ':matisse'