diff options
author | Allan Wang <me@allanwang.ca> | 2017-07-31 23:02:01 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-07-31 23:02:01 -0700 |
commit | 48213d0b427c478865c75fee912ff1ae8bbaffb5 (patch) | |
tree | 7aef1d8400fc3403ee5a40aba945f33a95319359 /mediapicker | |
parent | 8a4e9fd44dfbcf58aa7ab63167dcbdf8752db7d0 (diff) | |
download | kau-48213d0b427c478865c75fee912ff1ae8bbaffb5.tar.gz kau-48213d0b427c478865c75fee912ff1ae8bbaffb5.tar.bz2 kau-48213d0b427c478865c75fee912ff1ae8bbaffb5.zip |
Major update to core and kotterknife; create mediapicker (#15)
* Readme
* Fix kau direction bits
* Truly support transparent ripples
* Update changelog
* Test rect as base
* Replace fab transition with generic fade scale transition
* Add scalexy func
* Add scaleXY
* Add arguments to fadeScaleTransition
* Clean up ink indicator
* Create setOnSingleTapListener
* Fix lint and add rndColor
* Create kotterknife resettables
* Add readme and missing object
* Create lazy resettable registered
* Update core docs
* Opt for separate class for resettable registry
* Clean up resettable registry
* Rename functions
* Add ripple callback listener
* Adjust kprefactivity desc color
* Add more transitions
* Add delete keys option
* Add instrumentation tests
* switch id
* Revert automatic instrumental tests
* Generify imagepickercore and prepare video alternative
* Create working video picker
* Address possible null issue
* Update searchview
* Make layouts public
* Add changelog test
* Update logo link
* Add custom color gif
Diffstat (limited to 'mediapicker')
28 files changed, 1032 insertions, 0 deletions
diff --git a/mediapicker/.gitignore b/mediapicker/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/mediapicker/.gitignore @@ -0,0 +1 @@ +/build diff --git a/mediapicker/README.md b/mediapicker/README.md new file mode 100644 index 0000000..a743a47 --- /dev/null +++ b/mediapicker/README.md @@ -0,0 +1,43 @@ +# KAU :mediapicker + +MediaPicker is a beautiful collection of gallery activities that allow you to pick images or videos +from your storage. It is backed by FastAdapter and Glide, and stems from the PickerCore model. + + +Currently, there are two options: +Each takes in a MediaType argument, to specify whether it queries images or videos + +-------------------------------- + +## MediaPickerActivityBase + +A full screen multi media picker with beautiful animations. +Items are blurred when selected, and there is a counter on the top right. +There is a FAB to send back the response. + +`R.style.Kau.MediaPicker` is added for your convenience. + +## MediaPickerActivityOverlayBase + +This overlaying activity makes use of transitions and nested scrolling, and is only for Lollipop and up. +Only one item can be selected, so the overlay exists immediately upon the first selection. +Having this model also means that each item is only one simple image, as opposed to the blurrable image view above. +As a result, this activity has faster loading on scrolling. + +`R.style.Kau.MediaPicker.Overlay` is added for your convenience. + +-------------------------------- + +Both activities work out of the box and can be extended without needing further modifications. +Their convenience styles default to a slide in slide out animation from the bottom edge. + +You may also easily launch either activity through the simple binder: +``` +Activity.kauLaunchMediaPicker(YourClass::class.java, yourRequestCode) +``` + +Note that this launches the activity through a `startActivityForResult` call + +You may get the activity response by overriding your `onActivityResult` method +to first verify that the request code matches and then call `kauOnMediaPickerResult`, +which will return the list of MediaModels.
\ No newline at end of file diff --git a/mediapicker/build.gradle b/mediapicker/build.gradle new file mode 100644 index 0000000..813ec20 --- /dev/null +++ b/mediapicker/build.gradle @@ -0,0 +1,13 @@ +ext.kauSubModuleMinSdk = project.CORE_MIN_SDK + +apply from: '../android-lib.gradle' + +dependencies { + compile project(':core-ui') + + compile "com.github.bumptech.glide:glide:${GLIDE}" + kapt "com.github.bumptech.glide:compiler:${GLIDE}" + compile "jp.wasabeef:blurry:${BLURRY}" +} + +apply from: '../artifacts.gradle' diff --git a/mediapicker/progress-proguard.txt b/mediapicker/progress-proguard.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/mediapicker/progress-proguard.txt @@ -0,0 +1 @@ + diff --git a/mediapicker/src/main/AndroidManifest.xml b/mediapicker/src/main/AndroidManifest.xml new file mode 100644 index 0000000..89dccc2 --- /dev/null +++ b/mediapicker/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest package="ca.allanwang.kau.imagepicker" /> diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt new file mode 100644 index 0000000..0dcd7a2 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt @@ -0,0 +1,158 @@ +package ca.allanwang.kau.imagepicker + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import ca.allanwang.kau.ui.views.MeasureSpecContract +import ca.allanwang.kau.ui.views.MeasureSpecDelegate +import ca.allanwang.kau.utils.* +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import jp.wasabeef.blurry.internal.BlurFactor +import jp.wasabeef.blurry.internal.BlurTask + +/** + * Created by Allan Wang on 2017-07-14. + * + * ImageView that can be blurred and selected + * The frame is composed of three layers: the base, the blur, and the foreground + * Images should be placed in the base view, and the blur view should not be touched + * as the class will handle it + * The foreground by default contains a white checkmark, but can be customized or hidden depending on the situation + */ +class BlurredImageView @JvmOverloads constructor( + context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0 +) : FrameLayout(context, attrs, defStyleAttr), MeasureSpecContract by MeasureSpecDelegate() { + + private var blurred = false + val imageBase: ImageView by bindView(R.id.image_base) + internal val imageBlur: ImageView by bindView(R.id.image_blur) + val imageForeground: ImageView by bindView(R.id.image_foreground) + + init { + inflate(R.layout.kau_blurred_imageview, true) + initAttrs(context, attrs) + imageForeground.setIcon(GoogleMaterial.Icon.gmd_check, 30) + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val result = onMeasure(this, widthMeasureSpec, heightMeasureSpec) + super.onMeasure(result.first, result.second) + } + + override fun clearAnimation() { + super.clearAnimation() + imageBase.clearAnimation() + imageBlur.clearAnimation() + imageForeground.clearAnimation() + } + + private fun View.scaleAnimate(scale: Float) = animate().scaleXY(scale).setDuration(ANIMATION_DURATION) + private fun View.alphaAnimate(alpha: Float) = animate().alpha(alpha).setDuration(ANIMATION_DURATION) + + + fun isBlurred(): Boolean { + return blurred + } + + /** + * Applies a blur and fills the blur image asynchronously + * When ready, scales the image down and shows the blur & foreground + */ + fun blur() { + if (blurred) return + blurred = true + val factor = BlurFactor() + factor.width = width + factor.height = height + BlurTask(imageBase, factor) { + imageBlur.setImageDrawable(it) + scaleAnimate(ANIMATION_SCALE).start() + imageBlur.alphaAnimate(1f).start() + imageForeground.alphaAnimate(1f).start() + }.execute() + } + + /** + * Clears animations and blurs the image without further animations + * This method is relatively instantaneous, as retrieving the blurred image + * is still asynchronous and takes time + */ + fun blurInstantly() { + blurred = true + clearAnimation() + val factor = BlurFactor() + factor.width = width + factor.height = height + BlurTask(imageBase, factor) { drawable -> + imageBlur.setImageDrawable(drawable) + scaleXY = ANIMATION_SCALE + imageBlur.alpha = 1f + imageForeground.alpha = 1f + }.execute() + } + + /** + * Animate view back to original state and remove drawable when finished + */ + fun removeBlur() { + if (!blurred) return + blurred = false + scaleAnimate(1.0f).start() + imageBlur.alphaAnimate(0f).withEndAction { imageBlur.setImageDrawable(null) }.start() + imageForeground.alphaAnimate(0f).start() + } + + + /** + * Clear all animations and unblur the image + */ + fun removeBlurInstantly() { + blurred = false + clearAnimation() + scaleX = 1.0f + scaleX = 1.0f + imageBlur.alpha = 0f + imageBlur.setImageDrawable(null) + imageForeground.alpha = 0f + } + + /** + * Switch blur state and apply transition + * + * @return true if new state is blurred; false otherwise + */ + fun toggleBlur(): Boolean { + if (blurred) removeBlur() + else blur() + return blurred + } + + /** + * Clears all of the blur effects to restore the original states + * If views were modified in other ways, this method won't affect it + */ + fun reset() { + removeBlurInstantly() + imageBase.setImageDrawable(null) + } + + /** + * Reset most of possible changes to the view + */ + fun fullReset() { + reset() + fullAction({ it.visible().background = null }) + imageForeground.setBackgroundColorRes(R.color.kau_blurred_image_selection_overlay) + imageForeground.setIcon(GoogleMaterial.Icon.gmd_check, 30, Color.WHITE) + } + + private fun fullAction(action: (View) -> Unit) { + action(this) + action(imageBase) + action(imageBlur) + action(imageForeground) + } +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItem.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItem.kt new file mode 100644 index 0000000..4b70638 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItem.kt @@ -0,0 +1,69 @@ +package ca.allanwang.kau.imagepicker + +import android.graphics.drawable.Drawable +import android.support.v7.widget.RecyclerView +import android.view.View +import ca.allanwang.kau.iitems.KauIItem +import ca.allanwang.kau.utils.bindView +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.mikepenz.fastadapter.FastAdapter + +/** + * Created by Allan Wang on 2017-07-04. + */ +class MediaItem(val data: MediaModel) + : KauIItem<MediaItem, MediaItem.ViewHolder>(R.layout.kau_iitem_image, { ViewHolder(it) }) { + + private var failedToLoad = false + + companion object { + fun bindEvents(fastAdapter: FastAdapter<MediaItem>) { + fastAdapter.withMultiSelect(true) + .withSelectable(true) + //adapter selector occurs before the on click event + .withOnClickListener { v, _, item, _ -> + val image = v as BlurredImageView + if (item.isSelected) image.blur() + else image.removeBlur() + true + } + } + } + + override fun isSelectable(): Boolean = !failedToLoad + + override fun bindView(holder: ViewHolder, payloads: List<Any>?) { + super.bindView(holder, payloads) + Glide.with(holder.itemView) + .load(data.data) + .listener(object : RequestListener<Drawable> { + override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>, isFirstResource: Boolean): Boolean { + failedToLoad = true + holder.container.imageBase.setImageDrawable(MediaPickerCore.getErrorDrawable(holder.itemView.context)) + return true; + } + + override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>, dataSource: DataSource, isFirstResource: Boolean): Boolean { + holder.container.imageBase.setImageDrawable(resource) + if (isSelected) holder.container.blurInstantly() + return true; + } + }) + .into(holder.container.imageBase) + } + + override fun unbindView(holder: ViewHolder) { + super.unbindView(holder) + Glide.with(holder.itemView).clear(holder.container.imageBase) + holder.container.removeBlurInstantly() + failedToLoad = false + } + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + val container: BlurredImageView by bindView(R.id.kau_image) + } +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItemBasic.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItemBasic.kt new file mode 100644 index 0000000..7a1ebf7 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItemBasic.kt @@ -0,0 +1,71 @@ +package ca.allanwang.kau.imagepicker + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.graphics.drawable.Drawable +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.RecyclerView +import android.view.View +import ca.allanwang.kau.iitems.KauIItem +import ca.allanwang.kau.ui.views.MeasuredImageView +import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.buildIsLollipopAndUp +import com.bumptech.glide.Glide +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import com.mikepenz.fastadapter.FastAdapter + +/** + * Created by Allan Wang on 2017-07-04. + */ +class MediaItemBasic(val data: MediaModel) + : KauIItem<MediaItem, MediaItemBasic.ViewHolder>(R.layout.kau_iitem_image_basic, { ViewHolder(it) }) { + + companion object { + @SuppressLint("NewApi") + fun bindEvents(activity: Activity, fastAdapter: FastAdapter<MediaItemBasic>) { + fastAdapter.withSelectable(false) + //add image data and return right away + .withOnClickListener { _, _, item, _ -> + val intent = Intent() + val data = arrayListOf(item.data) + intent.putParcelableArrayListExtra(MEDIA_PICKER_RESULT, data) + activity.setResult(AppCompatActivity.RESULT_OK, intent) + if (buildIsLollipopAndUp) activity.finishAfterTransition() + else activity.finish() + true + } + } + } + + override fun isSelectable(): Boolean = false + + override fun bindView(holder: ViewHolder, payloads: List<Any>?) { + super.bindView(holder, payloads) + Glide.with(holder.itemView) + .load(data.data) + .listener(object : RequestListener<Drawable> { + override fun onLoadFailed(e: GlideException?, model: Any, target: Target<Drawable>, isFirstResource: Boolean): Boolean { + holder.image.setImageDrawable(MediaPickerCore.getErrorDrawable(holder.itemView.context)) + return true; + } + + override fun onResourceReady(resource: Drawable, model: Any, target: Target<Drawable>, dataSource: DataSource, isFirstResource: Boolean): Boolean { + return false + } + }) + .into(holder.image) + } + + override fun unbindView(holder: ViewHolder) { + super.unbindView(holder) + Glide.with(holder.itemView).clear(holder.image) + } + + class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { + val image: MeasuredImageView by bindView(R.id.kau_image) + } +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaModel.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaModel.kt new file mode 100644 index 0000000..c384d48 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaModel.kt @@ -0,0 +1,78 @@ +package ca.allanwang.kau.imagepicker + +import android.database.Cursor +import android.database.SQLException +import android.net.Uri +import android.os.Parcel +import android.os.Parcelable +import android.provider.MediaStore +import android.support.annotation.NonNull +import java.io.File + + +/** + * Created by Allan Wang on 2017-07-14. + */ + +data class MediaModel( + val data: String, val mimeType: String, val size: Long, val dateModified: Long, val displayName: String? +) : Parcelable { + + @Throws(SQLException::class) + constructor(@NonNull cursor: Cursor) : this( + cursor.getString(0), + cursor.getString(1), + cursor.getLong(2), + cursor.getLong(3), + cursor.getString(4) + ) + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readLong(), + parcel.readLong(), + parcel.readString()) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(this.data) + parcel.writeString(this.mimeType) + parcel.writeLong(this.size) + parcel.writeLong(this.dateModified) + parcel.writeString(this.displayName) + } + + val isGif + get() = mimeType.endsWith("gif") + + val isImage + get() = mimeType.endsWith("image") + + val isVideo + get() = mimeType.endsWith("video") + + val uri: Uri by lazy { Uri.fromFile(File(data)) } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator<MediaModel> { + val projection = arrayOf( + MediaStore.MediaColumns.DATA, + MediaStore.MediaColumns.MIME_TYPE, + MediaStore.MediaColumns.SIZE, + MediaStore.MediaColumns.DATE_MODIFIED, + MediaStore.MediaColumns.DISPLAY_NAME + ) + + override fun createFromParcel(parcel: Parcel): MediaModel { + return MediaModel(parcel) + } + + override fun newArray(size: Int): Array<MediaModel?> { + return arrayOfNulls(size) + } + } + +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityBase.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityBase.kt new file mode 100644 index 0000000..ee68f42 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityBase.kt @@ -0,0 +1,100 @@ +package ca.allanwang.kau.imagepicker + +import android.content.Intent +import android.database.Cursor +import android.os.Bundle +import android.support.design.widget.AppBarLayout +import android.support.design.widget.CoordinatorLayout +import android.support.design.widget.FloatingActionButton +import android.support.v4.content.Loader +import android.support.v7.widget.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.Toolbar +import android.widget.TextView +import ca.allanwang.kau.utils.* +import com.mikepenz.google_material_typeface_library.GoogleMaterial + +/** + * Created by Allan Wang on 2017-07-04. + * + * Base activity for selecting images from storage + * Images are blurred when selected, and multiple images can be selected at a time. + * Having three layered images makes this slightly slower than [MediaPickerActivityOverlayBase] + */ +abstract class MediaPickerActivityBase(mediaType: MediaType) : MediaPickerCore<MediaItem>(mediaType) { + + val coordinator: CoordinatorLayout by bindView(R.id.kau_coordinator) + val toolbar: Toolbar by bindView(R.id.kau_toolbar) + val selectionCount: TextView by bindView(R.id.kau_selection_count) + val recycler: RecyclerView by bindView(R.id.kau_recyclerview) + val fab: FloatingActionButton by bindView(R.id.kau_fab) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContentView(R.layout.kau_activity_image_picker) + + selectionCount.setCompoundDrawables(null, null, GoogleMaterial.Icon.gmd_image.toDrawable(this, 18), null) + + setSupportActionBar(toolbar) + supportActionBar?.apply { + setDisplayHomeAsUpEnabled(true) + setDisplayShowHomeEnabled(true) + setHomeAsUpIndicator(GoogleMaterial.Icon.gmd_close.toDrawable(this@MediaPickerActivityBase, 18)) + } + toolbar.setNavigationOnClickListener { onBackPressed() } + + initializeRecycler(recycler) + + MediaItem.bindEvents(adapter) + adapter.withSelectionListener({ _, _ -> selectionCount.text = adapter.selections.size.toString() }) + + fab.apply { + show() + setIcon(GoogleMaterial.Icon.gmd_send) + setOnClickListener { + val selection = adapter.selectedItems + if (selection.isEmpty()) { + toast(R.string.kau_no_items_selected) + } else { + val intent = Intent() + val data = ArrayList(selection.map { it.data }) + intent.putParcelableArrayListExtra(MEDIA_PICKER_RESULT, data) + setResult(RESULT_OK, intent) + finish() + } + } + hideOnDownwardsScroll(recycler) + } + + loadItems() + } + + override fun converter(model: MediaModel): MediaItem = MediaItem(model) + + /** + * Decide whether the toolbar can hide itself + * We typically want this behaviour unless we don't have enough images + * to fill the entire screen. In that case we don't want the recyclerview to be scrollable + * which means the toolbar shouldn't scroll either + + * @param scrollable true if scroll flags are enabled, false otherwise + */ + private fun setToolbarScrollable(scrollable: Boolean) { + val params = toolbar.layoutParams as AppBarLayout.LayoutParams + if (scrollable) + params.scrollFlags = AppBarLayout.LayoutParams.SCROLL_FLAG_ENTER_ALWAYS or AppBarLayout.LayoutParams.SCROLL_FLAG_SCROLL + else + params.scrollFlags = 0 + } + + override fun onLoadFinished(loader: Loader<Cursor>?, data: Cursor?) { + super.onLoadFinished(loader, data) + setToolbarScrollable((recycler.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() < adapter.getItemCount() - 1) + } + + override fun onStatusChange(loaded: Boolean) { + setToolbarScrollable(loaded) + } + +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityOverlayBase.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityOverlayBase.kt new file mode 100644 index 0000000..5161c08 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityOverlayBase.kt @@ -0,0 +1,51 @@ +package ca.allanwang.kau.imagepicker + +import android.os.Build +import android.os.Bundle +import android.support.annotation.RequiresApi +import android.support.v7.widget.RecyclerView +import ca.allanwang.kau.ui.widgets.ElasticDragDismissFrameLayout +import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.toast + +/** + * Created by Allan Wang on 2017-07-23. + * + * Base activity for selecting images from storage + * This variant is an overlay and selects one image only before returning directly + * It is more efficient than [MediaPickerActivityBase], as all images are one layer deep + * as opposed to three layers deep + */ +@RequiresApi(Build.VERSION_CODES.LOLLIPOP) +abstract class MediaPickerActivityOverlayBase(mediaType: MediaType) : MediaPickerCore<MediaItemBasic>(mediaType) { + + val draggable: ElasticDragDismissFrameLayout by bindView(R.id.kau_draggable) + val recycler: RecyclerView by bindView(R.id.kau_recyclerview) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.kau_activity_image_picker_overlay) + initializeRecycler(recycler) + MediaItemBasic.bindEvents(this, adapter) + + draggable.addExitListener(this, R.transition.kau_image_exit_bottom, R.transition.kau_image_exit_top) + draggable.setOnClickListener { finishAfterTransition() } + + loadItems() + } + + override fun finishAfterTransition() { + recycler.stopScroll() + super.finishAfterTransition() + } + + override fun onStatusChange(loaded: Boolean) { + if (!loaded) toast(R.string.kau_no_items_loaded) + } + + override fun converter(model: MediaModel): MediaItemBasic = MediaItemBasic(model) + + override fun onBackPressed() { + finishAfterTransition() + } +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerBinder.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerBinder.kt new file mode 100644 index 0000000..e423e84 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerBinder.kt @@ -0,0 +1,33 @@ +package ca.allanwang.kau.imagepicker + +import android.app.Activity +import android.content.Intent +import ca.allanwang.kau.utils.startActivityForResult + +/** + * Created by Allan Wang on 2017-07-21. + * + * Extension functions for interacting with the image picker + * as well as internal constants + */ + +/** + * Image picker launchers + */ +fun Activity.kauLaunchMediaPicker(clazz: Class<out MediaPickerCore<*>>, requestCode: Int) { + startActivityForResult(clazz, requestCode, transition = MediaPickerActivityOverlayBase::class.java.isAssignableFrom(clazz)) +} + +/** + * Image picker result + * call under [Activity.onActivityResult] + * and make sure that the requestCode matches first + */ +fun Activity.kauOnMediaPickerResult(resultCode: Int, data: Intent?) = MediaPickerCore.onMediaPickerResult(resultCode, data) + +internal const val LOADER_ID = 42 +internal const val MEDIA_PICKER_RESULT = "media_picker_result" + +internal const val ANIMATION_DURATION = 200L +internal const val ANIMATION_SCALE = 0.95f + diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerCore.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerCore.kt new file mode 100644 index 0000000..4b922f9 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerCore.kt @@ -0,0 +1,182 @@ +package ca.allanwang.kau.imagepicker + +import android.Manifest +import android.app.Activity +import android.content.Context +import android.content.Intent +import android.database.Cursor +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.provider.MediaStore +import android.support.v4.app.LoaderManager +import android.support.v4.content.CursorLoader +import android.support.v4.content.Loader +import android.support.v7.app.AppCompatActivity +import android.support.v7.widget.GridLayoutManager +import android.support.v7.widget.RecyclerView +import ca.allanwang.kau.animators.FadeScaleAnimatorAdd +import ca.allanwang.kau.animators.KauAnimator +import ca.allanwang.kau.permissions.kauRequestPermissions +import ca.allanwang.kau.utils.dimenPixelSize +import ca.allanwang.kau.utils.toast +import com.mikepenz.fastadapter.IItem +import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.IconicsDrawable + +/** + * Created by Allan Wang on 2017-07-23. + * + * Container for the main logic behind the both pickers + */ +abstract class MediaPickerCore<T : IItem<*, *>>(val mediaType: MediaType) : AppCompatActivity(), LoaderManager.LoaderCallbacks<Cursor> { + + companion object { + /** + * Given the dimensions of our device and a minimum image size, + * Computer the optimal column count for our grid layout + * + * @return column count + */ + fun computeColumnCount(context: Context): Int { + val minImageSizePx = context.dimenPixelSize(R.dimen.kau_image_minimum_size) + val screenWidthPx = context.resources.displayMetrics.widthPixels + return screenWidthPx / minImageSizePx + } + + /** + * Compute our resulting image size + */ + fun computeViewSize(context: Context): Int { + val screenWidthPx = context.resources.displayMetrics.widthPixels + return screenWidthPx / computeColumnCount(context) + } + + /** + * Create error tile for a given item + */ + fun getErrorDrawable(context: Context): Drawable { + val sizePx = MediaPickerCore.computeViewSize(context) + return IconicsDrawable(context, GoogleMaterial.Icon.gmd_error) + .sizePx(sizePx) + .backgroundColor(accentColor) + .paddingPx(sizePx / 3) + .color(Color.WHITE) + } + + var accentColor: Int = 0xff666666.toInt() + + /** + * Helper method to retrieve the media from our media picker + * This is used for both single and multiple photo picks + */ + fun onMediaPickerResult(resultCode: Int, data: Intent?): List<MediaModel> { + if (resultCode != Activity.RESULT_OK || data == null || !data.hasExtra(MEDIA_PICKER_RESULT)) + return emptyList() + return data.getParcelableArrayListExtra(MEDIA_PICKER_RESULT) + } + + /** + * Number of loaded items we should cache + * This is arbitrary + */ + const val CACHE_SIZE = 80 + } + + val adapter: FastItemAdapter<T> = FastItemAdapter() + + /** + * Further improve preloading by extending the layout space + */ + val extraSpace: Int by lazy { resources.displayMetrics.heightPixels } + + fun initializeRecycler(recycler: RecyclerView) { + recycler.apply { + val manager = object : GridLayoutManager(context, computeColumnCount(context)) { + override fun getExtraLayoutSpace(state: RecyclerView.State?): Int { + return if (mediaType != MediaType.VIDEO) extraSpace else super.getExtraLayoutSpace(state) + } + } + setItemViewCacheSize(CACHE_SIZE) + isDrawingCacheEnabled = true + layoutManager = manager + adapter = this@MediaPickerCore.adapter + setHasFixedSize(true) + itemAnimator = KauAnimator(FadeScaleAnimatorAdd(0.8f)) + } + } + + //Sort by descending date + var sortQuery = MediaStore.MediaColumns.DATE_MODIFIED + " DESC" + + override fun onCreateLoader(id: Int, args: Bundle?): Loader<Cursor> { + return CursorLoader(this, mediaType.contentUri, MediaModel.projection, null, null, sortQuery) + } + + /** + * Request read permissions and load all external items + * The result will be filtered through {@link #onLoadFinished(Loader, Cursor)} + * Call this to make sure that we request permissions each time + * The adapter will be cleared on each successful call + */ + open fun loadItems() { + kauRequestPermissions(Manifest.permission.READ_EXTERNAL_STORAGE) { + granted, _ -> + if (granted) { + supportLoaderManager.initLoader(LOADER_ID, null, this) + onStatusChange(true) + } else { + toast(R.string.kau_permission_denied) + onStatusChange(false) + } + } + } + + override fun onLoadFinished(loader: Loader<Cursor>?, data: Cursor?) { + reset() + if (data == null || !data.moveToFirst()) { + toast(R.string.kau_no_items_found) + onStatusChange(false) + return + } + val items = mutableListOf<T>() + do { + val model = MediaModel(data) + if (!shouldLoad(model)) continue + items.add(converter(model)) + } while (data.moveToNext()) + addItems(items) + } + + abstract fun converter(model: MediaModel): T + + override fun onLoaderReset(loader: Loader<Cursor>?) = reset() + + /** + * Called at the end of [onLoadFinished] + * when the adapter should add the items + */ + open fun addItems(items: List<T>) { + adapter.add(items) + } + + /** + * Clears the adapter to prepare for a new load + */ + open fun reset() { + adapter.clear() + } + + /** + * Optional filter to decide which items get displayed + * Defaults to checking their sizes to filter out + * very small items such as lurking drawables/icons + * + * Returns true if model should be displayed, false otherwise + */ + open fun shouldLoad(model: MediaModel): Boolean = model.size > 10000L + + open fun onStatusChange(loaded: Boolean) {} + +}
\ No newline at end of file diff --git a/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaType.kt b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaType.kt new file mode 100644 index 0000000..c934e04 --- /dev/null +++ b/mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaType.kt @@ -0,0 +1,13 @@ +package ca.allanwang.kau.imagepicker + +import android.net.Uri +import android.provider.MediaStore +import com.bumptech.glide.load.engine.DiskCacheStrategy + +/** + * Created by Allan Wang on 2017-07-30. + */ +enum class MediaType(val cacheStrategy: DiskCacheStrategy, val contentUri: Uri) { + IMAGE(DiskCacheStrategy.AUTOMATIC, MediaStore.Images.Media.EXTERNAL_CONTENT_URI), + VIDEO(DiskCacheStrategy.AUTOMATIC, MediaStore.Video.Media.EXTERNAL_CONTENT_URI) +}
\ No newline at end of file diff --git a/mediapicker/src/main/res-public/values-v21/styles.xml b/mediapicker/src/main/res-public/values-v21/styles.xml new file mode 100644 index 0000000..a5b9d96 --- /dev/null +++ b/mediapicker/src/main/res-public/values-v21/styles.xml @@ -0,0 +1,9 @@ +<resources> + + <style name="Kau.MediaPicker.Overlay" parent="Kau.Translucent"> + <item name="android:statusBarColor">@android:color/transparent</item> + <item name="android:windowEnterTransition">@transition/kau_image_enter</item> + <item name="android:windowReturnTransition">@transition/kau_image_exit_bottom</item> + </style> + +</resources> diff --git a/mediapicker/src/main/res-public/values/colors.xml b/mediapicker/src/main/res-public/values/colors.xml new file mode 100644 index 0000000..ebaa3f7 --- /dev/null +++ b/mediapicker/src/main/res-public/values/colors.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <color name="kau_blurred_image_selection_overlay">#30000000</color> + +</resources>
\ No newline at end of file diff --git a/mediapicker/src/main/res-public/values/dimens.xml b/mediapicker/src/main/res-public/values/dimens.xml new file mode 100644 index 0000000..3ff2dd7 --- /dev/null +++ b/mediapicker/src/main/res-public/values/dimens.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + + <dimen name="kau_image_minimum_size">120dp</dimen> + +</resources>
\ No newline at end of file diff --git a/mediapicker/src/main/res-public/values/public.xml b/mediapicker/src/main/res-public/values/public.xml new file mode 100644 index 0000000..ac608bb --- /dev/null +++ b/mediapicker/src/main/res-public/values/public.xml @@ -0,0 +1,7 @@ +<resources xmlns:tools='http://schemas.android.com/tools' tools:ignore='ResourceName'> +<!-- AUTO-GENERATED FILE. DO NOT MODIFY. public.xml is generated by the generatepublicxml gradle task --> + <public name='kau_blurred_image_selection_overlay' type='color' /> + <public name='kau_image_minimum_size' type='dimen' /> + <public name='Kau.MediaPicker' type='style' /> + <public name='Kau.MediaPicker.Overlay' type='style' /> +</resources>
\ No newline at end of file diff --git a/mediapicker/src/main/res-public/values/styles.xml b/mediapicker/src/main/res-public/values/styles.xml new file mode 100644 index 0000000..77bf2bd --- /dev/null +++ b/mediapicker/src/main/res-public/values/styles.xml @@ -0,0 +1,12 @@ +<resources> + + <style name="Kau.MediaPicker"> + <item name="android:windowAnimationStyle">@style/KauSlideInSlideOutBottom</item> + </style> + + <!--Just as a placeholder for public.xml--> + <style name="Kau.MediaPicker.Overlay" parent="Kau.Translucent"> + <item name="android:windowAnimationStyle">@null</item> + </style> + +</resources> diff --git a/mediapicker/src/main/res/layout-v21/kau_activity_image_picker_overlay.xml b/mediapicker/src/main/res/layout-v21/kau_activity_image_picker_overlay.xml new file mode 100644 index 0000000..a0ce301 --- /dev/null +++ b/mediapicker/src/main/res/layout-v21/kau_activity_image_picker_overlay.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<ca.allanwang.kau.ui.widgets.ElasticDragDismissFrameLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/kau_draggable" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:dragDismissDistance="@dimen/kau_drag_dismiss_distance_large" + app:dragDismissScale="0.95"> + + <android.support.v7.widget.RecyclerView + android:id="@+id/kau_recyclerview" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_marginTop="@dimen/kau_drag_dismiss_distance" + android:background="?android:colorBackground" /> + +</ca.allanwang.kau.ui.widgets.ElasticDragDismissFrameLayout>
\ No newline at end of file diff --git a/mediapicker/src/main/res/layout/kau_activity_image_picker.xml b/mediapicker/src/main/res/layout/kau_activity_image_picker.xml new file mode 100644 index 0000000..6d991b0 --- /dev/null +++ b/mediapicker/src/main/res/layout/kau_activity_image_picker.xml @@ -0,0 +1,55 @@ +<?xml version="1.0" encoding="utf-8"?> +<android.support.design.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/kau_coordinator" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:background="?android:colorBackground" + android:fitsSystemWindows="true"> + + <android.support.design.widget.AppBarLayout + android:id="@+id/kau_appbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar"> + + <android.support.v7.widget.Toolbar + android:id="@+id/kau_toolbar" + android:layout_width="match_parent" + android:layout_height="?attr/actionBarSize" + app:layout_scrollFlags="scroll|enterAlways" + app:popupTheme="@style/ThemeOverlay.AppCompat.Light"> + + <TextView + android:id="@+id/kau_selection_count" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_gravity="end" + android:drawablePadding="@dimen/kau_padding_small" + android:gravity="center_vertical" + android:paddingEnd="@dimen/kau_padding_normal" + android:paddingStart="@dimen/kau_padding_normal" + android:text="@string/kau_0" /> + + </android.support.v7.widget.Toolbar> + + </android.support.design.widget.AppBarLayout> + + <android.support.v7.widget.RecyclerView + android:id="@+id/kau_recyclerview" + android:layout_width="match_parent" + android:layout_height="match_parent" + app:layout_behavior="@string/appbar_scrolling_view_behavior" /> + + <android.support.design.widget.FloatingActionButton + android:id="@+id/kau_fab" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="@dimen/kau_fab_margin" + android:clickable="true" + app:backgroundTint="?colorAccent" + app:layout_anchor="@id/kau_recyclerview" + app:layout_anchorGravity="bottom|right|end" /> + + +</android.support.design.widget.CoordinatorLayout>
\ No newline at end of file diff --git a/mediapicker/src/main/res/layout/kau_blurred_imageview.xml b/mediapicker/src/main/res/layout/kau_blurred_imageview.xml new file mode 100644 index 0000000..e28cb9a --- /dev/null +++ b/mediapicker/src/main/res/layout/kau_blurred_imageview.xml @@ -0,0 +1,32 @@ +<?xml version="1.0" encoding="utf-8"?> +<merge xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <ImageView + android:id="@+id/image_base" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/kau_blurrable_imageview" + android:scaleType="centerCrop" /> + + <ImageView + android:id="@+id/image_blur" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:alpha="0" + android:contentDescription="@string/kau_blurrable_imageview" + android:scaleType="centerCrop" /> + + <ImageView + android:id="@+id/image_foreground" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_gravity="center" + android:alpha="0" + android:background="@color/kau_blurred_image_selection_overlay" + android:contentDescription="@string/kau_blurrable_imageview" + android:scaleType="centerInside" /> + +</merge>
\ No newline at end of file diff --git a/mediapicker/src/main/res/layout/kau_iitem_image.xml b/mediapicker/src/main/res/layout/kau_iitem_image.xml new file mode 100644 index 0000000..9d51d77 --- /dev/null +++ b/mediapicker/src/main/res/layout/kau_iitem_image.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="utf-8"?> +<ca.allanwang.kau.imagepicker.BlurredImageView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/kau_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="2dp" + android:foreground="@drawable/kau_selectable_white" + app:relativeHeight="1" />
\ No newline at end of file diff --git a/mediapicker/src/main/res/layout/kau_iitem_image_basic.xml b/mediapicker/src/main/res/layout/kau_iitem_image_basic.xml new file mode 100644 index 0000000..b89e41d --- /dev/null +++ b/mediapicker/src/main/res/layout/kau_iitem_image_basic.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="utf-8"?> +<ca.allanwang.kau.ui.views.MeasuredImageView xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/kau_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="2dp" + android:foreground="@drawable/kau_selectable_white" + android:scaleType="centerCrop" + app:relativeHeight="1" />
\ No newline at end of file diff --git a/mediapicker/src/main/res/transition-v21/kau_image_enter.xml b/mediapicker/src/main/res/transition-v21/kau_image_enter.xml new file mode 100644 index 0000000..447c0c9 --- /dev/null +++ b/mediapicker/src/main/res/transition-v21/kau_image_enter.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:interpolator/fast_out_linear_in" + android:transitionOrdering="together"> + + <slide + android:duration="400" + android:slideEdge="bottom"> + <targets android:targetId="@id/kau_draggable" /> + </slide> + + <fade + android:duration="200" + android:startDelay="200" /> + +</transitionSet> diff --git a/mediapicker/src/main/res/transition-v21/kau_image_exit_bottom.xml b/mediapicker/src/main/res/transition-v21/kau_image_exit_bottom.xml new file mode 100644 index 0000000..447c0c9 --- /dev/null +++ b/mediapicker/src/main/res/transition-v21/kau_image_exit_bottom.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:interpolator/fast_out_linear_in" + android:transitionOrdering="together"> + + <slide + android:duration="400" + android:slideEdge="bottom"> + <targets android:targetId="@id/kau_draggable" /> + </slide> + + <fade + android:duration="200" + android:startDelay="200" /> + +</transitionSet> diff --git a/mediapicker/src/main/res/transition-v21/kau_image_exit_top.xml b/mediapicker/src/main/res/transition-v21/kau_image_exit_top.xml new file mode 100644 index 0000000..8d64f48 --- /dev/null +++ b/mediapicker/src/main/res/transition-v21/kau_image_exit_top.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="utf-8"?> +<transitionSet xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:interpolator/fast_out_linear_in" + android:transitionOrdering="together"> + + <slide + android:duration="400" + android:slideEdge="top"> + <targets android:targetId="@id/kau_draggable" /> + </slide> + + <fade + android:duration="200" + android:startDelay="200" /> + +</transitionSet> diff --git a/mediapicker/src/main/res/values/strings.xml b/mediapicker/src/main/res/values/strings.xml new file mode 100644 index 0000000..39ab16b --- /dev/null +++ b/mediapicker/src/main/res/values/strings.xml @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="utf-8"?> +<resources> + <string name="kau_no_items_found">No items found</string> + <string name="kau_no_items_selected">No items have been selected</string> + <string name="kau_blurrable_imageview">Blurrable ImageView</string> + <string name="kau_no_items_loaded">No items loaded</string> +</resources>
\ No newline at end of file |