feat : app/src/module_album

This commit is contained in:
eggmanQQQ2
2025-07-07 10:37:05 +08:00
parent eac08699e9
commit f6d57e773f
111 changed files with 7198 additions and 0 deletions

View File

@@ -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<Activity> mContext;
private final WeakReference<Fragment> 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.
* <p>
* 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.
* <p>
* 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<Uri> 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<CustomItem> 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.
* <p>
* 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<MimeType> mimeTypes) {
return this.choose(mimeTypes, true);
}
/**
* MIME types the selection constrains on.
* <p>
* 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<MimeType> mimeTypes, boolean mediaTypeExclusive) {
return new SelectionCreator(this, mimeTypes, mediaTypeExclusive);
}
@Nullable
Activity getActivity() {
return mContext.get();
}
@Nullable
Fragment getFragment() {
return mFragment != null ? mFragment.get() : null;
}
}

View File

@@ -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 &quot;AS IS&quot; 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.
* <p>
* 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<String> mExtensions;
MimeType(String mimeTypeName, Set<String> extensions) {
mMimeTypeName = mimeTypeName;
mExtensions = extensions;
}
public static Set<MimeType> ofAll() {
return EnumSet.allOf(MimeType.class);
}
public static Set<MimeType> of(MimeType type, MimeType... rest) {
return EnumSet.of(type, rest);
}
public static Set<MimeType> ofImage() {
return EnumSet.of(JPEG, PNG, GIF, BMP, WEBP);
}
public static Set<MimeType> ofGif() {
return ofImage(true);
}
public static Set<MimeType> ofImage(boolean onlyGif) {
return EnumSet.of(GIF);
}
public static Set<MimeType> 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<String> 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;
}
}

View File

@@ -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 &quot;AS IS&quot; 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<MimeType> 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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;
}
}

View File

@@ -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();
}

View File

@@ -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;
}
}

View File

@@ -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<MimeType> 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;
}
}

View File

@@ -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 &quot;AS IS&quot; 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<Album> CREATOR = new Creator<Album>() {
@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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<CustomItem> CREATOR = new Creator<CustomItem>() {
@Override
public CustomItem createFromParcel(Parcel source) {
return new CustomItem(source);
}
@Override
public CustomItem[] newArray(int size) {
return new CustomItem[size];
}
};
}

View File

@@ -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 &quot;AS IS&quot; 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;
}
}
}

View File

@@ -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 &quot;AS IS&quot; 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<Item> CREATOR = new Creator<Item>() {
@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;
}
}

View File

@@ -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 &quot;AS IS&quot; 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<MimeType> 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<Filter> 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();
}
}

View File

@@ -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 &quot;AS IS&quot; 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<Long, Long> 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<Long> 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;
}
}

View File

@@ -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 &quot;AS IS&quot; 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
}
}

View File

@@ -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 &quot;AS IS&quot; 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<Cursor> {
private static final int LOADER_ID = 1;
private static final String STATE_CURRENT_SELECTION = "state_current_selection";
private WeakReference<Context> mContext;
private LoaderManager mLoaderManager;
private AlbumCallbacks mCallbacks;
private int mCurrentSelection;
private boolean mLoadFinished;
@Override
public Loader<Cursor> 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<Cursor> loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
if (!mLoadFinished) {
mLoadFinished = true;
mCallbacks.onAlbumLoad(data);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumReset();
}
public void onCreate(FragmentActivity activity, AlbumCallbacks callbacks) {
mContext = new WeakReference<Context>(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();
}
}

View File

@@ -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 &quot;AS IS&quot; 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<Cursor> {
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<Context> mContext;
private LoaderManager mLoaderManager;
private AlbumMediaCallbacks mCallbacks;
@Override
public Loader<Cursor> 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<Cursor> loader, Cursor data) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumMediaLoad(data);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
Context context = mContext.get();
if (context == null) {
return;
}
mCallbacks.onAlbumMediaReset();
}
public void onCreate(@NonNull FragmentActivity context, @NonNull AlbumMediaCallbacks callbacks) {
mContext = new WeakReference<Context>(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();
}
}

View File

@@ -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<Item> 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<Item> saved = bundle.getParcelableArrayList(STATE_SELECTION);
mItems = new LinkedHashSet<>(saved);
mCollectionType = bundle.getInt(STATE_COLLECTION_TYPE, COLLECTION_UNDEFINED);
}
}
public void setDefaultSelection(List<Item> 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<Item> items, int collectionType) {
if (items.size() == 0) {
mCollectionType = COLLECTION_UNDEFINED;
} else {
mCollectionType = collectionType;
}
mItems.clear();
mItems.addAll(items);
}
public List<Item> asList() {
return new ArrayList<>(mItems);
}
public List<Uri> asListOfUri() {
List<Uri> uris = new ArrayList<>();
for (Item item : mItems) {
uris.add(item.getContentUri());
}
return uris;
}
public List<String> asListOfString() {
List<String> paths = new ArrayList<>();
for (Item item : mItems) {
paths.add(PathUtils.getPath(mContext, item.getContentUri()));
}
return paths;
}
public List<CustomItem> asListOfCustomItem() {
List<CustomItem> 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;
}
}

View File

@@ -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<Item> 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() {
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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<Item> 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));
}
}

View File

@@ -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<RecyclerView.ViewHolder> 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);
}
}
}

View File

@@ -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());
}
}

View File

@@ -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<Item> 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<Item> items) {
mItems.addAll(items);
}
interface OnPrimaryItemSetListener {
void onPrimaryItemSet(int position);
}
}

View File

@@ -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<VH extends RecyclerView.ViewHolder> extends
RecyclerView.Adapter<VH> {
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();
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}
}

View File

@@ -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
}
}
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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;
}
}
}

View File

@@ -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<Activity> mContext;
private final WeakReference<Fragment> 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<ResolveInfo> 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;
}
}

View File

@@ -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());
}
}

View File

@@ -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 &quot;AS IS&quot; 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -0,0 +1,11 @@
package com.example.matisse.listener;
/**
* PreViewItemFragment 和 BasePreViewActivity 通信的接口 ,为了方便拿到 ImageViewTouch 的点击事件
*/
public interface OnFragmentInteractionListener {
/**
* ImageViewTouch 被点击了
*/
void onClick();
}

View File

@@ -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<Uri> uriList, @NonNull List<String> pathList);
}

View File

@@ -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<Item> 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<Uri> selectedUris = new ArrayList<>();
ArrayList<CustomItem> 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<Uri> selected = new ArrayList<>();
selected.add(contentUri);
ArrayList<CustomItem> 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<Item> selected = resultBundle.getParcelableArrayList(SelectedItemCollection.STATE_SELECTION);
ArrayList<CustomItem> 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<Uri> selectedUris = (ArrayList<Uri>) mSelectedCollection.asListOfUri();
result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selectedUris);
ArrayList<CustomItem> selectedPaths = (ArrayList<CustomItem>) 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);
}
}
}

View File

@@ -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);
}
}
}
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/dracula_bottom_toolbar_apply_text_disable"
android:state_enabled="false"/>
<item android:color="@color/dracula_bottom_toolbar_apply_text"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/dracula_bottom_toolbar_preview_text_disable"
android:state_enabled="false"/>
<item android:color="@color/dracula_bottom_toolbar_preview_text"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/dracula_preview_bottom_toolbar_apply_text_disable"
android:state_enabled="false"/>
<item android:color="@color/dracula_preview_bottom_toolbar_apply_text"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/zhihu_bottom_toolbar_apply_text_disable"
android:state_enabled="false"/>
<item android:color="@color/zhihu_bottom_toolbar_apply_text"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/zhihu_bottom_toolbar_preview_text_disable"
android:state_enabled="false"/>
<item android:color="@color/zhihu_bottom_toolbar_preview_text"/>
</selector>

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@color/zhihu_preview_bottom_toolbar_apply_text_disable"
android:state_enabled="false"/>
<item android:color="@color/zhihu_preview_bottom_toolbar_apply_text"/>
</selector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 635 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 873 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#30ffffff" />
<corners android:radius="20dp" />
</shape>

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 679 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 581 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 681 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 521 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 337 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#80000000" />
<corners android:radius="8dp" />
</shape>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/icon_circle_check_true" android:state_checked="true" />
<item android:drawable="@drawable/icon_circle_check_false" android:state_checked="false" />
</selector>

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_tick_disabled" android:state_enabled="false" />
<item android:drawable="@drawable/ic_tick_black" />
</selector>

View File

@@ -0,0 +1,206 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?colorPrimary"
android:elevation="4dp"
android:theme="?toolbar"
tools:targetApi="lollipop">
<TextView
android:id="@+id/selected_album"
android:layout_width="wrap_content"
android:layout_height="?actionBarSize"
android:drawableEnd="@drawable/ic_arrow_drop_down_white_24dp"
android:foreground="?selectableItemBackground"
android:gravity="center"
android:textColor="?attr/album.element.color"
android:textSize="18sp" />
</androidx.appcompat.widget.Toolbar>
<LinearLayout
android:id="@+id/ll_toolbar"
android:layout_width="match_parent"
android:layout_height="44dp"
android:gravity="center_vertical"
android:visibility="gone">
<ImageView
android:id="@+id/iv_close"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:src="@drawable/ic_close_black" />
<TextView
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="center"
android:text="@string/camera_roll"
android:textColor="#222222"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/iv_tick"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="15dp"
android:visibility="gone"
android:background="@drawable/tick_selector"
android:enabled="false" />
</LinearLayout>
<FrameLayout
android:id="@+id/bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:background="?attr/bottomToolbar.bg"
android:elevation="4dp"
tools:visibility="invisible"
tools:targetApi="lollipop">
<TextView
android:id="@+id/button_preview"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="start"
android:foreground="?selectableItemBackground"
android:padding="16dp"
android:text="@string/button_preview"
android:textColor="?attr/bottomToolbar.preview.textColor"
android:textSize="16sp" />
<LinearLayout
android:id="@+id/originalLayout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:foreground="?selectableItemBackground"
android:orientation="horizontal"
android:padding="16dp"
android:visibility="visible"
tools:showIn="@layout/activity_matisse">
<com.example.matisse.internal.ui.widget.CheckRadioView
android:id="@+id/original"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_preview_radio_off" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:enabled="true"
android:paddingStart="4dp"
android:text="@string/button_original"
android:textColor="?attr/bottomToolbar.preview.textColor"
android:textSize="14sp" />
</LinearLayout>
<TextView
android:id="@+id/button_apply"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:foreground="?selectableItemBackground"
android:padding="16dp"
android:textColor="?attr/bottomToolbar.apply.textColor"
android:textSize="16sp" />
</FrameLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/cl_custom_bottom_layout"
android:layout_width="match_parent"
android:layout_alignParentBottom="true"
android:background="@color/white"
android:layout_height="55dp" >
<CheckBox
android:id="@+id/cb_source_image"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:button="@drawable/selector_circle_check"
android:text="@string/layout_activity_matisse_01"
android:gravity="center_vertical"
android:checked="false"
android:paddingTop="5dp"
android:paddingBottom="5dp"
android:textSize="@dimen/sp_15"
app:layout_constraintStart_toStartOf="parent"
android:layout_marginStart="17.5dp"
android:textColor="@color/color_333333"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<com.example.matisse.widget.ConfirmPickView
android:id="@+id/tv_confirm_pick"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_width="55dp"
android:layout_marginEnd="15dp"
android:enabled="false"
android:layout_height="@dimen/dp_27" />
</androidx.constraintlayout.widget.ConstraintLayout>
<FrameLayout
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/cl_custom_bottom_layout"
android:layout_below="@id/toolbar"
android:visibility="gone" />
<FrameLayout
android:id="@+id/empty_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_above="@id/bottom_toolbar"
android:layout_below="@id/toolbar"
android:visibility="gone">
<TextView
android:id="@+id/empty_view_content"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawableTop="?attr/album.emptyView"
android:drawablePadding="8dp"
android:gravity="center"
android:text="@string/empty_text"
android:textColor="?attr/album.emptyView.textColor"
android:textSize="16sp" />
</FrameLayout>
</RelativeLayout>

View File

@@ -0,0 +1,135 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.example.matisse.internal.ui.widget.PreviewViewPager
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"/>
<ImageView
android:id="@+id/iv_close"
android:src="@drawable/icon_album_back"
android:scaleType="centerInside"
android:layout_width="48dp"
android:layout_height="48dp" />
<FrameLayout
android:id="@+id/bottom_toolbar"
android:layout_width="match_parent"
android:layout_height="55dp"
android:layout_alignParentBottom="true"
android:background="@color/white"
>
<TextView
android:id="@+id/button_back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:foreground="?selectableItemBackground"
android:padding="16dp"
android:layout_gravity="start"
android:text="@string/button_back"
android:visibility="gone"
android:textColor="?attr/preview.bottomToolbar.back.textColor"
android:textSize="16sp"/>
<LinearLayout
android:layout_gravity="center"
android:orientation="horizontal"
android:layout_width="wrap_content"
android:layout_height="wrap_content">
<LinearLayout
android:padding="16dp"
android:id="@+id/originalLayout"
android:visibility="gone"
android:orientation="horizontal"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:foreground="?selectableItemBackground"
android:layout_height="wrap_content"
tools:showIn="@layout/activity_matisse">
<com.example.matisse.internal.ui.widget.CheckRadioView
android:id="@+id/original"
android:src="@drawable/ic_preview_radio_off"
android:layout_gravity="center_vertical"
android:tint="#ffffff"
android:layout_width="16dp"
android:layout_height="16dp" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:enabled="true"
android:paddingStart="4dp"
android:text="@string/button_original"
android:textColor="?attr/preview.bottomToolbar.back.textColor"
android:textSize="14sp" />
</LinearLayout>
<TextView
android:id="@+id/size"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:textColor="@color/preview_bottom_size"
android:textSize="16sp"
android:visibility="gone"/>
</LinearLayout>
<com.example.matisse.widget.ConfirmPickView
android:id="@+id/button_apply"
android:layout_gravity="center_vertical|end"
android:layout_width="55dp"
android:layout_marginEnd="15dp"
android:gravity="center"
android:enabled="false"
android:layout_height="@dimen/dp_27"/>
</FrameLayout>
<FrameLayout
android:id="@+id/top_toolbar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_margin="8dp"
android:fitsSystemWindows="true">
<com.example.matisse.internal.ui.widget.CheckView
android:id="@+id/check_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"/>
</FrameLayout>
</RelativeLayout>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/album_item_height">
<com.example.matisse.internal.ui.widget.RoundedRectangleImageView
android:id="@+id/album_cover"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_centerVertical="true"
android:layout_marginStart="16dp" />
<TextView
android:id="@+id/album_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignTop="@id/album_cover"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?attr/album.dropdown.title.color"
android:textSize="16sp"
android:layout_marginStart="8dp"
android:layout_toEndOf="@id/album_cover" />
<TextView
android:id="@+id/album_media_count"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/album_name"
android:layout_marginTop="4dp"
android:ellipsize="end"
android:maxLines="1"
android:textColor="?album.dropdown.count.color"
android:textSize="14sp"
android:layout_alignStart="@id/album_name"
android:layout_toEndOf="@id/album_cover" />
</RelativeLayout>

View File

@@ -0,0 +1,31 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="?attr/page.bg">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingBottom="@dimen/media_grid_spacing"
android:paddingTop="@dimen/media_grid_spacing"/>
</FrameLayout>

View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<it.sephiroth.android.library.imagezoom.ImageViewTouch
android:id="@+id/image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<ImageView
android:id="@+id/video_play_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:src="@drawable/ic_play_circle_outline_white_48dp"/>
</FrameLayout>

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="horizontal"
tools:background="@drawable/selector_dy_send_btn"
tools:layout_height="@dimen/dp_30"
tools:layout_width="@dimen/dp_60">
<TextView
android:id="@+id/tv_confirm_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/layout_layout_album_pick_01"
android:textColor="@color/color_FEFEFE"
android:textSize="@dimen/sp_14"
android:textStyle="bold" />
<com.chwl.app.ui.widget.CharAlignTextView
android:id="@+id/tv_char_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textColor="@color/color_FEFEFE"
android:textSize="@dimen/sp_14"
android:textStyle="bold" />
</LinearLayout>

View File

@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?><!--
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.
-->
<merge xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/media_thumbnail"
android:layout_width="match_parent"
android:layout_height="match_parent" />
<com.example.matisse.internal.ui.widget.CheckView
android:id="@+id/check_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="top|right" />
<ImageView
android:id="@+id/gif"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end|bottom"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:src="@drawable/ic_gif"
android:visibility="gone" />
<TextView
android:id="@+id/video_duration"
android:layout_width="40dp"
android:layout_height="15dp"
android:layout_gravity="bottom"
android:layout_marginStart="5dp"
android:layout_marginBottom="5dp"
android:background="@drawable/bg_80000000_8dp"
android:gravity="center"
android:textColor="@android:color/white"
android:textSize="11sp"
tools:text="01:00" />
</merge>

Some files were not shown because too many files have changed in this diff Show More