diff --git a/app/src/module_album/java/com/example/matisse/Matisse.java b/app/src/module_album/java/com/example/matisse/Matisse.java new file mode 100644 index 0000000..d61ad0d --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.matisse; + +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.example.matisse.internal.entity.CustomItem; +import com.example.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 List obtainPathResult(Intent data) { + return data.getParcelableArrayListExtra(MatisseActivity.EXTRA_RESULT_SELECTION_PATH); + } + + public static boolean obtainOriginalImageResult(Intent data) { + return data.getBooleanExtra(MatisseActivity.EXTRA_RESULT_ORIGINAL_IMAGE, false); + } + + public static String obtainMineResult(Intent data) { + return data.getStringExtra(MatisseActivity.EXTRA_RESULT_MIME_TYPE); + } + + /** + * 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/app/src/module_album/java/com/example/matisse/MimeType.java b/app/src/module_album/java/com/example/matisse/MimeType.java new file mode 100644 index 0000000..3ecfd2a --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/MimeType.java @@ -0,0 +1,176 @@ +/* + * 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.example.matisse; + +import android.content.ContentResolver; +import android.net.Uri; +import android.text.TextUtils; +import android.webkit.MimeTypeMap; + +import androidx.collection.ArraySet; + +import com.example.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 ofGif() { + return ofImage(true); + } + public static Set ofImage(boolean onlyGif) { + return EnumSet.of(GIF); + } + + 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/app/src/module_album/java/com/example/matisse/SelectionCreator.java b/app/src/module_album/java/com/example/matisse/SelectionCreator.java new file mode 100644 index 0000000..9268c81 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/SelectionCreator.java @@ -0,0 +1,378 @@ +/* + * 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.example.matisse; + +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; + +import android.app.Activity; +import android.content.Intent; +import android.os.Build; + +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.example.matisse.engine.ImageEngine; +import com.example.matisse.filter.Filter; +import com.example.matisse.internal.entity.CaptureStrategy; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.listener.OnCheckedListener; +import com.example.matisse.listener.OnSelectedListener; +import com.example.matisse.ui.MatisseActivity; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Set; + +/** + * 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.example.matisse.R.style.Matisse_Zhihu; + * 2. com.example.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.example.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; + } + + /** + * 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; + 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.example.matisse.engine.impl.GlideEngine} + * 2. {@link com.example.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 setType(int type) { + mSelectionSpec.type = type; + return this; + } + + public SelectionCreator setOriginalImagee(boolean isOriginalImage) { + mSelectionSpec.isOriginalImage = isOriginalImage; + return this; + } +} diff --git a/app/src/module_album/java/com/example/matisse/engine/ImageEngine.java b/app/src/module_album/java/com/example/matisse/engine/ImageEngine.java new file mode 100644 index 0000000..3a73e6f --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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.example.matisse.engine.impl.GlideEngine} + * and {@link com.example.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/app/src/module_album/java/com/example/matisse/engine/impl/GlideEngine.java b/app/src/module_album/java/com/example/matisse/engine/impl/GlideEngine.java new file mode 100644 index 0000000..6e471c2 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/engine/impl/GlideEngine.java @@ -0,0 +1,81 @@ +/* + * 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.example.matisse.engine.impl; + +import android.content.Context; +import android.graphics.drawable.Drawable; +import android.net.Uri; +import android.widget.ImageView; + +import com.bumptech.glide.Priority; +import com.netease.nim.uikit.support.glide.GlideApp; +import com.example.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) { + GlideApp.with(context) + .asBitmap() // some .jpeg files are actually gif + .load(uri) + .placeholder(placeholder) + .override(resize, resize) + .centerCrop() + .into(imageView); + } + + @Override + public void loadGifThumbnail(Context context, int resize, Drawable placeholder, ImageView imageView, + Uri uri) { + GlideApp.with(context) + .asBitmap() + .load(uri) + .placeholder(placeholder) + .override(resize, resize) + .centerCrop() + .into(imageView); + } + + @Override + public void loadImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { + GlideApp.with(context) + .load(uri) + .override(resizeX, resizeY) + .priority(Priority.HIGH) + .fitCenter() + .into(imageView); + } + + @Override + public void loadGifImage(Context context, int resizeX, int resizeY, ImageView imageView, Uri uri) { + GlideApp.with(context) + .asGif() + .load(uri) + .override(resizeX, resizeY) + .priority(Priority.HIGH) + .into(imageView); + } + + @Override + public boolean supportAnimatedGif() { + return true; + } + +} diff --git a/app/src/module_album/java/com/example/matisse/filter/Filter.java b/app/src/module_album/java/com/example/matisse/filter/Filter.java new file mode 100644 index 0000000..d2ab5aa --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.matisse.filter; + +import android.content.Context; + +import com.example.matisse.MimeType; +import com.example.matisse.SelectionCreator; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; + +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/app/src/module_album/java/com/example/matisse/internal/entity/Album.java b/app/src/module_album/java/com/example/matisse/internal/entity/Album.java new file mode 100644 index 0000000..9dc60e6 --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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.chwl.app.R; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/entity/CaptureStrategy.java b/app/src/module_album/java/com/example/matisse/internal/entity/CaptureStrategy.java new file mode 100644 index 0000000..6a95e53 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/entity/CaptureStrategy.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.example.matisse.internal.entity; + +import com.netease.nim.uikit.common.util.log.LogUtil; +import com.chwl.app.R; +import com.chwl.library.utils.ResUtil; + +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; + LogUtil.print(ResUtil.getString(R.string.internal_entity_capturestrategy_01) + directory); + this.directory = directory; + } +} diff --git a/app/src/module_album/java/com/example/matisse/internal/entity/CustomItem.java b/app/src/module_album/java/com/example/matisse/internal/entity/CustomItem.java new file mode 100644 index 0000000..182512d --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/entity/CustomItem.java @@ -0,0 +1,112 @@ +package com.example.matisse.internal.entity; + +import android.os.Parcel; +import android.os.Parcelable; + +import java.io.Serializable; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; + +/** + * create by lvzebiao @2019/11/19 + */ +@AllArgsConstructor +@Getter +@Setter +public class CustomItem implements Parcelable, Serializable { + + public final static int UNKOWN = -1; + + public final static int IMAGE_NORMAL = 0; + + public final static int GIF = 1; + + public final static int VIDEO = 2; + + private String path; + /** + * -1 未知 + * 0 图片 + * 1 gif + * 2 视频 + */ + private int fileType; + + private String format; + + private int width; + + private int height; + + public String getPath(){ + return path; + } + + public int getFileType(){ + return fileType; + } + + public boolean isImage() { + return fileType == IMAGE_NORMAL; + } + + public boolean isGif() { + return fileType == GIF; + } + + /** + * 是否是网络图片 + * @return - + */ + public boolean isNetImage() { + try { + return path != null && path.startsWith("http"); + } catch (Exception ex) { + ex.printStackTrace(); + } + return false; + } + + public CustomItem(String path, int fileType) { + this(path, fileType, "jpeg",0,0); + } + + public CustomItem() { + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(this.path); + dest.writeInt(this.fileType); + dest.writeString(this.format); + dest.writeInt(this.width); + dest.writeInt(this.height); + } + + protected CustomItem(Parcel in) { + this.path = in.readString(); + this.fileType = in.readInt(); + this.format = in.readString(); + this.width = in.readInt(); + this.height = in.readInt(); + } + + public static final Creator CREATOR = new Creator() { + @Override + public CustomItem createFromParcel(Parcel source) { + return new CustomItem(source); + } + + @Override + public CustomItem[] newArray(int size) { + return new CustomItem[size]; + } + }; +} diff --git a/app/src/module_album/java/com/example/matisse/internal/entity/IncapableCause.java b/app/src/module_album/java/com/example/matisse/internal/entity/IncapableCause.java new file mode 100644 index 0000000..4323175 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/entity/IncapableCause.java @@ -0,0 +1,84 @@ +/* + * 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.example.matisse.internal.entity; + +import static java.lang.annotation.RetentionPolicy.SOURCE; + +import android.content.Context; + +import androidx.annotation.IntDef; +import androidx.fragment.app.FragmentActivity; + +import com.chwl.library.utils.SingleToastUtil; +import com.example.matisse.internal.ui.widget.IncapableDialog; + +import java.lang.annotation.Retention; + +@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: + SingleToastUtil.showToast(cause.mMessage); + break; + } + } +} diff --git a/app/src/module_album/java/com/example/matisse/internal/entity/Item.java b/app/src/module_album/java/com/example/matisse/internal/entity/Item.java new file mode 100644 index 0000000..314b1f6 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/entity/Item.java @@ -0,0 +1,153 @@ +/* + * 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.example.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.example.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; + } + + public Item(String mimeType, Uri uri, long size, long duration) { + this.id = 1; + this.mimeType = mimeType; + this.uri = uri; + 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/app/src/module_album/java/com/example/matisse/internal/entity/SelectionSpec.java b/app/src/module_album/java/com/example/matisse/internal/entity/SelectionSpec.java new file mode 100644 index 0000000..1cfa793 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/entity/SelectionSpec.java @@ -0,0 +1,123 @@ +/* + * 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.example.matisse.internal.entity; + +import android.content.pm.ActivityInfo; + +import androidx.annotation.StyleRes; + +import com.chwl.app.R; +import com.example.matisse.MimeType; +import com.example.matisse.engine.ImageEngine; +import com.example.matisse.engine.impl.GlideEngine; +import com.example.matisse.filter.Filter; +import com.example.matisse.listener.OnCheckedListener; +import com.example.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 int type; // 1 发布页跳转过来 + + public boolean isOriginalImage; + + 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; + isOriginalImage = false; + } + + public boolean singleSelectionModeEnabled() { + return !countable && (maxSelectable == 1 || (maxImageSelectable == 1 && maxVideoSelectable == 1)); + } + + public boolean needOrientationRestriction() { + return orientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED; + } + + public boolean onlyShowGif() { + return showSingleMediaType && MimeType.ofGif().equals(mimeTypeSet); + } + + public boolean onlyShowImages() { + return showSingleMediaType && MimeType.ofImage().containsAll(mimeTypeSet); + } + + public boolean onlyShowVideos() { + return showSingleMediaType && MimeType.ofVideo().containsAll(mimeTypeSet); + } + + private static final class InstanceHolder { + private static final SelectionSpec INSTANCE = new SelectionSpec(); + } +} diff --git a/app/src/module_album/java/com/example/matisse/internal/loader/AlbumLoader.java b/app/src/module_album/java/com/example/matisse/internal/loader/AlbumLoader.java new file mode 100644 index 0000000..2447ef6 --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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.example.matisse.MimeType; +import com.example.matisse.internal.entity.Album; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/loader/AlbumMediaLoader.java b/app/src/module_album/java/com/example/matisse/internal/loader/AlbumMediaLoader.java new file mode 100644 index 0000000..df7975f --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/loader/AlbumMediaLoader.java @@ -0,0 +1,155 @@ +/* + * 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.example.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.example.matisse.internal.entity.Album; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.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}; + } + // =============================================================== + + 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().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().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/app/src/module_album/java/com/example/matisse/internal/model/AlbumCollection.java b/app/src/module_album/java/com/example/matisse/internal/model/AlbumCollection.java new file mode 100644 index 0000000..a596ba2 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/model/AlbumCollection.java @@ -0,0 +1,115 @@ +/* + * 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.example.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.example.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/app/src/module_album/java/com/example/matisse/internal/model/AlbumMediaCollection.java b/app/src/module_album/java/com/example/matisse/internal/model/AlbumMediaCollection.java new file mode 100644 index 0000000..eae235c --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/model/AlbumMediaCollection.java @@ -0,0 +1,108 @@ +/* + * 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.example.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.example.matisse.internal.entity.Album; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/model/SelectedItemCollection.java b/app/src/module_album/java/com/example/matisse/internal/model/SelectedItemCollection.java new file mode 100644 index 0000000..aa8ccbd --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/model/SelectedItemCollection.java @@ -0,0 +1,268 @@ +/* + * 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.example.matisse.internal.model; + +import android.content.Context; +import android.content.res.Resources; +import android.net.Uri; +import android.os.Bundle; + +import com.chwl.app.R; +import com.example.matisse.internal.entity.CustomItem; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.ui.widget.CheckView; +import com.example.matisse.internal.utils.PathUtils; +import com.example.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; + } + } + } + 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 List asListOfCustomItem() { + List paths = new ArrayList<>(); + for (Item item : mItems) { + CustomItem customItem = new CustomItem(); + customItem.setPath(PathUtils.getPath(mContext, item.getContentUri())); + customItem.setFileType(item.isGif() ? 1 : 0); + paths.add(customItem); + } + return paths; + } + + 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(); + } + + // depends + private int currentMaxSelectable() { + SelectionSpec spec = SelectionSpec.getInstance(); + if (spec.maxSelectable > 0) { + 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/app/src/module_album/java/com/example/matisse/internal/ui/AlbumPreviewActivity.java b/app/src/module_album/java/com/example/matisse/internal/ui/AlbumPreviewActivity.java new file mode 100644 index 0000000..60506c1 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/AlbumPreviewActivity.java @@ -0,0 +1,101 @@ +/* + * 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.example.matisse.internal.ui; + +import android.database.Cursor; +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.example.matisse.internal.entity.Album; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.model.AlbumMediaCollection; +import com.example.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) { + if (cursor == null || cursor.isClosed()) { + return; + } + 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/app/src/module_album/java/com/example/matisse/internal/ui/BasePreviewActivity.java b/app/src/module_album/java/com/example/matisse/internal/ui/BasePreviewActivity.java new file mode 100644 index 0000000..1eedc53 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/BasePreviewActivity.java @@ -0,0 +1,373 @@ +/* + * 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.example.matisse.internal.ui; + +import android.app.Activity; +import android.content.Intent; +import android.graphics.Color; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.viewpager.widget.ViewPager; + +import com.chwl.app.R; +import com.chwl.app.base.TitleBar; +import com.chwl.library.utils.ResUtil; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.model.SelectedItemCollection; +import com.example.matisse.internal.ui.adapter.PreviewPagerAdapter; +import com.example.matisse.internal.ui.widget.CheckRadioView; +import com.example.matisse.internal.ui.widget.CheckView; +import com.example.matisse.internal.ui.widget.IncapableDialog; +import com.example.matisse.internal.utils.PhotoMetadataUtils; +import com.example.matisse.internal.utils.Platform; +import com.example.matisse.listener.OnFragmentInteractionListener; +import com.example.matisse.widget.ConfirmPickView; + +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 ConfirmPickView 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 = findViewById(R.id.button_apply); + mSize = (TextView) findViewById(R.id.size); + mButtonBack.setOnClickListener(this); + mButtonApply.setOnClickListener(this); + + ImageView ivClose = findViewById(R.id.iv_close); + ivClose.setOnClickListener(this); + ViewGroup.LayoutParams params = ivClose.getLayoutParams(); + if (params instanceof RelativeLayout.LayoutParams) { + ((RelativeLayout.LayoutParams) params).topMargin = TitleBar.getStatusBarHeight(); + ivClose.setLayoutParams(params); + } + + 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(new View.OnClickListener() { + + @Override + public void onClick(View 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(new View.OnClickListener() { + @Override + public void onClick(View 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 || v.getId() == R.id.iv_close) { + 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(ResUtil.getString(R.string.internal_ui_basepreviewactivity_01), 0); + mButtonApply.setEnabled(false); + } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) { + mButtonApply.setText(ResUtil.getString(R.string.internal_ui_basepreviewactivity_02), 0); + mButtonApply.setEnabled(true); + } else { + mButtonApply.setEnabled(true); + mButtonApply.setText(ResUtil.getString(R.string.internal_ui_basepreviewactivity_03), 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/app/src/module_album/java/com/example/matisse/internal/ui/MediaSelectionFragment.java b/app/src/module_album/java/com/example/matisse/internal/ui/MediaSelectionFragment.java new file mode 100644 index 0000000..70b9d65 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/MediaSelectionFragment.java @@ -0,0 +1,160 @@ +/* + * 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.example.matisse.internal.ui; + +import android.content.Context; +import android.database.Cursor; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.chwl.app.R; +import com.example.matisse.internal.entity.Album; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.model.AlbumMediaCollection; +import com.example.matisse.internal.model.SelectedItemCollection; +import com.example.matisse.internal.ui.adapter.AlbumMediaAdapter; +import com.example.matisse.internal.ui.widget.MediaGridInset; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/ui/PreviewItemFragment.java b/app/src/module_album/java/com/example/matisse/internal/ui/PreviewItemFragment.java new file mode 100644 index 0000000..0ef2235 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/PreviewItemFragment.java @@ -0,0 +1,130 @@ +/* + * 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.example.matisse.internal.ui; + +import android.content.ActivityNotFoundException; +import android.content.Context; +import android.content.Intent; +import android.graphics.Point; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + +import com.chwl.app.R; +import com.chwl.library.utils.SingleToastUtil; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.utils.PhotoMetadataUtils; +import com.example.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(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent intent = new Intent(Intent.ACTION_VIEW); + intent.setDataAndType(item.uri, "video/*"); + try { + startActivity(intent); + } catch (ActivityNotFoundException e) { + SingleToastUtil.showToast(R.string.error_no_video_activity); + } + } + }); + } else { + videoPlayButton.setVisibility(View.GONE); + } + + ImageViewTouch image = (ImageViewTouch) view.findViewById(R.id.image_view); + image.setDisplayType(ImageViewTouchBase.DisplayType.FIT_TO_SCREEN); + + image.setSingleTapListener(new ImageViewTouch.OnImageViewTouchSingleTapListener() { + @Override + public void onSingleTapConfirmed() { + 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/app/src/module_album/java/com/example/matisse/internal/ui/SelectedPreviewActivity.java b/app/src/module_album/java/com/example/matisse/internal/ui/SelectedPreviewActivity.java new file mode 100644 index 0000000..4bc91c2 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/SelectedPreviewActivity.java @@ -0,0 +1,52 @@ +/* + * 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.example.matisse.internal.ui; + +import android.os.Bundle; + +import androidx.annotation.Nullable; + +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/ui/adapter/AlbumMediaAdapter.java b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/AlbumMediaAdapter.java new file mode 100644 index 0000000..67d008c --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/AlbumMediaAdapter.java @@ -0,0 +1,292 @@ +/* + * 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.example.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.ImageView; + +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.chwl.app.R; +import com.example.matisse.internal.entity.Album; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.model.SelectedItemCollection; +import com.example.matisse.internal.ui.widget.CheckView; +import com.example.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(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getContext() instanceof OnPhotoCapture) { + ((OnPhotoCapture) v.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) holder).mMediaGrid.setCheckViewVisibility(mSelectionSpec.type == 1 && item.isVideo() ? View.GONE : View.VISIBLE); + 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 (mOnMediaClickListener != null) { + mOnMediaClickListener.onMediaClick(null, item, holder.getAdapterPosition()); + } + } + + @Override + public void onCheckViewClicked(CheckView checkView, 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/app/src/module_album/java/com/example/matisse/internal/ui/adapter/AlbumsAdapter.java b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/AlbumsAdapter.java new file mode 100644 index 0000000..ed998c0 --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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.chwl.app.R; +import com.example.matisse.internal.entity.Album; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/ui/adapter/PreviewPagerAdapter.java b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/PreviewPagerAdapter.java new file mode 100644 index 0000000..64cdd0f --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/PreviewPagerAdapter.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.example.matisse.internal.ui.adapter; + +import android.view.ViewGroup; + +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + +import com.example.matisse.internal.entity.Item; +import com.example.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/app/src/module_album/java/com/example/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java new file mode 100644 index 0000000..11a2f82 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java @@ -0,0 +1,106 @@ +/* + * 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.example.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/app/src/module_album/java/com/example/matisse/internal/ui/widget/AlbumsSpinner.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/AlbumsSpinner.java new file mode 100644 index 0000000..497c60d --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/AlbumsSpinner.java @@ -0,0 +1,130 @@ +/* + * 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.example.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 android.view.View; +import android.widget.AdapterView; +import android.widget.CursorAdapter; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.appcompat.widget.ListPopupWindow; + +import com.chwl.app.R; +import com.example.matisse.internal.entity.Album; +import com.example.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((int) (216 * density)); + mListPopupWindow.setHorizontalOffset((int) (16 * density)); + mListPopupWindow.setVerticalOffset((int) (-48 * density)); + + mListPopupWindow.setOnItemClickListener(new AdapterView.OnItemClickListener() { + + @Override + public void onItemClick(AdapterView parent, View view, int position, long 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(new View.OnClickListener() { + + @Override + public void onClick(View 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/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckRadioView.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckRadioView.java new file mode 100644 index 0000000..a85283d --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckRadioView.java @@ -0,0 +1,61 @@ +package com.example.matisse.internal.ui.widget; + +import android.content.Context; +import android.graphics.PorterDuff; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.content.res.ResourcesCompat; + +import com.chwl.app.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/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckView.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckView.java new file mode 100644 index 0000000..9445854 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/CheckView.java @@ -0,0 +1,230 @@ +/* + * 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.example.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 android.text.TextPaint; +import android.util.AttributeSet; +import android.view.View; + +import androidx.core.content.res.ResourcesCompat; + +import com.chwl.app.R; + +public class CheckView extends View { + + public static final int UNCHECKED = Integer.MIN_VALUE; + private static final float STROKE_WIDTH = 1.5f; // dp + private static final float SHADOW_WIDTH = 6.0f; // dp + private static final int SIZE = 25; // dp + private static final float STROKE_RADIUS = 8.5f; // dp + private static final float BG_RADIUS = 9.5f; // 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.WHITE); + + 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/app/src/module_album/java/com/example/matisse/internal/ui/widget/IncapableDialog.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/IncapableDialog.java new file mode 100644 index 0000000..3393d1b --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/IncapableDialog.java @@ -0,0 +1,66 @@ +/* + * 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.example.matisse.internal.ui.widget; + +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.TextUtils; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import com.chwl.app.R; +import com.chwl.app.common.widget.dialog.BaseAlertDialogBuilder; + +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 BaseAlertDialogBuilder(getActivity()); + if (!TextUtils.isEmpty(title)) { + builder.setTitle(title); + } + if (!TextUtils.isEmpty(message)) { + builder.setMessage(message); + } + builder.setPositiveButton(R.string.button_ok, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }); + + return builder.create(); + } +} diff --git a/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGrid.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGrid.java new file mode 100644 index 0000000..03119f9 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGrid.java @@ -0,0 +1,166 @@ +/* + * 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.example.matisse.internal.ui.widget; + +import android.content.Context; +import android.graphics.drawable.Drawable; +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 androidx.recyclerview.widget.RecyclerView; + +import com.chwl.app.R; +import com.example.matisse.internal.entity.Item; +import com.example.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); + } + + public void setCheckViewVisibility(int visibility) { + mCheckView.setVisibility(visibility); + } + + 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/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGridInset.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGridInset.java new file mode 100644 index 0000000..3ad2b81 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/MediaGridInset.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.example.matisse.internal.ui.widget; + +import android.graphics.Rect; +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +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/app/src/module_album/java/com/example/matisse/internal/ui/widget/PreviewViewPager.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/PreviewViewPager.java new file mode 100644 index 0000000..828b549 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/PreviewViewPager.java @@ -0,0 +1,39 @@ +/* + * 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.example.matisse.internal.ui.widget; + +import android.content.Context; +import android.util.AttributeSet; +import android.view.View; + +import androidx.viewpager.widget.ViewPager; + +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/app/src/module_album/java/com/example/matisse/internal/ui/widget/RoundedRectangleImageView.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/RoundedRectangleImageView.java new file mode 100644 index 0000000..fc7d4f2 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/ui/widget/RoundedRectangleImageView.java @@ -0,0 +1,66 @@ +/* + * 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.example.matisse.internal.ui.widget; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Path; +import android.graphics.RectF; +import android.util.AttributeSet; + +import androidx.appcompat.widget.AppCompatImageView; + +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/app/src/module_album/java/com/example/matisse/internal/ui/widget/SquareFrameLayout.java b/app/src/module_album/java/com/example/matisse/internal/ui/widget/SquareFrameLayout.java new file mode 100644 index 0000000..8cc332e --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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/app/src/module_album/java/com/example/matisse/internal/utils/ExifInterfaceCompat.java b/app/src/module_album/java/com/example/matisse/internal/utils/ExifInterfaceCompat.java new file mode 100644 index 0000000..b76d0a9 --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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/app/src/module_album/java/com/example/matisse/internal/utils/MediaStoreCompat.java b/app/src/module_album/java/com/example/matisse/internal/utils/MediaStoreCompat.java new file mode 100644 index 0000000..1748f0d --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/utils/MediaStoreCompat.java @@ -0,0 +1,144 @@ +/* + * 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.example.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.core.content.FileProvider; +import androidx.core.os.EnvironmentCompat; +import androidx.fragment.app.Fragment; + +import com.example.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/app/src/module_album/java/com/example/matisse/internal/utils/PathUtils.java b/app/src/module_album/java/com/example/matisse/internal/utils/PathUtils.java new file mode 100644 index 0000000..c7a6a04 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/utils/PathUtils.java @@ -0,0 +1,133 @@ +package com.example.matisse.internal.utils; + +import android.content.ContentUris; +import android.content.Context; +import android.database.Cursor; +import android.net.Uri; +import android.provider.DocumentsContract; +import android.provider.MediaStore; + +import com.chwl.library.common.file.FileHelper; + +/** + * 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 + */ + 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 FileHelper.getRootFilesDir(null).getAbsolutePath() + "/" + 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/app/src/module_album/java/com/example/matisse/internal/utils/PhotoMetadataUtils.java b/app/src/module_album/java/com/example/matisse/internal/utils/PhotoMetadataUtils.java new file mode 100644 index 0000000..8769dac --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/utils/PhotoMetadataUtils.java @@ -0,0 +1,180 @@ +/* + * 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.example.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.chwl.app.R; +import com.example.matisse.MimeType; +import com.example.matisse.filter.Filter; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +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.setDecimalFormatSymbols(DecimalFormatSymbols.getInstance(Locale.ENGLISH)); + 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/app/src/module_album/java/com/example/matisse/internal/utils/Platform.java b/app/src/module_album/java/com/example/matisse/internal/utils/Platform.java new file mode 100644 index 0000000..dfd8d6d --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/utils/Platform.java @@ -0,0 +1,16 @@ +package com.example.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/app/src/module_album/java/com/example/matisse/internal/utils/SingleMediaScanner.java b/app/src/module_album/java/com/example/matisse/internal/utils/SingleMediaScanner.java new file mode 100644 index 0000000..3ce7391 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/internal/utils/SingleMediaScanner.java @@ -0,0 +1,44 @@ +package com.example.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(); + } + } +} \ No newline at end of file diff --git a/app/src/module_album/java/com/example/matisse/internal/utils/UIUtils.java b/app/src/module_album/java/com/example/matisse/internal/utils/UIUtils.java new file mode 100644 index 0000000..e55ab06 --- /dev/null +++ b/app/src/module_album/java/com/example/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.example.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/app/src/module_album/java/com/example/matisse/listener/OnCheckedListener.java b/app/src/module_album/java/com/example/matisse/listener/OnCheckedListener.java new file mode 100644 index 0000000..e7545c6 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/listener/OnCheckedListener.java @@ -0,0 +1,9 @@ +package com.example.matisse.listener; + + +/** + * when original is enabled , callback immediately when user check or uncheck original. + */ +public interface OnCheckedListener { + void onCheck(boolean isChecked); +} diff --git a/app/src/module_album/java/com/example/matisse/listener/OnFragmentInteractionListener.java b/app/src/module_album/java/com/example/matisse/listener/OnFragmentInteractionListener.java new file mode 100644 index 0000000..6e0b2e3 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/listener/OnFragmentInteractionListener.java @@ -0,0 +1,11 @@ +package com.example.matisse.listener; + +/** + * PreViewItemFragment 和 BasePreViewActivity 通信的接口 ,为了方便拿到 ImageViewTouch 的点击事件 + */ +public interface OnFragmentInteractionListener { + /** + * ImageViewTouch 被点击了 + */ + void onClick(); +} diff --git a/app/src/module_album/java/com/example/matisse/listener/OnSelectedListener.java b/app/src/module_album/java/com/example/matisse/listener/OnSelectedListener.java new file mode 100644 index 0000000..f6743b2 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/listener/OnSelectedListener.java @@ -0,0 +1,31 @@ +/* + * 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.example.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/app/src/module_album/java/com/example/matisse/ui/MatisseActivity.java b/app/src/module_album/java/com/example/matisse/ui/MatisseActivity.java new file mode 100644 index 0000000..58a7d63 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/ui/MatisseActivity.java @@ -0,0 +1,550 @@ +/* + * 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.example.matisse.ui; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +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.view.MenuItem; +import android.view.View; +import android.widget.AdapterView; +import android.widget.CheckBox; +import android.widget.LinearLayout; +import android.widget.RelativeLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.core.app.ActivityCompat; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import com.netease.nim.uikit.common.util.log.LogUtil; +import com.chwl.app.R; +import com.chwl.library.utils.ResUtil; +import com.example.matisse.internal.entity.Album; +import com.example.matisse.internal.entity.CustomItem; +import com.example.matisse.internal.entity.IncapableCause; +import com.example.matisse.internal.entity.Item; +import com.example.matisse.internal.entity.SelectionSpec; +import com.example.matisse.internal.model.AlbumCollection; +import com.example.matisse.internal.model.SelectedItemCollection; +import com.example.matisse.internal.ui.AlbumPreviewActivity; +import com.example.matisse.internal.ui.BasePreviewActivity; +import com.example.matisse.internal.ui.MediaSelectionFragment; +import com.example.matisse.internal.ui.SelectedPreviewActivity; +import com.example.matisse.internal.ui.adapter.AlbumMediaAdapter; +import com.example.matisse.internal.ui.adapter.AlbumsAdapter; +import com.example.matisse.internal.ui.widget.AlbumsSpinner; +import com.example.matisse.internal.ui.widget.CheckRadioView; +import com.example.matisse.internal.ui.widget.IncapableDialog; +import com.example.matisse.internal.utils.MediaStoreCompat; +import com.example.matisse.internal.utils.PathUtils; +import com.example.matisse.internal.utils.PhotoMetadataUtils; +import com.example.matisse.internal.utils.SingleMediaScanner; +import com.example.matisse.widget.ConfirmPickView; + +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_ORIGINAL_ENABLE = "extra_result_original_enable"; + public static final String EXTRA_RESULT_MIME_TYPE = "extra_result_mime_type"; + /** + * ture 表示 上传原图到服务器 + */ + public static final String EXTRA_RESULT_ORIGINAL_IMAGE = "EXTRA_RESULT_ORIGINAL_IMAGE"; + + private static final int REQUEST_CODE_PREVIEW = 23; + private static final int REQUEST_CODE_CAPTURE = 24; + private static final int REQUEST_CODE_EDIT_VIDEO = 25; + 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 View mIvTick; + + private CheckBox cbSourceImage; + + private ConfirmPickView tvConfirmPick; + + @Override + protected void onCreate(@Nullable Bundle savedInstanceState) { + // programmatically set theme before super.onCreate() + 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); + View buttonToolbar = findViewById(R.id.bottom_toolbar); + View llToolbar = findViewById(R.id.ll_toolbar); + 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); + mIvTick = findViewById(R.id.iv_tick); + + cbSourceImage = findViewById(R.id.cb_source_image); + tvConfirmPick = findViewById(R.id.tv_confirm_pick); + tvConfirmPick.setOnClickListener(this); + tvConfirmPick.setEnabled(false); + + cbSourceImage.setChecked(mSpec.isOriginalImage); + + if (mSpec.type == 1 || mSpec.type == 2) { + View ivClose = findViewById(R.id.iv_close); + ivClose.setOnClickListener(this); + mIvTick.setOnClickListener(this); + toolbar.setVisibility(View.INVISIBLE); + buttonToolbar.setVisibility(View.GONE); + llToolbar.setVisibility(View.VISIBLE); + RelativeLayout.LayoutParams layoutParams = (RelativeLayout.LayoutParams) mContainer.getLayoutParams(); + layoutParams.addRule(RelativeLayout.BELOW, R.id.ll_toolbar); + mContainer.setLayoutParams(layoutParams); + } else { + 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); + } + + 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 && resultCode != 101 && resultCode != 102)// 101: 拍照 102:拍摄视频 + return; + + if (requestCode == REQUEST_CODE_PREVIEW) { + 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<>(); + if (selected != null) { + for (Item item : selected) { + selectedUris.add(item.getContentUri()); + CustomItem customItem = new CustomItem(); + customItem.setFileType(item.isGif() ? 1 : 0); + customItem.setPath(PathUtils.getPath(this, item.getContentUri())); + selectedPaths.add(customItem); + } + } + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris); + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths); + result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); + result.putExtra(EXTRA_RESULT_ORIGINAL_IMAGE, cbSourceImage.isChecked()); + 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) { + // 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<>(); + CustomItem customItem = new CustomItem(); + customItem.setFileType(0); + customItem.setPath(path); + selectedPath.add(customItem); + Intent result = new Intent(); + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selected); + result.putParcelableArrayListExtra(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, () -> LogUtil.print("scan finish!")); + finish(); + + } else if (requestCode == REQUEST_CODE_EDIT_VIDEO) { + Bundle resultBundle = data.getBundleExtra(BasePreviewActivity.EXTRA_RESULT_BUNDLE); + ArrayList selected = resultBundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION); + ArrayList selectedPath = new ArrayList<>(1); + Item item = selected.get(0); + CustomItem customItem = new CustomItem(); + customItem.setFileType(2); + customItem.setPath(item.uri.toString()); + selectedPath.add(customItem); + Intent result = new Intent(); + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPath); + result.putExtra(EXTRA_RESULT_MIME_TYPE, "video"); + setResult(RESULT_OK, result); + finish(); + } + } + + private void updateBottomToolbar() { + int selectedCount = mSelectedCollection.count(); + if (selectedCount == 0) { + mButtonPreview.setEnabled(false); + mButtonApply.setEnabled(false); + mButtonApply.setText(getString(R.string.button_sure_default)); + mIvTick.setEnabled(false); + + tvConfirmPick.setEnabled(false); + tvConfirmPick.setText(ResUtil.getString(R.string.matisse_ui_matisseactivity_01), 0); + } else if (selectedCount == 1 && mSpec.singleSelectionModeEnabled()) { + mButtonPreview.setEnabled(true); + mButtonApply.setText(R.string.button_sure_default); + mButtonApply.setEnabled(true); + mIvTick.setEnabled(true); + + tvConfirmPick.setEnabled(true); + tvConfirmPick.setText(ResUtil.getString(R.string.matisse_ui_matisseactivity_02), 0); + } else { + mButtonPreview.setEnabled(true); + mButtonApply.setEnabled(true); + mButtonApply.setText(getString(R.string.button_sure, selectedCount)); + mIvTick.setEnabled(true); + + tvConfirmPick.setEnabled(true); + tvConfirmPick.setText(ResUtil.getString(R.string.matisse_ui_matisseactivity_03), 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 || v.getId() == R.id.iv_tick || v.getId() == R.id.tv_confirm_pick) { + Intent result = new Intent(); + ArrayList selectedUris = (ArrayList) mSelectedCollection.asListOfUri(); + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris); + ArrayList selectedPaths = (ArrayList) mSelectedCollection.asListOfCustomItem(); + result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPaths); + result.putExtra(EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); + result.putExtra(EXTRA_RESULT_ORIGINAL_IMAGE, cbSourceImage.isChecked()); + 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); + } + } else if (v.getId() == R.id.iv_close) { + finish(); + } + } + + @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(new Runnable() { + + @Override + public void run() { + 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 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); + Fragment fragment = MediaSelectionFragment.newInstance(album); + getSupportFragmentManager() + .beginTransaction() + .replace(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) { + if (mSelectedCollection.typeConflict(item)) { + IncapableCause.handleCause(this, new IncapableCause(getString(R.string.error_type_conflict))); + return; + } + if ((mSpec.type == 1 || mSpec.type == 2) && item.isVideo()) { + Intent intent = new Intent(); + intent.setClassName(this, "com.aliyun.demo.crop.AliyunVideoCropActivity"); + intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); + intent.putExtra("video_path", PathUtils.getPath(this, item.getContentUri())); + intent.putExtra("video_duration", item.duration); + intent.putExtra("video_ratio", 2); + intent.putExtra("video_RESOLUTION", 3); + intent.putExtra("tail_animation", true); + intent.putExtra("entrance", "community"); + intent.putExtra("type", 1); + intent.putExtra("from", 11); // 11 动态发布 + startActivityForResult(intent, REQUEST_CODE_EDIT_VIDEO); + } else { + 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) { + if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED + && ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) == PackageManager.PERMISSION_GRANTED) { + mMediaStoreCompat.dispatchCaptureIntent(this, REQUEST_CODE_CAPTURE); +// startActivityForResult(new Intent(this, CameraActivity.class) +// .putExtra(ConstantValue.KEY_TYPE, mSpec.type), REQUEST_CODE_CAPTURE); + } else { + ActivityCompat.requestPermissions(this, new String[]{ + Manifest.permission.WRITE_EXTERNAL_STORAGE, + Manifest.permission.RECORD_AUDIO}, 10); + } +// } + } + + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults); + if (grantResults[0] == PackageManager.PERMISSION_GRANTED /*&& mMediaStoreCompat != null*/ + && grantResults[1] == PackageManager.PERMISSION_GRANTED + && grantResults[2] == PackageManager.PERMISSION_GRANTED) { + mMediaStoreCompat.dispatchCaptureIntent(this, REQUEST_CODE_CAPTURE); +// startActivityForResult(new Intent(this, CameraActivity.class) +// .putExtra(ConstantValue.KEY_TYPE, mSpec.type), REQUEST_CODE_CAPTURE); + } + } +} diff --git a/app/src/module_album/java/com/example/matisse/widget/ConfirmPickView.java b/app/src/module_album/java/com/example/matisse/widget/ConfirmPickView.java new file mode 100644 index 0000000..7bb0fb0 --- /dev/null +++ b/app/src/module_album/java/com/example/matisse/widget/ConfirmPickView.java @@ -0,0 +1,60 @@ +package com.example.matisse.widget; + + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.Nullable; + +import com.chwl.app.R; +import com.chwl.app.ui.widget.magicindicator.buildins.UIUtil; + +/** + * create by lvzebiao @2019/12/2 + */ +public class ConfirmPickView extends LinearLayout { + + private TextView tvConfirmText; + + private TextView tvCharText; + + public ConfirmPickView(Context context) { + this(context, null); + } + + public ConfirmPickView(Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); + } + + public ConfirmPickView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + inflate(context, R.layout.layout_album_pick, this); + tvConfirmText = findViewById(R.id.tv_confirm_text); + tvCharText = findViewById(R.id.tv_char_text); + setBackgroundResource(R.drawable.selector_dy_send_btn); + } + + public void setText(String confirmText, int pickCount) { + tvConfirmText.setText(confirmText); + int width = UIUtil.dip2px(getContext(), 55); + if (pickCount > 0) { + width = UIUtil.dip2px(getContext(), 65); + tvCharText.setText("(" + pickCount + ")"); + setEnabled(true); + } else { + tvCharText.setText(""); + setEnabled(false); + } + tvCharText.invalidate(); + if (getLayoutParams() instanceof MarginLayoutParams) { + MarginLayoutParams params = (MarginLayoutParams) getLayoutParams(); + if (params.width != width) { + params.width = width; + setLayoutParams(params); + } + } + } + +} diff --git a/app/src/module_album/res/color/dracula_bottom_toolbar_apply.xml b/app/src/module_album/res/color/dracula_bottom_toolbar_apply.xml new file mode 100644 index 0000000..84e13af --- /dev/null +++ b/app/src/module_album/res/color/dracula_bottom_toolbar_apply.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/color/dracula_bottom_toolbar_preview.xml b/app/src/module_album/res/color/dracula_bottom_toolbar_preview.xml new file mode 100644 index 0000000..56e04b7 --- /dev/null +++ b/app/src/module_album/res/color/dracula_bottom_toolbar_preview.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/color/dracula_preview_bottom_toolbar_apply.xml b/app/src/module_album/res/color/dracula_preview_bottom_toolbar_apply.xml new file mode 100644 index 0000000..9920c27 --- /dev/null +++ b/app/src/module_album/res/color/dracula_preview_bottom_toolbar_apply.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/color/zhihu_bottom_toolbar_apply.xml b/app/src/module_album/res/color/zhihu_bottom_toolbar_apply.xml new file mode 100644 index 0000000..d12c968 --- /dev/null +++ b/app/src/module_album/res/color/zhihu_bottom_toolbar_apply.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/color/zhihu_bottom_toolbar_preview.xml b/app/src/module_album/res/color/zhihu_bottom_toolbar_preview.xml new file mode 100644 index 0000000..fef8d16 --- /dev/null +++ b/app/src/module_album/res/color/zhihu_bottom_toolbar_preview.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/color/zhihu_preview_bottom_toolbar_apply.xml b/app/src/module_album/res/color/zhihu_preview_bottom_toolbar_apply.xml new file mode 100644 index 0000000..0bec454 --- /dev/null +++ b/app/src/module_album/res/color/zhihu_preview_bottom_toolbar_apply.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/module_album/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.webp b/app/src/module_album/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.webp new file mode 100644 index 0000000..3d30294 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_arrow_drop_down_white_24dp.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_check_white_18dp.webp b/app/src/module_album/res/drawable-hdpi/ic_check_white_18dp.webp new file mode 100644 index 0000000..812ba86 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_check_white_18dp.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_empty_dracula.webp b/app/src/module_album/res/drawable-hdpi/ic_empty_dracula.webp new file mode 100644 index 0000000..c9565c5 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_empty_dracula.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_empty_zhihu.webp b/app/src/module_album/res/drawable-hdpi/ic_empty_zhihu.webp new file mode 100644 index 0000000..6378836 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_empty_zhihu.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_gif.webp b/app/src/module_album/res/drawable-hdpi/ic_gif.webp new file mode 100644 index 0000000..d57b3cc Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_gif.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_play_circle_outline_white_48dp.webp b/app/src/module_album/res/drawable-hdpi/ic_play_circle_outline_white_48dp.webp new file mode 100644 index 0000000..1cb2c51 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_play_circle_outline_white_48dp.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_preview_radio_off.webp b/app/src/module_album/res/drawable-hdpi/ic_preview_radio_off.webp new file mode 100644 index 0000000..c470487 Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_preview_radio_off.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/ic_preview_radio_on.webp b/app/src/module_album/res/drawable-hdpi/ic_preview_radio_on.webp new file mode 100644 index 0000000..4bfd9bd Binary files /dev/null and b/app/src/module_album/res/drawable-hdpi/ic_preview_radio_on.webp differ diff --git a/app/src/module_album/res/drawable-hdpi/shape_trans_radius_20.xml b/app/src/module_album/res/drawable-hdpi/shape_trans_radius_20.xml new file mode 100644 index 0000000..54d36e2 --- /dev/null +++ b/app/src/module_album/res/drawable-hdpi/shape_trans_radius_20.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.webp b/app/src/module_album/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.webp new file mode 100644 index 0000000..405034b Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_arrow_drop_down_white_24dp.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_camera_matisse.webp b/app/src/module_album/res/drawable-xhdpi/ic_camera_matisse.webp new file mode 100644 index 0000000..f784ac9 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_camera_matisse.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_check_white_18dp.webp b/app/src/module_album/res/drawable-xhdpi/ic_check_white_18dp.webp new file mode 100644 index 0000000..ce23aff Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_check_white_18dp.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_close_black.webp b/app/src/module_album/res/drawable-xhdpi/ic_close_black.webp new file mode 100644 index 0000000..6c03456 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_close_black.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_empty_dracula.webp b/app/src/module_album/res/drawable-xhdpi/ic_empty_dracula.webp new file mode 100644 index 0000000..1de006b Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_empty_dracula.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_empty_zhihu.webp b/app/src/module_album/res/drawable-xhdpi/ic_empty_zhihu.webp new file mode 100644 index 0000000..cf87677 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_empty_zhihu.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_gif.webp b/app/src/module_album/res/drawable-xhdpi/ic_gif.webp new file mode 100644 index 0000000..f8696f9 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_gif.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.webp b/app/src/module_album/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.webp new file mode 100644 index 0000000..65628f8 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_play_circle_outline_white_48dp.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_off.webp b/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_off.webp new file mode 100644 index 0000000..9f40b48 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_off.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_on.webp b/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_on.webp new file mode 100644 index 0000000..f43238f Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_preview_radio_on.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_tick_black.webp b/app/src/module_album/res/drawable-xhdpi/ic_tick_black.webp new file mode 100644 index 0000000..da75af1 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_tick_black.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/ic_tick_disabled.webp b/app/src/module_album/res/drawable-xhdpi/ic_tick_disabled.webp new file mode 100644 index 0000000..e69a1cd Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/ic_tick_disabled.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/icon_album_back.webp b/app/src/module_album/res/drawable-xhdpi/icon_album_back.webp new file mode 100644 index 0000000..280c6a4 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/icon_album_back.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/icon_circle_check_false.webp b/app/src/module_album/res/drawable-xhdpi/icon_circle_check_false.webp new file mode 100644 index 0000000..5733f14 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/icon_circle_check_false.webp differ diff --git a/app/src/module_album/res/drawable-xhdpi/icon_circle_check_true.webp b/app/src/module_album/res/drawable-xhdpi/icon_circle_check_true.webp new file mode 100644 index 0000000..7e35c13 Binary files /dev/null and b/app/src/module_album/res/drawable-xhdpi/icon_circle_check_true.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.webp b/app/src/module_album/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.webp new file mode 100644 index 0000000..c04a244 Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_arrow_drop_down_white_24dp.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_check_white_18dp.webp b/app/src/module_album/res/drawable-xxhdpi/ic_check_white_18dp.webp new file mode 100644 index 0000000..38b1a50 Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_check_white_18dp.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_empty_dracula.webp b/app/src/module_album/res/drawable-xxhdpi/ic_empty_dracula.webp new file mode 100644 index 0000000..0ebadb5 Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_empty_dracula.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_empty_zhihu.webp b/app/src/module_album/res/drawable-xxhdpi/ic_empty_zhihu.webp new file mode 100644 index 0000000..e57b13b Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_empty_zhihu.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_gif.webp b/app/src/module_album/res/drawable-xxhdpi/ic_gif.webp new file mode 100644 index 0000000..c4428d0 Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_gif.webp differ diff --git a/app/src/module_album/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.webp b/app/src/module_album/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.webp new file mode 100644 index 0000000..6273198 Binary files /dev/null and b/app/src/module_album/res/drawable-xxhdpi/ic_play_circle_outline_white_48dp.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.webp new file mode 100644 index 0000000..c9b6a78 Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_arrow_drop_down_white_24dp.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_check_white_18dp.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_check_white_18dp.webp new file mode 100644 index 0000000..32fc824 Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_check_white_18dp.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_empty_dracula.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_empty_dracula.webp new file mode 100644 index 0000000..33ae2cc Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_empty_dracula.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_empty_zhihu.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_empty_zhihu.webp new file mode 100644 index 0000000..f8018db Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_empty_zhihu.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_gif.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_gif.webp new file mode 100644 index 0000000..dc11bb4 Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_gif.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.webp new file mode 100644 index 0000000..4fe62d3 Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_photo_camera_white_24dp.webp differ diff --git a/app/src/module_album/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.webp b/app/src/module_album/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.webp new file mode 100644 index 0000000..7d76852 Binary files /dev/null and b/app/src/module_album/res/drawable-xxxhdpi/ic_play_circle_outline_white_48dp.webp differ diff --git a/app/src/module_album/res/drawable/bg_80000000_8dp.xml b/app/src/module_album/res/drawable/bg_80000000_8dp.xml new file mode 100644 index 0000000..d6a3636 --- /dev/null +++ b/app/src/module_album/res/drawable/bg_80000000_8dp.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/drawable/selector_circle_check.xml b/app/src/module_album/res/drawable/selector_circle_check.xml new file mode 100644 index 0000000..f1c664a --- /dev/null +++ b/app/src/module_album/res/drawable/selector_circle_check.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/drawable/tick_selector.xml b/app/src/module_album/res/drawable/tick_selector.xml new file mode 100644 index 0000000..fbdb853 --- /dev/null +++ b/app/src/module_album/res/drawable/tick_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/activity_matisse.xml b/app/src/module_album/res/layout/activity_matisse.xml new file mode 100644 index 0000000..364d01f --- /dev/null +++ b/app/src/module_album/res/layout/activity_matisse.xml @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/activity_media_preview.xml b/app/src/module_album/res/layout/activity_media_preview.xml new file mode 100644 index 0000000..e6ae44a --- /dev/null +++ b/app/src/module_album/res/layout/activity_media_preview.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/album_list_item.xml b/app/src/module_album/res/layout/album_list_item.xml new file mode 100644 index 0000000..e37a840 --- /dev/null +++ b/app/src/module_album/res/layout/album_list_item.xml @@ -0,0 +1,52 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/fragment_media_selection.xml b/app/src/module_album/res/layout/fragment_media_selection.xml new file mode 100644 index 0000000..79393ee --- /dev/null +++ b/app/src/module_album/res/layout/fragment_media_selection.xml @@ -0,0 +1,31 @@ + + + + + + + diff --git a/app/src/module_album/res/layout/fragment_preview_item.xml b/app/src/module_album/res/layout/fragment_preview_item.xml new file mode 100644 index 0000000..2cfc58d --- /dev/null +++ b/app/src/module_album/res/layout/fragment_preview_item.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/app/src/module_album/res/layout/layout_album_pick.xml b/app/src/module_album/res/layout/layout_album_pick.xml new file mode 100644 index 0000000..ae624aa --- /dev/null +++ b/app/src/module_album/res/layout/layout_album_pick.xml @@ -0,0 +1,29 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/media_grid_content.xml b/app/src/module_album/res/layout/media_grid_content.xml new file mode 100644 index 0000000..aaefa2b --- /dev/null +++ b/app/src/module_album/res/layout/media_grid_content.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/media_grid_item.xml b/app/src/module_album/res/layout/media_grid_item.xml new file mode 100644 index 0000000..2bb1230 --- /dev/null +++ b/app/src/module_album/res/layout/media_grid_item.xml @@ -0,0 +1,21 @@ + + + \ No newline at end of file diff --git a/app/src/module_album/res/layout/photo_capture_item.xml b/app/src/module_album/res/layout/photo_capture_item.xml new file mode 100644 index 0000000..ecd8b3c --- /dev/null +++ b/app/src/module_album/res/layout/photo_capture_item.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/values-ar/strings.xml b/app/src/module_album/res/values-ar/strings.xml new file mode 100644 index 0000000..4471a3f --- /dev/null +++ b/app/src/module_album/res/values-ar/strings.xml @@ -0,0 +1,46 @@ + + + + + + كل الوسائط + + معاينة + تطبيق + تطبيق (%d) + رجوع + الكاميرا + لا يوجد وسائط حتى الآن + موافق + + لقد وصلت إلى الحد الأقصى للتحديد + يمكنك اختيار ما يصل إلى %d ملف وسائط فقط + جودة منخفضة + جودة عالية + نوع الملف غير مدعوم + لا يمكن اختيار الصور ومقاطع الفيديو في نفس الوقت + لم يتم العثور على تطبيق يدعم معاينة الفيديو + لا يمكن اختيار الصور التي تزيد عن %d ميجابايت + تجاوز عدد %d من الصور بحجم %d ميجابايت. سيتم إلغاء التحديد الأصلي + الأصلي + تأكيد + تأكيد (%d) + تحميل + لفة الكاميرا + + تم (%d) + \ No newline at end of file diff --git a/app/src/module_album/res/values-zh-rTW/strings.xml b/app/src/module_album/res/values-zh-rTW/strings.xml new file mode 100644 index 0000000..9e5635d --- /dev/null +++ b/app/src/module_album/res/values-zh-rTW/strings.xml @@ -0,0 +1,44 @@ + + + + 全部 + + 預覽 + 使用 + 使用(%1$d) + 返回 + 拍一張 + 還沒有圖片或視頻 + 我知道了 + + 您已經達到最大選擇數量 + 最多只能選擇 %1$d 個文件 + 圖片質量太低 + 圖片質量太高 + 不支持的文件類型 + 不能同時選擇圖片和視頻 + 沒有支持視頻預覽的應用 + "該照片大於 %1$d M,無法上傳將取消勾選原圖" + "有 %1$d 張照片大於 %2$d M\n無法上傳,將取消勾選原圖" + 原圖 + 確定 + 確定(%1$d) + 上傳 + 相機膠卷 + + 確定(%d) + \ No newline at end of file diff --git a/app/src/module_album/res/values/attrs.xml b/app/src/module_album/res/values/attrs.xml new file mode 100644 index 0000000..852a3e3 --- /dev/null +++ b/app/src/module_album/res/values/attrs.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/module_album/res/values/colors.xml b/app/src/module_album/res/values/colors.xml new file mode 100644 index 0000000..24ec1f9 --- /dev/null +++ b/app/src/module_album/res/values/colors.xml @@ -0,0 +1,22 @@ + + + + + #CC000000 + #61FFFFFF + + \ No newline at end of file diff --git a/app/src/module_album/res/values/colors_dracula.xml b/app/src/module_album/res/values/colors_dracula.xml new file mode 100644 index 0000000..11bd2b9 --- /dev/null +++ b/app/src/module_album/res/values/colors_dracula.xml @@ -0,0 +1,43 @@ + + + + #263237 + #1D282C + + #34474E + #DEFFFFFF + #89FFFFFF + #455A64 + #4DFFFFFF + + #222222 + #263237 + #FFFFFF + #FFFFFF + + #232E32 + #34474E + + #DEFFFFFF + #4DFFFFFF + #03A9F4 + #4D03A9F4 + + #FFFFFF + #03A9F4 + #4D03A9F4 + \ No newline at end of file diff --git a/app/src/module_album/res/values/colors_zhihu.xml b/app/src/module_album/res/values/colors_zhihu.xml new file mode 100644 index 0000000..6a57a1a --- /dev/null +++ b/app/src/module_album/res/values/colors_zhihu.xml @@ -0,0 +1,45 @@ + + + + #FFFFFF + #000000 + + #FFFFFF + #DE000000 + #999999 + #EAEEF4 + #4D000000 + + #222222 + #39E2C6 + #80000000 + #424242 + + #FFFFFF + #FFFFFF + + #DE000000 + #4D000000 + #0077D9 + #4D0077D9 + + #FFFFFF + #0077D9 + #4D0077D9 + + #808080 + \ No newline at end of file diff --git a/app/src/module_album/res/values/dimens.xml b/app/src/module_album/res/values/dimens.xml new file mode 100644 index 0000000..5ba8206 --- /dev/null +++ b/app/src/module_album/res/values/dimens.xml @@ -0,0 +1,22 @@ + + + + 48dp + 1dp + + 72dp + \ No newline at end of file diff --git a/app/src/module_album/res/values/strings.xml b/app/src/module_album/res/values/strings.xml new file mode 100644 index 0000000..b924768 --- /dev/null +++ b/app/src/module_album/res/values/strings.xml @@ -0,0 +1,46 @@ + + + + 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) + Upload + Camera Roll + + Done(%d) + diff --git a/app/src/module_album/res/values/styles.xml b/app/src/module_album/res/values/styles.xml new file mode 100644 index 0000000..0d9cc13 --- /dev/null +++ b/app/src/module_album/res/values/styles.xml @@ -0,0 +1,88 @@ + + + + + //====================================== Theme Zhihu =========================================== + + + + + + + + //===================================== Theme Dracula ========================================== + + + + + + + + \ No newline at end of file