From 7c44a3383a829cae36951709b93762de93ccadad Mon Sep 17 00:00:00 2001 From: ngocsontr Date: Wed, 30 Oct 2019 14:09:22 +0700 Subject: [PATCH] [Add] Video Editor: Crop & Trim --- build.gradle | 6 +- gradle.properties | 2 + gradle/wrapper/gradle-wrapper.properties | 4 +- matisse/build.gradle | 16 +- matisse/src/main/AndroidManifest.xml | 1 + .../main/java/com/zhihu/matisse/Matisse.java | 5 +- .../main/java/com/zhihu/matisse/MimeType.java | 3 +- .../com/zhihu/matisse/SelectionCreator.java | 31 ++- .../zhihu/matisse/internal/entity/Album.java | 3 +- .../internal/entity/IncapableCause.java | 5 +- .../zhihu/matisse/internal/entity/Item.java | 13 +- .../internal/entity/SelectionSpec.java | 7 +- .../matisse/internal/loader/AlbumLoader.java | 3 +- .../internal/loader/AlbumMediaLoader.java | 3 +- .../internal/model/AlbumCollection.java | 7 +- .../internal/model/AlbumMediaCollection.java | 11 +- .../model/SelectedItemCollection.java | 53 +++- .../internal/ui/AlbumPreviewActivity.java | 3 +- .../internal/ui/BasePreviewActivity.java | 13 +- .../internal/ui/MediaSelectionFragment.java | 143 ++++++++-- .../internal/ui/PreviewItemFragment.java | 5 +- .../internal/ui/SelectedPreviewActivity.java | 3 +- .../ui/adapter/AlbumMediaAdapter.java | 46 ++-- .../ui/adapter/PreviewPagerAdapter.java | 7 +- .../ui/adapter/RecyclerViewCursorAdapter.java | 3 +- .../internal/ui/widget/AlbumsSpinner.java | 5 +- .../internal/ui/widget/CheckRadioView.java | 5 +- .../matisse/internal/ui/widget/CheckView.java | 3 +- .../internal/ui/widget/IncapableDialog.java | 7 +- .../matisse/internal/ui/widget/MediaGrid.java | 7 +- .../internal/ui/widget/MediaGridInset.java | 3 +- .../internal/ui/widget/PreviewViewPager.java | 3 +- .../ui/widget/RoundedRectangleImageView.java | 3 +- .../ui/widget/SquareAppBarLayout.java | 4 +- .../internal/utils/MediaStoreCompat.java | 7 +- .../zhihu/matisse/internal/utils/UIUtils.java | 32 --- .../com/zhihu/matisse/internal/utils/Utils.kt | 98 +++++++ .../matisse/listener/OnSelectedListener.java | 3 +- .../com/zhihu/matisse/ui/FilterActivity.kt | 8 +- .../com/zhihu/matisse/ui/MatisseActivity.java | 106 +++++--- .../com/zhihu/matisse/ui/ThumbnailsAdapter.kt | 3 +- .../zhihu/matisse/ui/VideoEditorActivity.kt | 160 ++++++++++++ .../matisse/ui/widget/AllGestureDetector.java | 123 +++++++++ .../ui/widget/DragGestureDetector.java | 139 ++++++++++ .../ui/widget/GesturePlayerTextureView.java | 78 ++++++ .../ui/widget/PinchGestureDetector.java | 112 ++++++++ .../matisse/ui/widget/PlayerTextureView.kt | 156 +++++++++++ .../ui/widget/RotateGestureDetector.java | 120 +++++++++ .../zhihu/matisse/ui/widget/SceneCropColor.kt | 19 ++ .../matisse/ui/widget/TimeSelectorView.kt | 245 ++++++++++++++++++ .../main/res/drawable/icon_time_selector.xml | 5 + .../drawable/icon_time_selector_disabled.xml | 12 + .../drawable/icon_time_selector_normal.xml | 26 ++ .../res/drawable/icon_time_selector_size.xml | 7 + .../src/main/res/layout/activity_matisse.xml | 4 +- .../main/res/layout/activity_post_filter.xml | 11 +- .../main/res/layout/activity_video_trim.xml | 83 ++++++ .../res/layout/fragment_media_selection.xml | 3 +- matisse/src/main/res/values/colors.xml | 13 +- matisse/src/main/res/values/dimens.xml | 4 + matisse/src/main/res/values/strings.xml | 9 +- sample/build.gradle | 10 +- sample/src/main/AndroidManifest.xml | 8 +- .../zhihu/matisse/sample/SampleActivity.java | 17 +- sample/src/main/res/layout/activity_main.xml | 4 +- 65 files changed, 1833 insertions(+), 228 deletions(-) delete mode 100644 matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/internal/utils/Utils.kt create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/VideoEditorActivity.kt create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/AllGestureDetector.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/DragGestureDetector.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/GesturePlayerTextureView.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/PinchGestureDetector.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/PlayerTextureView.kt create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/RotateGestureDetector.java create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/SceneCropColor.kt create mode 100644 matisse/src/main/java/com/zhihu/matisse/ui/widget/TimeSelectorView.kt create mode 100644 matisse/src/main/res/drawable/icon_time_selector.xml create mode 100644 matisse/src/main/res/drawable/icon_time_selector_disabled.xml create mode 100644 matisse/src/main/res/drawable/icon_time_selector_normal.xml create mode 100644 matisse/src/main/res/drawable/icon_time_selector_size.xml create mode 100644 matisse/src/main/res/layout/activity_video_trim.xml diff --git a/build.gradle b/build.gradle index bebd3fbdb..e7e192d7e 100644 --- a/build.gradle +++ b/build.gradle @@ -21,9 +21,9 @@ buildscript { google() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.4' - classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.40' - classpath 'com.novoda:bintray-release:0.8.1' + classpath 'com.android.tools.build:gradle:3.5.1' + classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50' + classpath 'com.novoda:bintray-release:0.9.1' } } diff --git a/gradle.properties b/gradle.properties index aac7c9b46..9e6fce102 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,8 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +android.enableJetifier=true +android.useAndroidX=true org.gradle.jvmargs=-Xmx1536m # When configured, Gradle will run in incubating parallel mode. diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index cb65260eb..558c30580 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jun 27 11:32:18 CST 2018 +#Tue Oct 29 13:40:36 ICT 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.4-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.4.1-all.zip diff --git a/matisse/build.gradle b/matisse/build.gradle index ca9b2f92b..298f18a6f 100644 --- a/matisse/build.gradle +++ b/matisse/build.gradle @@ -29,6 +29,10 @@ android { lintOptions { abortOnError true } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } } ext.supportLibVersion = '28.0.0' @@ -36,15 +40,17 @@ ext.supportLibVersion = '28.0.0' dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation "com.android.support:support-v4:${supportLibVersion}" - implementation "com.android.support:appcompat-v7:${supportLibVersion}" - implementation "com.android.support:support-annotations:${supportLibVersion}" - implementation "com.android.support:recyclerview-v7:${supportLibVersion}" + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.annotation:annotation:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.0.0' implementation 'it.sephiroth.android.library.imagezoom:library:1.0.4' implementation 'com.github.yalantis:ucrop:2.2.3-native' implementation 'info.androidhive:imagefilters:1.0.7' - implementation 'com.github.bumptech.glide:glide:4.9.0' + implementation 'com.github.bumptech.glide:glide:4.10.0' compileOnly 'com.squareup.picasso:picasso:2.5.2' + implementation 'com.github.MasayukiSuda:Mp4Composer-android:v0.3.3' + implementation 'com.google.android.exoplayer:exoplayer:2.10.4' } // jcenter configuration for novoda's bintray-release diff --git a/matisse/src/main/AndroidManifest.xml b/matisse/src/main/AndroidManifest.xml index a06ca9d4f..e08d1df18 100644 --- a/matisse/src/main/AndroidManifest.xml +++ b/matisse/src/main/AndroidManifest.xml @@ -25,5 +25,6 @@ + \ No newline at end of file diff --git a/matisse/src/main/java/com/zhihu/matisse/Matisse.java b/matisse/src/main/java/com/zhihu/matisse/Matisse.java index 74763215d..6831a4de8 100644 --- a/matisse/src/main/java/com/zhihu/matisse/Matisse.java +++ b/matisse/src/main/java/com/zhihu/matisse/Matisse.java @@ -18,8 +18,9 @@ import android.app.Activity; import android.content.Intent; import android.net.Uri; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; + +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; import com.zhihu.matisse.ui.MatisseActivity; diff --git a/matisse/src/main/java/com/zhihu/matisse/MimeType.java b/matisse/src/main/java/com/zhihu/matisse/MimeType.java index 4b6c39cad..69bdc3c62 100644 --- a/matisse/src/main/java/com/zhihu/matisse/MimeType.java +++ b/matisse/src/main/java/com/zhihu/matisse/MimeType.java @@ -19,9 +19,10 @@ import android.content.ContentResolver; import android.net.Uri; import android.text.TextUtils; -import android.support.v4.util.ArraySet; import android.webkit.MimeTypeMap; +import androidx.collection.ArraySet; + import com.zhihu.matisse.internal.utils.PhotoMetadataUtils; import java.util.Arrays; diff --git a/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java b/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java index c4c835c02..0cf67ac56 100644 --- a/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java +++ b/matisse/src/main/java/com/zhihu/matisse/SelectionCreator.java @@ -19,12 +19,13 @@ import android.app.Activity; import android.content.Intent; import android.os.Build; -import android.support.annotation.IntDef; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.annotation.RequiresApi; -import android.support.annotation.StyleRes; -import android.support.v4.app.Fragment; + +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.annotation.StyleRes; +import androidx.fragment.app.Fragment; import com.zhihu.matisse.engine.ImageEngine; import com.zhihu.matisse.filter.Filter; @@ -257,9 +258,20 @@ public SelectionCreator filterEnable(boolean enable) { return this; } + /** + * Enable/disable the trim Video. + * + * @param enable is enable. Default value is true + * @return {@link SelectionCreator} for fluent API. + */ + public SelectionCreator trimVideoEnable(boolean enable) { + mSelectionSpec.hasTrimVideo = enable; + return this; + } + /** * Capture strategy provided for the location to save photos including internal and external - * storage and also a authority for {@link android.support.v4.content.FileProvider}. + * 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. @@ -365,6 +377,11 @@ public SelectionCreator setOnCheckedListener(@Nullable OnCheckedListener listene return this; } +// public SelectionCreator showPreview(boolean showPreview) { +// mSelectionSpec.showPreview = showPreview; +// return this; +// } + /** * Start to select media and wait for result. * diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java index 9ebafb8c5..f375c8e39 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Album.java @@ -21,7 +21,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.provider.MediaStore; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import com.zhihu.matisse.R; import com.zhihu.matisse.internal.loader.AlbumLoader; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java index 5c3920693..3fc0b980c 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/IncapableCause.java @@ -16,10 +16,11 @@ package com.zhihu.matisse.internal.entity; import android.content.Context; -import android.support.annotation.IntDef; -import android.support.v4.app.FragmentActivity; import android.widget.Toast; +import androidx.annotation.IntDef; +import androidx.fragment.app.FragmentActivity; + import com.zhihu.matisse.internal.ui.widget.IncapableDialog; import java.lang.annotation.Retention; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java index b9087d7d8..98e67a5e0 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/Item.java @@ -22,7 +22,8 @@ import android.os.Parcel; import android.os.Parcelable; import android.provider.MediaStore; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import com.zhihu.matisse.MimeType; @@ -43,8 +44,7 @@ public Item[] newArray(int size) { public static final String ITEM_DISPLAY_NAME_CAPTURE = "Capture"; public final long id; public final String mimeType; - public final Uri uri; - public Uri uriCrop; + public Uri uri; public final long size; public final long duration; // only for video, in ms @@ -61,7 +61,6 @@ private Item(long id, String mimeType, long size, long duration) { contentUri = MediaStore.Files.getContentUri("external"); } this.uri = ContentUris.withAppendedId(contentUri, id); - this.uriCrop = null; this.size = size; this.duration = duration; } @@ -72,7 +71,6 @@ private Item(Parcel source) { uri = source.readParcelable(Uri.class.getClassLoader()); size = source.readLong(); duration = source.readLong(); - uriCrop = source.readParcelable(Uri.class.getClassLoader()); } public static Item valueOf(Cursor cursor) { @@ -92,7 +90,6 @@ public void writeToParcel(Parcel dest, int flags) { dest.writeLong(id); dest.writeString(mimeType); dest.writeParcelable(uri, 0); - dest.writeParcelable(uriCrop, 0); dest.writeLong(size); dest.writeLong(duration); } @@ -127,10 +124,10 @@ public boolean equals(Object obj) { return id == other.id && (mimeType != null && mimeType.equals(other.mimeType) || (mimeType == null && other.mimeType == null)) - && (uri != null && uri.equals(other.uri) + /*&& (uri != null && uri.equals(other.uri) || (uri == null && other.uri == null)) && size == other.size - && duration == other.duration; + && duration == other.duration*/; } @Override diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java b/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java index 85b77d621..ffc4f6191 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/entity/SelectionSpec.java @@ -17,7 +17,8 @@ package com.zhihu.matisse.internal.entity; import android.content.pm.ActivityInfo; -import android.support.annotation.StyleRes; + +import androidx.annotation.StyleRes; import com.zhihu.matisse.MimeType; import com.zhihu.matisse.R; @@ -56,6 +57,8 @@ public final class SelectionSpec { public int originalMaxSize; public int cropMaxSize; public boolean hasFilter; + public boolean hasTrimVideo; + public boolean showPreview; public OnCheckedListener onCheckedListener; private SelectionSpec() { @@ -94,6 +97,8 @@ private void reset() { originalMaxSize = Integer.MAX_VALUE; cropMaxSize = 4000; hasFilter = true; + hasTrimVideo = true; + showPreview = false; } public boolean singleSelectionModeEnabled() { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java index f328296a9..7bf05db4e 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumLoader.java @@ -22,7 +22,8 @@ import android.database.MergeCursor; import android.net.Uri; import android.provider.MediaStore; -import android.support.v4.content.CursorLoader; + +import androidx.loader.content.CursorLoader; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.SelectionSpec; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java index ea2b2919e..709905be0 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/loader/AlbumMediaLoader.java @@ -22,7 +22,8 @@ import android.database.MergeCursor; import android.net.Uri; import android.provider.MediaStore; -import android.support.v4.content.CursorLoader; + +import androidx.loader.content.CursorLoader; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java index bdfa925e1..7d0b48ec8 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumCollection.java @@ -19,9 +19,10 @@ import android.content.Context; import android.database.Cursor; import android.os.Bundle; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; + +import androidx.fragment.app.FragmentActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import com.zhihu.matisse.internal.loader.AlbumLoader; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java index 90a938ab7..770178e6b 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/AlbumMediaCollection.java @@ -19,11 +19,12 @@ import android.content.Context; import android.database.Cursor; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.FragmentActivity; -import android.support.v4.app.LoaderManager; -import android.support.v4.content.Loader; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.fragment.app.FragmentActivity; +import androidx.loader.app.LoaderManager; +import androidx.loader.content.Loader; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.loader.AlbumMediaLoader; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java b/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java index 096f29651..134db848d 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/model/SelectedItemCollection.java @@ -145,7 +145,7 @@ public List asList() { public List asListOfUri() { List uris = new ArrayList<>(); for (Item item : mItems) { - uris.add(item.uriCrop); + uris.add(item.uri); } return uris; } @@ -167,8 +167,8 @@ public boolean isSelected(Item item) { } public IncapableCause isAcceptable(Item item) { - if (maxSelectableReached()) { - int maxSelectable = currentMaxSelectable(); + if (maxSelectableReached(item)) { + int maxSelectable = currentMaxSelectable(item); String cause; try { @@ -197,18 +197,27 @@ public IncapableCause isAcceptable(Item item) { return PhotoMetadataUtils.isAcceptable(mContext, item); } - public boolean maxSelectableReached() { - return mItems.size() == currentMaxSelectable(); + public boolean maxSelectableReached(Item item) { + SelectionSpec spec = SelectionSpec.getInstance(); + int maxSelectable = currentMaxSelectable(item); + if (spec.maxSelectable > 0) { + return count() >= maxSelectable; + } else if (item.isImage()) { + return countImage() >= maxSelectable; + } else if (item.isVideo()) { + return countVideo() >= maxSelectable; + } + return count() >= maxSelectable; } // depends - private int currentMaxSelectable() { + private int currentMaxSelectable(Item item) { SelectionSpec spec = SelectionSpec.getInstance(); if (spec.maxSelectable > 0) { return spec.maxSelectable; - } else if (mCollectionType == COLLECTION_IMAGE) { + } else if (item.isImage()) { return spec.maxImageSelectable; - } else if (mCollectionType == COLLECTION_VIDEO) { + } else if (item.isVideo()) { return spec.maxVideoSelectable; } else { return spec.maxSelectable; @@ -249,8 +258,36 @@ public int count() { return mItems.size(); } + public int countImage() { + int count = 0; + for (Item item : mItems) if (item.isImage()) count++; + return count; + } + + public int countVideo() { + int count = 0; + for (Item item : mItems) if (item.isVideo()) count++; + return count; + } + public int checkedNumOf(Item item) { int index = new ArrayList<>(mItems).indexOf(item); return index == -1 ? CheckView.UNCHECKED : index + 1; } + + public boolean hasImage() { + return mCollectionType == COLLECTION_IMAGE || mCollectionType == COLLECTION_MIXED; + } + + public boolean hasVideo() { + return mCollectionType == COLLECTION_VIDEO || mCollectionType == COLLECTION_MIXED; + } + + public void updateItem(Item result) { + for (Item item : mItems) + if (item.isVideo()) { + item.uri = result.uri; + break; + } + } } diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java index 73ff53e2a..ac694cf2d 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/AlbumPreviewActivity.java @@ -17,7 +17,8 @@ import android.database.Cursor; import android.os.Bundle; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.Item; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java index 82383fd76..24cccc0d7 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/BasePreviewActivity.java @@ -19,16 +19,17 @@ import android.content.Intent; import android.graphics.Color; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.view.ViewPager; -import android.support.v4.view.animation.FastOutSlowInInterpolator; -import android.support.v7.app.AppCompatActivity; import android.view.View; import android.view.WindowManager; import android.widget.FrameLayout; import android.widget.LinearLayout; 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.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.IncapableCause; import com.zhihu.matisse.internal.entity.Item; @@ -247,7 +248,7 @@ public void onPageSelected(int position) { if (checkedNum > 0) { mCheckView.setEnabled(true); } else { - mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached()); + mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached(item)); } } else { boolean checked = mSelectedCollection.isSelected(item); @@ -255,7 +256,7 @@ public void onPageSelected(int position) { if (checked) { mCheckView.setEnabled(true); } else { - mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached()); + mCheckView.setEnabled(!mSelectedCollection.maxSelectableReached(item)); } } updateSize(item); diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java index 6cc6c719c..5b32ce9f9 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/MediaSelectionFragment.java @@ -19,17 +19,25 @@ import android.content.ContextWrapper; import android.database.Cursor; import android.graphics.Bitmap; +import android.graphics.Point; import android.net.Uri; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; import android.util.Log; +import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; +import android.widget.FrameLayout; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.daasuu.mp4compose.FillMode; +import com.daasuu.mp4compose.FillModeCustomItem; +import com.daasuu.mp4compose.composer.Mp4Composer; import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCropFragment; import com.zhihu.matisse.R; @@ -39,9 +47,12 @@ import com.zhihu.matisse.internal.model.AlbumMediaCollection; import com.zhihu.matisse.internal.model.SelectedItemCollection; import com.zhihu.matisse.internal.ui.adapter.AlbumMediaAdapter; -import com.zhihu.matisse.internal.ui.widget.CheckView; import com.zhihu.matisse.internal.ui.widget.MediaGridInset; -import com.zhihu.matisse.internal.utils.UIUtils; +import com.zhihu.matisse.internal.utils.PathUtils; +import com.zhihu.matisse.internal.utils.Utils; +import com.zhihu.matisse.ui.MatisseActivity; +import com.zhihu.matisse.ui.widget.GesturePlayerTextureView; +import com.zhihu.matisse.ui.widget.SceneCropColor; import java.io.File; import java.util.Calendar; @@ -56,6 +67,7 @@ public class MediaSelectionFragment extends Fragment implements private final AlbumMediaCollection mAlbumMediaCollection = new AlbumMediaCollection(); private RecyclerView mRecyclerView; + private GesturePlayerTextureView playerTextureView; private AlbumMediaAdapter mAdapter; private SelectionProvider mSelectionProvider; private AlbumMediaAdapter.CheckStateListener mCheckStateListener; @@ -64,6 +76,7 @@ public class MediaSelectionFragment extends Fragment implements private Uri destinationUri; private Album album; private boolean isFirst = true; + private Context context; public static MediaSelectionFragment newInstance(Album album) { MediaSelectionFragment fragment = new MediaSelectionFragment(); @@ -76,6 +89,7 @@ public static MediaSelectionFragment newInstance(Album album) { @Override public void onAttach(Context context) { super.onAttach(context); + this.context = context; if (context instanceof SelectionProvider) { mSelectionProvider = (SelectionProvider) context; } else { @@ -107,7 +121,7 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); album = getArguments().getParcelable(EXTRA_ALBUM); - mAdapter = new AlbumMediaAdapter(getContext(), + mAdapter = new AlbumMediaAdapter(context, mSelectionProvider.provideSelectedItemCollection(), mRecyclerView); mAdapter.registerCheckStateListener(this); mAdapter.registerOnMediaClickListener(this); @@ -116,11 +130,11 @@ public void onActivityCreated(@Nullable Bundle savedInstanceState) { int spanCount; SelectionSpec selectionSpec = SelectionSpec.getInstance(); if (selectionSpec.gridExpectedSize > 0) { - spanCount = UIUtils.spanCount(getContext(), selectionSpec.gridExpectedSize); + spanCount = Utils.Companion.spanCount(context, selectionSpec.gridExpectedSize); } else { spanCount = selectionSpec.spanCount; } - mRecyclerView.setLayoutManager(new GridLayoutManager(getContext(), spanCount)); + mRecyclerView.setLayoutManager(new GridLayoutManager(context, spanCount)); int spacing = getResources().getDimensionPixelSize(R.dimen.media_grid_spacing); mRecyclerView.addItemDecoration(new MediaGridInset(spanCount, spacing, false)); @@ -149,7 +163,7 @@ public void onAlbumMediaLoad(Cursor cursor) { if (isFirst) { isFirst = false; cursor.moveToPosition(album.isAll() ? 1 : 0); - showPreviewImage(Item.valueOf(cursor).uri); + showPreviewItem(Item.valueOf(cursor)); // SelectedItemCollection collection = mSelectionProvider.provideSelectedItemCollection(); // if (collection.isEmpty()) collection.add(Item.valueOf(cursor)); } @@ -169,26 +183,75 @@ public void onUpdate() { } @Override - public void onMediaClick(Album album, Item item, Item mPrevious, int adapterPosition) { - if (!item.equals(mPrevious)) { - cropCurrentImage(mPrevious); + public void onMediaClick(Album album, Item item, int adapterPosition) { + if (mOnMediaClickListener != null) { + mOnMediaClickListener.onMediaClick(getArguments().getParcelable(EXTRA_ALBUM), + item, adapterPosition); } + } + + + @Override + public void onResume() { + super.onResume(); + if (playerTextureView != null) { + playerTextureView.play(); + } + } + + @Override + public void onPause() { + super.onPause(); + if (playerTextureView != null) { + playerTextureView.pause(); + } + } + + @Override + public void onMediaAdded(Item item, Item prev) { + cropItem(prev); - if (!item.equals(mPrevious)) - showPreviewImage(item.uri); + showPreviewItem(item); } public boolean onNextButtonClick() { - return cropCurrentImage(mAdapter.mPrevious); + return cropItem(mAdapter.mPrevious); } - private boolean cropCurrentImage(Item item) { + private boolean cropItem(Item item) { + if (!mSelectionProvider.provideSelectedItemCollection().isSelected(item)) return true; + + if (item.isImage()) return cropImage(item); + else return cropVideo(item); + } + + private boolean cropVideo(Item item) { + MatisseActivity activity = (MatisseActivity) getActivity(); + activity.showProgress(); + File file = getFile(false); + destinationUri = Uri.fromFile(file); + if (destinationUri.getPath() == null) return true; + + String path = PathUtils.getPath(context, item.getContentUri()); + FillModeCustomItem fillModeCustomItem = Utils.Companion.getFillMode(playerTextureView, path); + + new Mp4Composer(path, file.getPath()) + .size(720, 720) + .filter(Utils.Companion.getFill(SceneCropColor.WHITE)) + .fillMode(FillMode.CUSTOM) + .customFillMode(fillModeCustomItem) + .listener(activity) + .start(); + + item.uri = destinationUri; + return false; + } + + private boolean cropImage(Item item) { // crop and save Current image - SelectedItemCollection collection = mSelectionProvider.provideSelectedItemCollection(); - int checkedNum = collection.checkedNumOf(item); - if (fragment != null && fragment.isAdded() && checkedNum != CheckView.UNCHECKED) { + if (fragment != null && fragment.isAdded()) { fragment.cropAndSaveImage(); - item.uriCrop = destinationUri; + item.uri = destinationUri; Log.d("cropAndSaveImage: ", destinationUri.toString()); return false; } @@ -196,11 +259,33 @@ private boolean cropCurrentImage(Item item) { } - public void showPreviewImage(Uri uri) { - // load new image - String destinationFileName = String.format("%s.jpeg", Calendar.getInstance().getTimeInMillis()); + private void showPreviewItem(Item item) { + if (item.isImage()) showPreviewImage(item.uri); + else showPreviewVideo(item); + } + + private void showPreviewVideo(Item item) { + FrameLayout parent = getView().findViewById(R.id.mPreview); + parent.removeAllViews(); + playerTextureView = new GesturePlayerTextureView(context, item.uri, null); - destinationUri = Uri.fromFile(new File(new ContextWrapper(getContext()).getCacheDir(), destinationFileName)); + Point size = new Point(); + ((WindowManager) context.getSystemService(Context.WINDOW_SERVICE)).getDefaultDisplay().getSize(size); + float baseWidthSize = size.x; + playerTextureView.setBaseWidthSize(baseWidthSize); + + FrameLayout.LayoutParams lp = new FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT); + lp.gravity = Gravity.CENTER; + playerTextureView.setLayoutParams(lp); + + parent.addView(playerTextureView); + } + + private void showPreviewImage(Uri uri) { + FrameLayout parent = getView().findViewById(R.id.mPreview); + parent.removeAllViews(); + + destinationUri = Uri.fromFile(getFile(true)); UCrop uCrop = UCrop.of(uri, destinationUri); uCrop = setupConfig(uCrop); @@ -211,6 +296,14 @@ public void showPreviewImage(Uri uri) { .commitAllowingStateLoss(); } + private File getFile(boolean isImage) { + String name = String.valueOf(Calendar.getInstance().getTimeInMillis()); + if (isImage) name += ".jpeg"; + else name += ".mp4"; + File file = new File(new ContextWrapper(context).getCacheDir(), name); + Log.d("getFile: ", file.getPath()); + return file; + } private UCrop setupConfig(UCrop uCrop) { UCrop.Options options = new UCrop.Options(); diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java index ef7fb1231..ec3cab8d6 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/PreviewItemFragment.java @@ -20,13 +20,14 @@ import android.content.Intent; import android.graphics.Point; import android.os.Bundle; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Toast; +import androidx.annotation.Nullable; +import androidx.fragment.app.Fragment; + import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java index 4bd6e25a7..7b8132d84 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/SelectedPreviewActivity.java @@ -16,7 +16,8 @@ package com.zhihu.matisse.internal.ui; import android.os.Bundle; -import android.support.annotation.Nullable; + +import androidx.annotation.Nullable; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java index 305425665..9b6349654 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/AlbumMediaAdapter.java @@ -20,14 +20,15 @@ import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.support.v7.widget.GridLayoutManager; -import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.GridLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.entity.IncapableCause; @@ -69,12 +70,9 @@ 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(); - } + holder.itemView.setOnClickListener(v1 -> { + if (v1.getContext() instanceof OnPhotoCapture) { + ((OnPhotoCapture) v1.getContext()).capture(); } }); return holder; @@ -133,7 +131,7 @@ private void setCheckStatus(Item item, MediaGrid mediaGrid) { mediaGrid.setCheckEnabled(true); mediaGrid.setCheckedNum(checkedNum); } else { - if (mSelectedCollection.maxSelectableReached()) { + if (mSelectedCollection.maxSelectableReached(item)) { mediaGrid.setCheckEnabled(false); mediaGrid.setCheckedNum(CheckView.UNCHECKED); } else { @@ -147,7 +145,7 @@ private void setCheckStatus(Item item, MediaGrid mediaGrid) { mediaGrid.setCheckEnabled(true); mediaGrid.setChecked(true); } else { - if (mSelectedCollection.maxSelectableReached()) { + if (mSelectedCollection.maxSelectableReached(item)) { mediaGrid.setCheckEnabled(false); mediaGrid.setChecked(false); } else { @@ -160,30 +158,29 @@ private void setCheckStatus(Item item, MediaGrid mediaGrid) { @Override public void onThumbnailClicked(ImageView thumbnail, Item item, RecyclerView.ViewHolder holder) { - if (mOnMediaClickListener != null) { - int checkedNum = mSelectedCollection.checkedNumOf(item); - if (checkedNum == CheckView.UNCHECKED) { - if (assertAddSelection(holder.itemView.getContext(), item)) { - mSelectedCollection.add(item); - mOnMediaClickListener.onMediaClick(null, item, mPrevious, holder.getAdapterPosition()); - } - } else { - mSelectedCollection.remove(item); + if (mSelectionSpec.showPreview) { + if (mOnMediaClickListener != null) { + mOnMediaClickListener.onMediaClick(null, item, holder.getAdapterPosition()); } - notifyCheckStateChanged(); - - mPrevious = item; + } else { + updateSelectedItem(item, holder); } } @Override public void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.ViewHolder holder) { + updateSelectedItem(item, holder); + } + + private void updateSelectedItem(Item item, RecyclerView.ViewHolder holder) { if (mSelectionSpec.countable) { int checkedNum = mSelectedCollection.checkedNumOf(item); if (checkedNum == CheckView.UNCHECKED) { if (assertAddSelection(holder.itemView.getContext(), item)) { mSelectedCollection.add(item); notifyCheckStateChanged(); + mOnMediaClickListener.onMediaAdded(item, mPrevious); + mPrevious = item; } } else { mSelectedCollection.remove(item); @@ -197,6 +194,8 @@ public void onCheckViewClicked(CheckView checkView, Item item, RecyclerView.View if (assertAddSelection(holder.itemView.getContext(), item)) { mSelectedCollection.add(item); notifyCheckStateChanged(); + mOnMediaClickListener.onMediaAdded(item, mPrevious); + mPrevious = item; } } } @@ -273,7 +272,8 @@ public interface CheckStateListener { } public interface OnMediaClickListener { - void onMediaClick(Album album, Item item, Item mPrevious, int adapterPosition); + void onMediaClick(Album album, Item item, int adapterPosition); + void onMediaAdded(Item item, Item prev); } public interface OnPhotoCapture { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java index eef46f055..7dcbf594b 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/PreviewPagerAdapter.java @@ -15,11 +15,12 @@ */ package com.zhihu.matisse.internal.ui.adapter; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; import android.view.ViewGroup; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentManager; +import androidx.fragment.app.FragmentPagerAdapter; + import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.ui.PreviewItemFragment; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java index 6557dde4d..feee6533c 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/adapter/RecyclerViewCursorAdapter.java @@ -17,7 +17,8 @@ import android.database.Cursor; import android.provider.MediaStore; -import android.support.v7.widget.RecyclerView; + +import androidx.recyclerview.widget.RecyclerView; public abstract class RecyclerViewCursorAdapter extends RecyclerView.Adapter { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java index b3c9534b7..e4a3b1a22 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/AlbumsSpinner.java @@ -20,13 +20,14 @@ import android.database.Cursor; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.support.annotation.NonNull; -import android.support.v7.widget.ListPopupWindow; 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.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Album; import com.zhihu.matisse.internal.utils.Platform; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java index f860c58cf..d539dacba 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckRadioView.java @@ -3,10 +3,11 @@ import android.content.Context; import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; -import android.support.v4.content.res.ResourcesCompat; -import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; +import androidx.appcompat.widget.AppCompatImageView; +import androidx.core.content.res.ResourcesCompat; + import com.zhihu.matisse.R; public class CheckRadioView extends AppCompatImageView { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java index b95811365..6a77f4dff 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/CheckView.java @@ -27,11 +27,12 @@ import android.graphics.Shader; import android.graphics.Typeface; import android.graphics.drawable.Drawable; -import android.support.v4.content.res.ResourcesCompat; import android.text.TextPaint; import android.util.AttributeSet; import android.view.View; +import androidx.core.content.res.ResourcesCompat; + import com.zhihu.matisse.R; public class CheckView extends View { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java index 1599fce21..a13a697e5 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/IncapableDialog.java @@ -18,11 +18,12 @@ import android.app.Dialog; import android.content.DialogInterface; import android.os.Bundle; -import android.support.annotation.NonNull; -import android.support.v4.app.DialogFragment; -import android.support.v7.app.AlertDialog; import android.text.TextUtils; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + import com.zhihu.matisse.R; public class IncapableDialog extends DialogFragment { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java index b795cafc9..9458cde5c 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGrid.java @@ -17,7 +17,6 @@ import android.content.Context; import android.graphics.drawable.Drawable; -import android.support.v7.widget.RecyclerView; import android.text.format.DateUtils; import android.util.AttributeSet; import android.view.LayoutInflater; @@ -25,6 +24,8 @@ import android.widget.ImageView; import android.widget.TextView; +import androidx.recyclerview.widget.RecyclerView; + import com.zhihu.matisse.R; import com.zhihu.matisse.internal.entity.Item; import com.zhihu.matisse.internal.entity.SelectionSpec; @@ -59,7 +60,7 @@ private void init(Context context) { mVideoDuration = (TextView) findViewById(R.id.video_duration); mThumbnail.setOnClickListener(this); -// mCheckView.setOnClickListener(this); + mCheckView.setOnClickListener(this); } @Override @@ -68,7 +69,7 @@ public void onClick(View v) { if (v == mThumbnail) { mListener.onThumbnailClicked(mThumbnail, mMedia, mPreBindInfo.mViewHolder); } else if (v == mCheckView) { -// mListener.onCheckViewClicked(mCheckView, mMedia, mPreBindInfo.mViewHolder); + mListener.onCheckViewClicked(mCheckView, mMedia, mPreBindInfo.mViewHolder); } } } diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java index eebcd429b..67358134f 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/MediaGridInset.java @@ -16,9 +16,10 @@ package com.zhihu.matisse.internal.ui.widget; import android.graphics.Rect; -import android.support.v7.widget.RecyclerView; import android.view.View; +import androidx.recyclerview.widget.RecyclerView; + public class MediaGridInset extends RecyclerView.ItemDecoration { private int mSpanCount; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java index 56fd37042..1771bae7b 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/PreviewViewPager.java @@ -16,10 +16,11 @@ package com.zhihu.matisse.internal.ui.widget; import android.content.Context; -import android.support.v4.view.ViewPager; 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 { diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java index 285515702..54aaa11cd 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/RoundedRectangleImageView.java @@ -19,9 +19,10 @@ import android.graphics.Canvas; import android.graphics.Path; import android.graphics.RectF; -import android.support.v7.widget.AppCompatImageView; import android.util.AttributeSet; +import androidx.appcompat.widget.AppCompatImageView; + public class RoundedRectangleImageView extends AppCompatImageView { private float mRadius; // dp diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareAppBarLayout.java b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareAppBarLayout.java index 202bf624e..ef97a036e 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareAppBarLayout.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/ui/widget/SquareAppBarLayout.java @@ -3,7 +3,9 @@ import android.content.Context; import android.util.AttributeSet; -public class SquareAppBarLayout extends android.support.design.widget.AppBarLayout { +import com.google.android.material.appbar.AppBarLayout; + +public class SquareAppBarLayout extends AppBarLayout { public SquareAppBarLayout(Context context) { super(context); diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java index 1ddfd9207..952c6aeb5 100644 --- a/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java +++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/MediaStoreCompat.java @@ -24,9 +24,10 @@ import android.os.Build; import android.os.Environment; import android.provider.MediaStore; -import android.support.v4.app.Fragment; -import android.support.v4.content.FileProvider; -import android.support.v4.os.EnvironmentCompat; + +import androidx.core.content.FileProvider; +import androidx.core.os.EnvironmentCompat; +import androidx.fragment.app.Fragment; import com.zhihu.matisse.internal.entity.CaptureStrategy; diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java b/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java deleted file mode 100644 index 129b59946..000000000 --- a/matisse/src/main/java/com/zhihu/matisse/internal/utils/UIUtils.java +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2017 Zhihu Inc. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package com.zhihu.matisse.internal.utils; - -import android.content.Context; - -public class UIUtils { - - public static int spanCount(Context context, int gridExpectedSize) { - int screenWidth = context.getResources().getDisplayMetrics().widthPixels; - float expected = (float) screenWidth / (float) gridExpectedSize; - int spanCount = Math.round(expected); - if (spanCount == 0) { - spanCount = 1; - } - return spanCount; - } - -} diff --git a/matisse/src/main/java/com/zhihu/matisse/internal/utils/Utils.kt b/matisse/src/main/java/com/zhihu/matisse/internal/utils/Utils.kt new file mode 100644 index 000000000..66a5d0f72 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/internal/utils/Utils.kt @@ -0,0 +1,98 @@ +/* + * Copyright 2017 Zhihu Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.zhihu.matisse.internal.utils + +import android.content.ContentValues +import android.content.Context +import android.content.Intent +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.provider.MediaStore +import android.util.Size +import com.daasuu.mp4compose.FillModeCustomItem +import com.daasuu.mp4compose.filter.GlFilter +import com.zhihu.matisse.ui.widget.GesturePlayerTextureView +import com.zhihu.matisse.ui.widget.SceneCropColor +import kotlin.math.roundToInt + +class Utils { + companion object { + fun spanCount(context: Context, gridExpectedSize: Int): Int { + val screenWidth = context.resources.displayMetrics.widthPixels + val expected = screenWidth.toFloat() / gridExpectedSize.toFloat() + var spanCount = expected.roundToInt() + if (spanCount == 0) { + spanCount = 1 + } + return spanCount + } + + fun getFillMode(playerTextureView: GesturePlayerTextureView, path: String?): FillModeCustomItem { + val resolution = getVideoResolution(path) + return FillModeCustomItem( + playerTextureView!!.scaleX, + playerTextureView!!.rotation, + playerTextureView!!.translationX / playerTextureView!!.baseWidthSize * 2f, + playerTextureView!!.translationY / playerTextureView!!.baseWidthSize * 2f, + resolution.width.toFloat(), + resolution.height.toFloat() + ) + } + + + fun getFill(sceneCropColor: SceneCropColor = SceneCropColor.WHITE): GlFilter { + val glFilter = GlFilter() + val clearColorItem = sceneCropColor.clearColorItem + glFilter.setClearColor(clearColorItem.red, clearColorItem.green, clearColorItem.blue, clearColorItem.alpha) + return glFilter + } + + fun exportMp4ToGallery(context: Context, filePath: String) { + val values = ContentValues(2) + values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") + values.put(MediaStore.Video.Media.DATA, filePath) + context.contentResolver.insert(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values) + context.sendBroadcast(Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE, + Uri.parse("file://$filePath"))) + } + + fun getVideoResolution(path: String?): Size { + val retriever = MediaMetadataRetriever() + retriever.setDataSource(path) + val width = Integer.valueOf( + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH) + ) + val height = Integer.valueOf( + retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT) + ) + retriever.release() + val rotation = getVideoRotation(path) + return if (rotation == 90 || rotation == 270) { + Size(height, width) + } else Size(width, height) + } + + + fun getVideoRotation(videoFilePath: String?): Int { + val mediaMetadataRetriever = MediaMetadataRetriever() + mediaMetadataRetriever.setDataSource(videoFilePath) + val orientation = mediaMetadataRetriever.extractMetadata( + MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION + ) + return Integer.valueOf(orientation) + } + } +} diff --git a/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java b/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java index 33e374e62..515ab9148 100644 --- a/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java +++ b/matisse/src/main/java/com/zhihu/matisse/listener/OnSelectedListener.java @@ -17,7 +17,8 @@ package com.zhihu.matisse.listener; import android.net.Uri; -import android.support.annotation.NonNull; + +import androidx.annotation.NonNull; import java.util.List; diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/FilterActivity.kt b/matisse/src/main/java/com/zhihu/matisse/ui/FilterActivity.kt index b8ee3928d..d9246a60a 100644 --- a/matisse/src/main/java/com/zhihu/matisse/ui/FilterActivity.kt +++ b/matisse/src/main/java/com/zhihu/matisse/ui/FilterActivity.kt @@ -5,16 +5,16 @@ import android.content.Context import android.graphics.Bitmap import android.net.Uri import android.os.Bundle -import android.support.v4.view.PagerAdapter -import android.support.v4.view.ViewPager -import android.support.v7.app.AppCompatActivity -import android.support.v7.widget.LinearLayoutManager import android.util.Log import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup import android.widget.ImageView +import androidx.appcompat.app.AppCompatActivity +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.viewpager.widget.PagerAdapter +import androidx.viewpager.widget.ViewPager import com.bumptech.glide.Glide import com.bumptech.glide.load.DataSource import com.bumptech.glide.load.engine.GlideException diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java b/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java index d571ca66a..0ce3e570a 100644 --- a/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java +++ b/matisse/src/main/java/com/zhihu/matisse/ui/MatisseActivity.java @@ -24,16 +24,9 @@ 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.support.annotation.NonNull; -import android.support.annotation.Nullable; -import android.support.v4.app.Fragment; -import android.support.v7.app.ActionBar; -import android.support.v7.app.AppCompatActivity; -import android.support.v7.widget.Toolbar; import android.util.Log; import android.view.MenuItem; import android.view.View; @@ -42,6 +35,14 @@ import android.widget.TextView; import android.widget.Toast; +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.fragment.app.Fragment; + +import com.daasuu.mp4compose.composer.Mp4Composer; import com.yalantis.ucrop.UCrop; import com.yalantis.ucrop.UCropFragment; import com.yalantis.ucrop.UCropFragmentCallback; @@ -51,6 +52,7 @@ import com.zhihu.matisse.internal.entity.SelectionSpec; import com.zhihu.matisse.internal.model.AlbumCollection; import com.zhihu.matisse.internal.model.SelectedItemCollection; +import com.zhihu.matisse.internal.ui.AlbumPreviewActivity; import com.zhihu.matisse.internal.ui.BasePreviewActivity; import com.zhihu.matisse.internal.ui.MediaSelectionFragment; import com.zhihu.matisse.internal.ui.SelectedPreviewActivity; @@ -74,7 +76,7 @@ public class MatisseActivity extends AppCompatActivity implements UCropFragmentC AlbumCollection.AlbumCallbacks, AdapterView.OnItemSelectedListener, MediaSelectionFragment.SelectionProvider, View.OnClickListener, AlbumMediaAdapter.CheckStateListener, AlbumMediaAdapter.OnMediaClickListener, - AlbumMediaAdapter.OnPhotoCapture { + AlbumMediaAdapter.OnPhotoCapture, Mp4Composer.Listener { public static final String EXTRA_RESULT_SELECTION = "extra_result_selection"; public static final String EXTRA_RESULT_SELECTION_PATH = "extra_result_selection_path"; @@ -82,6 +84,7 @@ public class MatisseActivity extends AppCompatActivity implements UCropFragmentC private static final int REQUEST_CODE_PREVIEW = 23; private static final int REQUEST_CODE_CAPTURE = 24; private static final int REQUEST_CODE_FILTER = 25; + private static final int REQUEST_CODE_TRIM_VIDEO = 26; public static final String CHECK_STATE = "checkState"; private final AlbumCollection mAlbumCollection = new AlbumCollection(); private MediaStoreCompat mMediaStoreCompat; @@ -99,7 +102,9 @@ public class MatisseActivity extends AppCompatActivity implements UCropFragmentC private CheckRadioView mOriginal; private boolean mOriginalEnable; private ProgressDialog mProgressDialog; + private boolean clickNextButton = false; + private boolean isTrimVideo = false; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -213,12 +218,18 @@ public void onCropFinish(UCropFragment.UCropResult result) { if (clickNextButton) { clickNextButton = false; - if (mSpec.hasFilter) startFilterActivity(); - else returnResult(); + handleNextAction(); } hideProgress(); } + private void handleNextAction() { + if (mSpec.hasTrimVideo && mSelectedCollection.hasVideo() && !isTrimVideo) + startVideoEditorActivity(); + else if (mSpec.hasFilter && mSelectedCollection.hasImage()) startFilterActivity(); + else returnResult(); + } + private void returnResult() { Intent intent = new Intent(); ArrayList selectedUris = (ArrayList) mSelectedCollection.asListOfUri(); @@ -230,6 +241,16 @@ private void returnResult() { finish(); } + private void startVideoEditorActivity() { + Intent intent = new Intent(this, VideoEditorActivity.class); + for (Item item : mSelectedCollection.asList()) + if (item.isVideo()) { + intent.putExtra(VideoEditorActivity.PATH_ARG, item); + break; + } + startActivityForResult(intent, REQUEST_CODE_TRIM_VIDEO); + } + private void startFilterActivity() { Intent intent = new Intent(this, FilterActivity.class); ArrayList selectedUris = (ArrayList) mSelectedCollection.asListOfUri(); @@ -238,7 +259,7 @@ private void startFilterActivity() { } public void showProgress() { - hideProgress(); + if (mProgressDialog != null && mProgressDialog.isShowing()) return; mProgressDialog = ProgressDialog.show(this, null, "Please wait..."); } @@ -311,19 +332,17 @@ protected void onActivityResult(int requestCode, int resultCode, Intent data) { result.putParcelableArrayListExtra(EXTRA_RESULT_SELECTION, selected); result.putStringArrayListExtra(EXTRA_RESULT_SELECTION_PATH, selectedPath); setResult(RESULT_OK, result); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) - MatisseActivity.this.revokeUriPermission(contentUri, - Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION); galleryAddPic(getApplicationContext()); Handler handler = new Handler(Looper.getMainLooper()); - handler.postDelayed(new Runnable() { - @Override - public void run() { - onAlbumSelected(Album.valueOf(mAlbumsAdapter.getCursor())); - } - }, 400); + handler.postDelayed(() -> onAlbumSelected(Album.valueOf(mAlbumsAdapter.getCursor())), 400); } else if (requestCode == REQUEST_CODE_FILTER) { returnResult(); + } else if (requestCode == REQUEST_CODE_TRIM_VIDEO) { + Item result = data.getParcelableExtra(VideoEditorActivity.PATH_ARG); + mSelectedCollection.updateItem(result); + + isTrimVideo = true; + handleNextAction(); } } @@ -411,8 +430,7 @@ public void onClick(View v) { MediaSelectionFragment.class.getSimpleName()); if (mediaSelectionFragment instanceof MediaSelectionFragment) { if (((MediaSelectionFragment) mediaSelectionFragment).onNextButtonClick()) { - if (mSpec.hasFilter) startFilterActivity(); - else returnResult(); + handleNextAction(); } } } else if (v.getId() == R.id.originalLayout) { @@ -499,13 +517,18 @@ public void onUpdate() { } @Override - public void onMediaClick(Album album, Item item, Item mPrevious, int adapterPosition) { -// Intent intent = new Intent(this, AlbumPreviewActivity.class); -// intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); -// intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item); -// intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); -// intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); - //startActivityForResult(intent, REQUEST_CODE_PREVIEW); + public void onMediaClick(Album album, Item item, int adapterPosition) { + Intent intent = new Intent(this, AlbumPreviewActivity.class); + intent.putExtra(AlbumPreviewActivity.EXTRA_ALBUM, album); + intent.putExtra(AlbumPreviewActivity.EXTRA_ITEM, item); + intent.putExtra(BasePreviewActivity.EXTRA_DEFAULT_BUNDLE, mSelectedCollection.getDataWithBundle()); + intent.putExtra(BasePreviewActivity.EXTRA_RESULT_ORIGINAL_ENABLE, mOriginalEnable); + startActivityForResult(intent, REQUEST_CODE_PREVIEW); + } + + @Override + public void onMediaAdded(Item item, Item prev) { + } @Override @@ -520,4 +543,29 @@ public void capture() { } } + @Override + public void onProgress(double progress) { + Log.d(this + "", "onProgress = " + progress); + showProgress(); + } + + @Override + public void onCompleted() { + Log.d(this + "", "onCompleted()"); + if (clickNextButton) { + clickNextButton = false; + handleNextAction(); + } + hideProgress(); + } + + @Override + public void onCanceled() { + } + + @Override + public void onFailed(Exception exception) { + Log.d(this + "", "onFailed()"); + hideProgress(); + } } diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/ThumbnailsAdapter.kt b/matisse/src/main/java/com/zhihu/matisse/ui/ThumbnailsAdapter.kt index 9334b8959..c56458051 100644 --- a/matisse/src/main/java/com/zhihu/matisse/ui/ThumbnailsAdapter.kt +++ b/matisse/src/main/java/com/zhihu/matisse/ui/ThumbnailsAdapter.kt @@ -1,14 +1,13 @@ package com.zhihu.matisse.ui -import android.support.v7.widget.RecyclerView import android.util.Log import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView import com.zhihu.matisse.R - import com.zomato.photofilters.utils.ThumbnailCallback import com.zomato.photofilters.utils.ThumbnailItem diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/VideoEditorActivity.kt b/matisse/src/main/java/com/zhihu/matisse/ui/VideoEditorActivity.kt new file mode 100644 index 000000000..7f76b84f7 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/VideoEditorActivity.kt @@ -0,0 +1,160 @@ +package com.zhihu.matisse.ui + +import android.app.Activity +import android.app.AlertDialog +import android.app.ProgressDialog +import android.content.Context +import android.content.ContextWrapper +import android.content.Intent +import android.graphics.Point +import android.net.Uri +import android.os.Bundle +import android.os.Environment +import android.view.Gravity +import android.view.WindowManager +import android.widget.FrameLayout.LayoutParams +import androidx.appcompat.app.AppCompatActivity +import androidx.core.content.ContextCompat +import com.daasuu.mp4compose.FillMode +import com.daasuu.mp4compose.composer.Mp4Composer +import com.zhihu.matisse.R +import com.zhihu.matisse.internal.entity.Item +import com.zhihu.matisse.internal.utils.PathUtils +import com.zhihu.matisse.internal.utils.Utils +import com.zhihu.matisse.ui.widget.GesturePlayerTextureView +import com.zhihu.matisse.ui.widget.SceneCropColor +import kotlinx.android.synthetic.main.activity_video_trim.* +import java.io.File +import java.util.* + +class VideoEditorActivity : AppCompatActivity() { + companion object { + internal const val PATH_ARG = "PATH_ARG" + } + + private lateinit var item: Item + private lateinit var srcPath: Uri + private var baseWidthSize: Float = 0.toFloat() + private var playerTextureView: GesturePlayerTextureView? = null + private var clearColorDialog: AlertDialog? = null + private var sceneCropColor = SceneCropColor.WHITE + + private val windowHeight: Int + get() { + val size = Point() + (getSystemService(Context.WINDOW_SERVICE) as WindowManager).defaultDisplay.getSize(size) + return size.x + } + + private val videoFilePath: String + get() = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES) + .absolutePath + "/necosta_" + Date().time + ".mp4" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_video_trim) + + if (intent == null) { + finish() + return + } + item = intent.getParcelableExtra(PATH_ARG) + srcPath = item.uri + + btn_rotate?.setOnClickListener { playerTextureView!!.updateRotate() } + done?.setOnClickListener { codec() } + + btn_color_change?.setOnClickListener { v -> + if (clearColorDialog == null) { + val builder = AlertDialog.Builder(v.context) + builder.setTitle(getString(R.string.video_background_color_title)) + builder.setOnDismissListener { clearColorDialog = null } + + val items = SceneCropColor.values() + val charList = arrayOfNulls(items.size) + var i = 0 + val n = items.size + while (i < n) { + charList[i] = items[i].name + i++ + } + builder.setItems(charList) { _, item -> + sceneCropColor = items[item] + layout_crop_trim_video?.setBackgroundColor(ContextCompat.getColor(v.context, + sceneCropColor.colorRes)) + } + clearColorDialog = builder.show() + } else { + clearColorDialog!!.dismiss() + } + } + + initPlayer() + } + + override fun onResume() { + super.onResume() + if (playerTextureView != null) { + playerTextureView!!.play() + } + } + + override fun onPause() { + super.onPause() + if (playerTextureView != null) { + playerTextureView!!.pause() + } + } + + private fun initPlayer() { + playerTextureView = GesturePlayerTextureView(applicationContext, srcPath, timeSelector) + + val layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT) + layoutParams.gravity = Gravity.CENTER + + playerTextureView!!.layoutParams = layoutParams + baseWidthSize = windowHeight.toFloat() + playerTextureView!!.setBaseWidthSize(baseWidthSize) + + layout_crop_trim_video?.addView(playerTextureView) + } + + private fun codec() { + val progress = ProgressDialog.show(this, null, "Please wait...") + btn_rotate?.isEnabled = false + btn_color_change?.isEnabled = false + + val file = File(ContextWrapper(this).cacheDir, "${Calendar.getInstance().timeInMillis}.mp4") + val timeCrop = timeSelector.getCurrent() + + Mp4Composer(PathUtils.getPath(this, srcPath), file.path) + .size(720, 720) + .trim(timeCrop[0], timeCrop[1]) + .filter(Utils.getFill(sceneCropColor)) + .fillMode(FillMode.CUSTOM) + .customFillMode(Utils.getFillMode(playerTextureView!!, srcPath.path)) + .listener(object : Mp4Composer.Listener { + override fun onProgress(progress: Double) { + } + + override fun onCompleted() { + progress.dismiss() +// exportMp4ToGallery(applicationContext, videoPath) + runOnUiThread { + val intent = Intent() + item.uri = Uri.fromFile(file) + intent.putExtra(PATH_ARG, item) + setResult(Activity.RESULT_OK, intent) + finish() + } + } + + override fun onCanceled() {} + + override fun onFailed(exception: Exception) { + progress.dismiss() + } + }) + .start() + } +} diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/AllGestureDetector.java b/matisse/src/main/java/com/zhihu/matisse/ui/widget/AllGestureDetector.java new file mode 100644 index 000000000..35bb52924 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/AllGestureDetector.java @@ -0,0 +1,123 @@ +package com.zhihu.matisse.ui.widget; + +import android.graphics.PointF; +import android.view.MotionEvent; +import android.view.View; + +class AllGestureDetector implements DragGestureDetector.DragGestureListener, RotateGestureDetector.RotateGestureListener, PinchGestureDetector.PinchGestureListener { + + private static final float DEFAULT_LIMIT_SCALE_MAX = 2.7f; + private static final float DEFAULT_LIMIT_SCALE_MIN = 0.5f; + + private float limitScaleMax = DEFAULT_LIMIT_SCALE_MAX; + private float limitScaleMin = DEFAULT_LIMIT_SCALE_MIN; + + private float scaleFactor = 1.0f; + + private final RotateGestureDetector rotateGestureDetector; + private final DragGestureDetector dragGestureDetector; + private final PinchGestureDetector pinchGestureDetector; + private final View view; + + private float angle = 0f; + private boolean rotateFlag = true; + + private MoveDragXYListener moveDragXYListener; + + AllGestureDetector(View view) { + dragGestureDetector = new DragGestureDetector(this); + rotateGestureDetector = new RotateGestureDetector(this); + pinchGestureDetector = new PinchGestureDetector(this); + this.view = view; + } + + void onTouch(MotionEvent event) { + if (rotateFlag) { + rotateGestureDetector.onTouchEvent(event); + } + dragGestureDetector.onTouchEvent(event); + pinchGestureDetector.onTouchEvent(event); + } + + void noRotate() { + rotateFlag = false; + } + + public void setMoveDragXYListener(MoveDragXYListener moveDragXYListener) { + this.moveDragXYListener = moveDragXYListener; + } + + void updateAngle() { + this.angle = view.getRotation(); + } + + public void setLimitScaleMax(float limit) { + this.limitScaleMax = limit; + } + + void setLimitScaleMin(float limit) { + this.limitScaleMin = limit; + } + + + @Override + public void onPinchGestureListener(float scale) { + float tmpScale = scaleFactor * scale; + + if (limitScaleMin <= tmpScale && tmpScale <= limitScaleMax) { + scaleFactor = tmpScale; + view.setScaleX(scaleFactor); + view.setScaleY(scaleFactor); + } + + } + + + // rotate + @Override + public void onRotation(float deltaAngle) { + angle += deltaAngle; + view.setRotation(view.getRotation() + deltaAngle); + } + + + @Override + synchronized public void onDragGestureListener(float deltaX, float deltaY) { + + // touch move + + float dx = deltaX; + float dy = deltaY; + PointF pf = createRotatePointF(0, 0, angle, dx, dy); + + dx = pf.x; + dy = pf.y; + + float x = view.getX() + dx * scaleFactor; + float y = view.getY() + dy * scaleFactor; + + if (moveDragXYListener != null) { + moveDragXYListener.onMove(x, y); + } + + view.setX(x); + view.setY(y); + } + + + private static PointF createRotatePointF(float centerX, float centerY, float angle, float x, float y) { + + double rad = Math.toRadians(angle); + + float resultX = (float) ((x - centerX) * Math.cos(rad) - (y - centerY) * Math.sin(rad) + centerX); + float resultY = (float) ((x - centerX) * Math.sin(rad) + (y - centerY) * Math.cos(rad) + centerY); + + return new PointF(resultX, resultY); + } + + public interface MoveDragXYListener { + void onMove(float x, float y); + } + +} + diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/DragGestureDetector.java b/matisse/src/main/java/com/zhihu/matisse/ui/widget/DragGestureDetector.java new file mode 100644 index 000000000..802f9824c --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/DragGestureDetector.java @@ -0,0 +1,139 @@ +package com.zhihu.matisse.ui.widget; + +import android.util.Log; +import android.view.MotionEvent; + +import java.util.HashMap; + +class DragGestureDetector { + + private static final String TAG = DragGestureDetector.class.getName(); + + private int originalIndex; + + private HashMap pointMap = new HashMap<>(); + + private DragGestureListener dragGestureListener; + + public interface DragGestureListener { + void onDragGestureListener(float deltaX, float deltaY); + } + + DragGestureDetector(DragGestureListener dragGestureListener) { + this.dragGestureListener = dragGestureListener; + pointMap.put(0, createPoint(0.f, 0.f)); + originalIndex = 0; + } + + synchronized boolean onTouchEvent(MotionEvent event) { + + if (event.getPointerCount() >= 3) { + return false; + } + + float eventX = event.getX(originalIndex); + float eventY = event.getY(originalIndex); + + int action = event.getAction() & MotionEvent.ACTION_MASK; + int actionPointer = event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + + // 最初のpointしか来ない + + TouchPoint downPoint = pointMap.get(0); + if (downPoint != null) { + downPoint.setXY(eventX, eventY); + } else { + downPoint = createPoint(eventX, eventY); + pointMap.put(0, downPoint); + } + + originalIndex = 0; + + break; + } + case MotionEvent.ACTION_MOVE: { + + TouchPoint originalPoint = pointMap.get(originalIndex); + if (originalPoint != null) { + float deltaX = eventX - originalPoint.x; + float deltaY = eventY - originalPoint.y; + + if (dragGestureListener != null) {// && (count < 2)) { + dragGestureListener.onDragGestureListener(deltaX, deltaY); + } + } + + break; + } + case MotionEvent.ACTION_POINTER_DOWN: { + + int downId = actionPointer >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + + float multiTouchX = event.getX(downId); + float multiTouchY = event.getY(downId); + + TouchPoint p = pointMap.get(downId); + + if (p != null) { + p.x = multiTouchX; + p.y = multiTouchY; + } else { + pointMap.put(downId, createPoint(multiTouchX, multiTouchY)); + } + + break; + } + case MotionEvent.ACTION_POINTER_UP: { + + int upId = actionPointer >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + Log.d(TAG, "ACTION_POINTER_UP id : " + upId); + + if (originalIndex == upId) { + Log.d(TAG, "ACTION_POINTER_UP orig up"); + pointMap.remove(upId); + + TouchPoint secondPoint = null; + for (int index = 0, n = pointMap.size(); index < n; index++) { + if (originalIndex != index) { + secondPoint = pointMap.get(index); + if (secondPoint != null) { + secondPoint.setXY(event.getX(index), event.getY(index)); + originalIndex = index; + break; + } + } + } + } + + break; + } + + default: + } + return false; + } + + private TouchPoint createPoint(float x, float y) { + return new TouchPoint(x, y); + } + + class TouchPoint { + float x; + float y; + + TouchPoint(float x, float y) { + this.x = x; + this.y = y; + } + + TouchPoint setXY(float x, float y) { + this.x = x; + this.y = y; + return this; + } + } + +} diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/GesturePlayerTextureView.java b/matisse/src/main/java/com/zhihu/matisse/ui/widget/GesturePlayerTextureView.java new file mode 100644 index 000000000..37f831e77 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/GesturePlayerTextureView.java @@ -0,0 +1,78 @@ +package com.zhihu.matisse.ui.widget; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.net.Uri; +import android.view.MotionEvent; +import android.view.View; + +@SuppressLint("ViewConstructor") +public class GesturePlayerTextureView extends PlayerTextureView implements View.OnTouchListener { + + private final AllGestureDetector allGestureDetector; + + // 基準となる枠のサイズ + public float baseWidthSize = 0; + + public GesturePlayerTextureView(Context context, Uri path, TimeSelectorView timeSelector) { + super(context, path, timeSelector); + setOnTouchListener(this); + allGestureDetector = new AllGestureDetector(this); + allGestureDetector.setLimitScaleMin(0.1f); + allGestureDetector.noRotate(); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + allGestureDetector.onTouch(event); + return true; + } + + public void setBaseWidthSize(float baseSize) { + this.baseWidthSize = baseSize; + requestLayout(); + } + + public void updateRotate() { + final int rotation = (int) getRotation(); + + switch (rotation) { + case 0: + super.setRotation(90f); + break; + case 90: + super.setRotation(180f); + break; + case 180: + super.setRotation(270f); + break; + case 270: + super.setRotation(0f); + break; + } + + allGestureDetector.updateAngle(); + } + + + @Override + public void setRotation(float rotation) { + // do nothing + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + if (getVideoAspect() == Companion.getDEFAULT_ASPECT() || baseWidthSize == 0) return; + + // 正方形 + if (getVideoAspect() == 1.0f) { + setMeasuredDimension((int) baseWidthSize, (int) baseWidthSize); + return; + } + + // 縦長 or 横長 + setMeasuredDimension((int) baseWidthSize, (int) (baseWidthSize / getVideoAspect())); + } +} + diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/PinchGestureDetector.java b/matisse/src/main/java/com/zhihu/matisse/ui/widget/PinchGestureDetector.java new file mode 100644 index 000000000..84bd85086 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/PinchGestureDetector.java @@ -0,0 +1,112 @@ +package com.zhihu.matisse.ui.widget; + +import android.view.MotionEvent; + +class PinchGestureDetector { + private float scale = 1.0f; + + private float adjustDistanceRate = 1f; + + private float distance; + + private float preDistance; + + private PinchGestureListener pinchGestureListener; + + public interface PinchGestureListener { + void onPinchGestureListener(float scale); + } + + PinchGestureDetector(PinchGestureListener dragGestureListener) { + this.pinchGestureListener = dragGestureListener; + } + + public float getScale() { + return this.scale; + } + + public float getDistance() { + return this.distance; + } + + public float getPreDistance() { + return this.preDistance; + } + + synchronized public boolean onTouchEvent(MotionEvent event) { + + float eventX = event.getX() * scale; + float eventY = event.getY() * scale; + int count = event.getPointerCount(); + + int action = event.getAction() & MotionEvent.ACTION_MASK; + int actionPointerIndex = event.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK; + + switch (action) { + case MotionEvent.ACTION_DOWN: { + + /** 最初のpointしか来ない */ + + break; + } + case MotionEvent.ACTION_MOVE: { + + if (count == 2) { + + float multiTouchX = event.getX(1) * scale; + float multiTouchY = event.getY(1) * scale; + + distance = culcDistance(eventX, eventY, multiTouchX, multiTouchY); + + float adjustDistance = distance + ((preDistance - distance) * adjustDistanceRate); + + pinchGestureListener.onPinchGestureListener(distance / adjustDistance); + scale *= distance / preDistance; + preDistance = distance; + + } + + break; + } + case MotionEvent.ACTION_POINTER_DOWN: { + + /** 2本の位置を記録 以後、moveにて距離の差分を算出 */ + + if (count == 2) { + int downId = actionPointerIndex >> MotionEvent.ACTION_POINTER_INDEX_SHIFT; + + float multiTouchX = event.getX(downId) * scale; + float multiTouchY = event.getY(downId) * scale; + + distance = culcDistance(eventX, eventY, multiTouchX, multiTouchY); + float adjustDistance = distance + ((preDistance - distance) * adjustDistanceRate); + pinchGestureListener.onPinchGestureListener(adjustDistance); + preDistance = distance; + } + + break; + } + case MotionEvent.ACTION_POINTER_UP: { + + distance = 0; + preDistance = 0; + scale = 1.0f; + + break; + } + + default: + } + return false; + } + + private float culcDistance(float x1, float y1, float x2, float y2) { + final float dx = x1 - x2; + final float dy = y1 - y2; + return (float) Math.sqrt(dx * dx + dy * dy); + } + + public void setAdjustDistanceRate(float adjustDistanceRate) { + this.adjustDistanceRate = adjustDistanceRate; + } +} diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/PlayerTextureView.kt b/matisse/src/main/java/com/zhihu/matisse/ui/widget/PlayerTextureView.kt new file mode 100644 index 000000000..eec8fc625 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/PlayerTextureView.kt @@ -0,0 +1,156 @@ +package com.zhihu.matisse.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.SurfaceTexture +import android.media.MediaMetadataRetriever +import android.net.Uri +import android.util.Log +import android.view.Surface +import android.view.TextureView +import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.ExoPlayerFactory +import com.google.android.exoplayer2.SimpleExoPlayer +import com.google.android.exoplayer2.source.ClippingMediaSource +import com.google.android.exoplayer2.source.LoopingMediaSource +import com.google.android.exoplayer2.source.MediaSource +import com.google.android.exoplayer2.source.ProgressiveMediaSource +import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory +import com.google.android.exoplayer2.video.VideoListener +import java.util.concurrent.TimeUnit + +@SuppressLint("ViewConstructor") +open class PlayerTextureView(context: Context, path: Uri, private val timeSelector: TimeSelectorView?) + : TextureView(context, null, 0), TextureView.SurfaceTextureListener, VideoListener { + + private val VIDEO_LIMIT_US = TimeUnit.SECONDS.toMicros(30) // 30秒 + private var duration = TimeUnit.SECONDS.toMicros(1) + private val player: SimpleExoPlayer? + protected var videoAspect = DEFAULT_ASPECT + + init { + timeSelector?.apply { + selectionStartListener = { start, left, right -> + if (start) pause() + else { + play() + onSelectionChanged(context, path, left, right) + } + } + initDuration(context, path) + } + + // SimpleExoPlayer + player = ExoPlayerFactory.newSimpleInstance(context) + player!!.addVideoListener(this) + + // Prepare the player with the source. + player.prepare(createLoopingMediaSource(context, path)) + surfaceTextureListener = this + } + + private fun initDuration(context: Context, path: Uri) { + val retriever = MediaMetadataRetriever() + //use one of overloaded setDataSource() functions to set your data source + retriever.setDataSource(context, path) + val time = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + duration = TimeUnit.MILLISECONDS.toMicros(java.lang.Long.parseLong(time)) + timeSelector?.setLimit(VIDEO_LIMIT_US.toFloat() / duration) + timeSelector?.duration = duration + Log.d(TAG, "duration = $duration") + retriever.release() + } + + private fun createLoopingMediaSource(context: Context, path: Uri): MediaSource { + // Produces DataSource instances through which media data is loaded. + val dataSourceFactory = DefaultDataSourceFactory(context, "No-Agent") + + // This is the MediaSource representing the media to be played. + val videoSource = ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(path) + + return LoopingMediaSource(videoSource) + } + + private fun onSelectionChanged(context: Context, path: Uri, left: Float?, right: Float?) { + // Produces DataSource instances through which media data is loaded. + val dataSourceFactory = DefaultDataSourceFactory(context, "No-Agent") + + // This is the MediaSource representing the media to be played. + var videoSource: MediaSource = ProgressiveMediaSource.Factory(dataSourceFactory) + .createMediaSource(path) + + val start = (left!! * duration).toLong() + val end = (right!! * duration).toLong() + if (start != C.TIME_UNSET) videoSource = ClippingMediaSource(videoSource, start, end) + + player?.prepare(LoopingMediaSource(videoSource)) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + + if (videoAspect == DEFAULT_ASPECT) return + + val measuredWidth = measuredWidth + val viewHeight = (measuredWidth / videoAspect).toInt() + Log.d(TAG, "onMeasure videoAspect = $videoAspect") + Log.d(TAG, "onMeasure viewWidth = $measuredWidth viewHeight = $viewHeight") + + setMeasuredDimension(measuredWidth, viewHeight) + } + + + override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) { + Log.d(TAG, "onSurfaceTextureAvailable width = $width height = $height") + + //3. bind the player to the view + player!!.setVideoSurface(Surface(surface)) + player.playWhenReady = true + } + + override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) { + + } + + override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean { + player!!.stop() + player.release() + return false + } + + override fun onSurfaceTextureUpdated(surface: SurfaceTexture) { + + } + + override fun onVideoSizeChanged(width: Int, height: Int, unappliedRotationDegrees: Int, pixelWidthHeightRatio: Float) { + Log.d(TAG, "width = " + width + " height = " + height + " unappliedRotationDegrees = " + + unappliedRotationDegrees + " pixelWidthHeightRatio = " + pixelWidthHeightRatio) + videoAspect = width.toFloat() / height * pixelWidthHeightRatio + Log.d(TAG, "videoAspect = $videoAspect") + requestLayout() + } + + override fun onSurfaceSizeChanged(width: Int, height: Int) { + + } + + override fun onRenderedFirstFrame() { + + } + + fun play() { + player!!.playWhenReady = true + } + + fun pause() { + player!!.playWhenReady = false + } + + companion object { + + private val TAG = PlayerTextureView::class.java.simpleName + protected val DEFAULT_ASPECT = -1f + } + +} diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/RotateGestureDetector.java b/matisse/src/main/java/com/zhihu/matisse/ui/widget/RotateGestureDetector.java new file mode 100644 index 000000000..757298a9c --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/RotateGestureDetector.java @@ -0,0 +1,120 @@ +package com.zhihu.matisse.ui.widget; + +import android.view.MotionEvent; + +class RotateGestureDetector { + + private final static int SLOPE_0 = 10000; + + private RotateGestureListener rotationGestureListener; + + private float angle; + private float downX = 0; + private float downY = 0; + private float downX2 = 0; + private float downY2 = 0; + private boolean isFirstPointerUp = false; + + public interface RotateGestureListener { + void onRotation(float deltaAngle); + } + + RotateGestureDetector(RotateGestureListener rotationGestureListener2) { + this.rotationGestureListener = rotationGestureListener2; + } + + @SuppressWarnings("deprecation") + synchronized public boolean onTouchEvent(MotionEvent event) { + + float eventX = event.getX(); + float eventY = event.getY(); + int count = event.getPointerCount(); + + switch (event.getAction() & MotionEvent.ACTION_MASK) { + case MotionEvent.ACTION_DOWN: + downX = eventX; + downY = eventY; + if (count >= 2) { + downX2 = event.getX(1); + downY2 = event.getY(1); + } + break; + case MotionEvent.ACTION_POINTER_DOWN: + downX2 = event.getX(1); + downY2 = event.getY(1); + break; + case MotionEvent.ACTION_MOVE: + + if (count >= 2) { + + // 回転角度の取得 + float angle = getAngle(downX, downY, downX2, downY2, eventX, eventY, event.getX(1), event.getY(1)); + + // 画像の回転 + if (angle != SLOPE_0) { + this.angle -= angle * 180d / Math.PI; + } + + downX2 = event.getX(1); + downY2 = event.getY(1); + + if (rotationGestureListener != null) { + rotationGestureListener.onRotation(getDeltaAngle()); + } + } + + break; + case MotionEvent.ACTION_POINTER_UP: + switch (event.getAction()) { + case MotionEvent.ACTION_POINTER_1_UP: + isFirstPointerUp = true; + break; + default: + } + break; + default: + } + + if (isFirstPointerUp) { + downX = downX2; + downY = downY2; + isFirstPointerUp = false; + } else { + downX = eventX; + downY = eventY; + } + + return true; + } + + private float getDeltaAngle() { + return angle; + } + + private static float getAngle(float xi1, float yi1, float xm1, float ym1, float xi2, float yi2, float xm2, float ym2) { + + // 2本の直線の傾き・y切片を算出 + float firstLinearSlope; + if ((xm1 - xi1) != 0 && (ym1 - yi1) != 0) { + firstLinearSlope = (xm1 - xi1) / (ym1 - yi1); + } else { + return SLOPE_0; + } + + float secondLinearSlope = (xm2 - xi2) / (ym2 - yi2); + if ((xm2 - xi2) != 0 && (ym2 - yi2) != 0) { + secondLinearSlope = (xm2 - xi2) / (ym2 - yi2); + } else { + return SLOPE_0; + } + + if (firstLinearSlope * secondLinearSlope == -1) { + return 90.0f; + } + + float tan = (secondLinearSlope - firstLinearSlope) / (1 + secondLinearSlope * firstLinearSlope); + + return (float) Math.atan(tan); + } + +} diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/SceneCropColor.kt b/matisse/src/main/java/com/zhihu/matisse/ui/widget/SceneCropColor.kt new file mode 100644 index 000000000..9446cdfd7 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/SceneCropColor.kt @@ -0,0 +1,19 @@ +package com.zhihu.matisse.ui.widget + +import androidx.annotation.ColorRes +import com.zhihu.matisse.R + +enum class SceneCropColor(@param:ColorRes val colorRes: Int, val clearColorItem: ClearColorItem) { + + WHITE(R.color.white, ClearColorItem(1f, 1f, 1f, 1f)), + GRAY(R.color.crop_background_gray, ClearColorItem(0.867f, 0.867f, 0.867f, 1f)), + DARK(R.color.crop_background_dark, ClearColorItem(0.267f, 0.267f, 0.267f, 1f)), + BLACK(R.color.black, ClearColorItem(0f, 0f, 0f, 1f)), + PINK(R.color.crop_background_pink, ClearColorItem(1f, 0.827f, 0.87f, 1f)), + FLESH(R.color.crop_background_flesh, ClearColorItem(1f, 0.945f, 0.768f, 1f)), + GREEN(R.color.crop_background_green, ClearColorItem(0.905f, 1f, 0.898f, 1f)), + BLUE(R.color.crop_background_blue, ClearColorItem(0.898f, 0.937f, 1f, 1f)), + BROWN(R.color.crop_background_brown, ClearColorItem(0.85f, 0.807f, 0.745f, 1f)) +} + +class ClearColorItem(val red: Float, val green: Float, val blue: Float, val alpha: Float) \ No newline at end of file diff --git a/matisse/src/main/java/com/zhihu/matisse/ui/widget/TimeSelectorView.kt b/matisse/src/main/java/com/zhihu/matisse/ui/widget/TimeSelectorView.kt new file mode 100644 index 000000000..16235b2f0 --- /dev/null +++ b/matisse/src/main/java/com/zhihu/matisse/ui/widget/TimeSelectorView.kt @@ -0,0 +1,245 @@ +package com.zhihu.matisse.ui.widget + +import android.annotation.SuppressLint +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.graphics.drawable.Drawable +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.View +import androidx.annotation.FloatRange +import androidx.core.content.ContextCompat +import androidx.core.math.MathUtils +import com.zhihu.matisse.R +import java.util.concurrent.TimeUnit + +class TimeSelectorView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + companion object { + private const val ALPHA_SELECTED = 255 + private const val ALPHA_UNSELECTED_ = (255 * 0.38f).toInt() + + private val STATE_PRESSED = intArrayOf(android.R.attr.state_pressed, android.R.attr.state_enabled) + private val STATE_ENABLED = intArrayOf(android.R.attr.state_enabled) + private val STATE_DISABLED = intArrayOf() + } + + private val leftThumbDrawable: Drawable = ContextCompat.getDrawable(context, R.drawable.icon_time_selector)!! + .mutate() + .let { + it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) + it.callback = this + it + } + private val rightThumbDrawable: Drawable = ContextCompat.getDrawable(context, R.drawable.icon_time_selector)!! + .mutate() + .let { + it.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) + it.callback = this + it + } + private val progressLineEnabledColor = ContextCompat.getColor(context, R.color.colorAccent) + private val progressLineDisabledColor = ContextCompat.getColor(context, R.color.gray) + private val progressLinePaint = Paint().apply { + style = Paint.Style.STROKE + strokeWidth = 2 * context.resources.displayMetrics.density + } + private val halfSlop = leftThumbDrawable.intrinsicWidth / 2f + + /** Left pointer position is in range [0..1] of available width. */ + @FloatRange(from = 0.0, to = 1.0) + private var leftPointerPosition: Float = 0f + /** Right pointer position is in range [0..1] of available width. */ + @FloatRange(from = 0.0, to = 1.0) + private var rightPointerPosition: Float = 1f + /** Limit of pointer's positions. */ + @FloatRange(from = 0.0) + private var limit: Float = 1f + var duration: Long = 0 + + /** Left pointer position in pixels from left of view. */ + private val realLeftPointerPosition: Float + get() = paddingLeft + leftPointerPosition * (width - paddingRight - paddingLeft) + /** Right pointer position in pixels from left of view. */ + private val realRightPointerPosition: Float + get() = paddingLeft + rightPointerPosition * (width - paddingRight - paddingLeft) + + /** [onTouchEvent] implementation's variables. */ + private var oldX: Float = 0f + private var oldY: Float = 0f + private var touched: Boolean = false + private var touchedRight: Boolean = false + + /** Listeners. **/ + var selectionStartListener: ((Boolean, Float, Float) -> Unit)? = null + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val heightMode = MeasureSpec.getMode(heightMeasureSpec) + val newHeightMeasureSpec = if (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED) { + MeasureSpec.makeMeasureSpec( + leftThumbDrawable.intrinsicHeight + paddingTop + paddingBottom, MeasureSpec.EXACTLY) + } else { + heightMeasureSpec + } + super.onMeasure(widthMeasureSpec, newHeightMeasureSpec) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + val lineWidth = width - paddingRight - paddingLeft + val drawLeft = paddingLeft.toFloat() + val drawRight = (width - paddingRight).toFloat() + val lineY = height / 2f + progressLinePaint.color = if (isEnabled) progressLineEnabledColor else progressLineDisabledColor + progressLinePaint.alpha = ALPHA_UNSELECTED_ + canvas.drawLine(drawLeft, lineY, drawRight, lineY, progressLinePaint) + val leftBorder = drawLeft + lineWidth * leftPointerPosition + val rightBorder = drawLeft + lineWidth * rightPointerPosition + progressLinePaint.alpha = ALPHA_SELECTED + canvas.drawLine(leftBorder, lineY, rightBorder, lineY, progressLinePaint) + canvas.save() + canvas.translate( + leftBorder - leftThumbDrawable.intrinsicWidth / 2, + lineY - rightThumbDrawable.intrinsicHeight / 2 + ) + leftThumbDrawable.draw(canvas) + canvas.restore() + canvas.save() + canvas.translate( + rightBorder - rightThumbDrawable.intrinsicWidth / 2, + lineY - rightThumbDrawable.intrinsicHeight / 2 + ) + rightThumbDrawable.draw(canvas) + canvas.restore() + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent): Boolean { + // ClickableViewAccessibility - super.onTouchEvent called. + if (isEnabled) { + when (event.action) { + MotionEvent.ACTION_DOWN -> { + oldX = event.x + oldY = event.y + if (event.x > realRightPointerPosition - halfSlop && + event.x < realRightPointerPosition + halfSlop) { + touched = true + touchedRight = true + } else if (event.x > realLeftPointerPosition - halfSlop && + event.x < realLeftPointerPosition + halfSlop) { + touched = true + touchedRight = false + } + if (touched) { + refreshDrawableState() + notifyStartChanged(true) + invalidate() + super.onTouchEvent(event) + return true + } + } + MotionEvent.ACTION_MOVE -> { + if (touched) { + changePosition(event) + oldX = event.x + oldY = event.y + invalidate() + super.onTouchEvent(event) + return true + } + } + MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> { + if (touched) { + touched = false + refreshDrawableState() + notifyStartChanged(false) + invalidate() + super.onTouchEvent(event) + return true + } + } + } + } + return super.onTouchEvent(event) + } + + override fun drawableStateChanged() { + super.drawableStateChanged() + var changed = false + if (isEnabled) { + changed = changed or leftThumbDrawable.setState( + if (touched && !touchedRight) STATE_PRESSED else STATE_ENABLED) + changed = changed or rightThumbDrawable.setState( + if (touched && touchedRight) STATE_PRESSED else STATE_ENABLED) + } else { + changed = changed or leftThumbDrawable.setState(STATE_DISABLED) + changed = changed or rightThumbDrawable.setState(STATE_DISABLED) + } + if (changed) { + invalidate() + } + } + + /** + * Set limit for selection. + * User can't move pointer such way, that pointer's positions delta is more than [range]. + * Default value is ```1```. + * @throws IllegalArgumentException if range less than 0. + */ + fun setLimit(@FloatRange(from = 0.0) range: Float) { + require(range > 0f) { "Limit can't be less than 0" } + + limit = range + validateRightPosition() + validateLeftPosition() + } + + private fun changePosition(event: MotionEvent) { + val deltaX = (event.x - oldX) / (width - paddingLeft - paddingRight) + if (touchedRight) { + rightPointerPosition += deltaX + validateRightPosition() + } else { + leftPointerPosition += deltaX + validateLeftPosition() + } + } + + private fun validateRightPosition() { + rightPointerPosition = MathUtils.clamp(rightPointerPosition, 0f, 1f) + // it is not possible to swap pointers + if (rightPointerPosition < leftPointerPosition) { + leftPointerPosition = rightPointerPosition + } + // it is not possible to move pointer above limit + if (rightPointerPosition - leftPointerPosition > limit) { + rightPointerPosition = leftPointerPosition + limit + } + } + + private fun validateLeftPosition() { + leftPointerPosition = MathUtils.clamp(leftPointerPosition, 0f, 1f) + // it is not possible to swap pointers + if (leftPointerPosition > rightPointerPosition) { + rightPointerPosition = leftPointerPosition + } + // it is not possible to move pointer above limit + if (rightPointerPosition - leftPointerPosition > limit) { + leftPointerPosition = rightPointerPosition - limit + } + } + + private fun notifyStartChanged(started: Boolean) { + selectionStartListener?.apply { invoke(started, leftPointerPosition, rightPointerPosition) } + } + + fun getCurrent(): List = + arrayOf(leftPointerPosition * duration , rightPointerPosition * duration) + .map { TimeUnit.MICROSECONDS.toMillis(it.toLong()) } + +} diff --git a/matisse/src/main/res/drawable/icon_time_selector.xml b/matisse/src/main/res/drawable/icon_time_selector.xml new file mode 100644 index 000000000..c15911541 --- /dev/null +++ b/matisse/src/main/res/drawable/icon_time_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/matisse/src/main/res/drawable/icon_time_selector_disabled.xml b/matisse/src/main/res/drawable/icon_time_selector_disabled.xml new file mode 100644 index 000000000..c712e3afe --- /dev/null +++ b/matisse/src/main/res/drawable/icon_time_selector_disabled.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/matisse/src/main/res/drawable/icon_time_selector_normal.xml b/matisse/src/main/res/drawable/icon_time_selector_normal.xml new file mode 100644 index 000000000..13c09c854 --- /dev/null +++ b/matisse/src/main/res/drawable/icon_time_selector_normal.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/matisse/src/main/res/drawable/icon_time_selector_size.xml b/matisse/src/main/res/drawable/icon_time_selector_size.xml new file mode 100644 index 000000000..f0845507c --- /dev/null +++ b/matisse/src/main/res/drawable/icon_time_selector_size.xml @@ -0,0 +1,7 @@ + + + + \ No newline at end of file diff --git a/matisse/src/main/res/layout/activity_matisse.xml b/matisse/src/main/res/layout/activity_matisse.xml index 99aacd18b..86cff3995 100644 --- a/matisse/src/main/res/layout/activity_matisse.xml +++ b/matisse/src/main/res/layout/activity_matisse.xml @@ -20,7 +20,7 @@ android:layout_height="match_parent" android:orientation="vertical"> - - + - - + - - + + + + + + + + + + + + + + + + + + + + + +