From 61d87976e8b29ed25061ae98743a6cf4f4274542 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 22 Jul 2017 16:08:08 -0700 Subject: Support sdk 19 where possible and add image picker (#10) * Fix plural * Switch to long * Test plural again * Comment * Major update to image picker and view utils * Make image activity full screen * Update min sdk and prefix * Lower sdk requirement and make string private * Bring kpref activity to sdk 19 --- .../allanwang/kau/imagepicker/BlurredImageView.kt | 17 +- .../ca/allanwang/kau/imagepicker/ImageHelper.kt | 10 -- .../ca/allanwang/kau/imagepicker/ImageItem.kt | 37 ++-- .../ca/allanwang/kau/imagepicker/ImageModel.kt | 49 ++++- .../kau/imagepicker/ImagePickerActivity.kt | 200 +++++++++++++++++++++ .../kau/imagepicker/ImagePickerActivityBase.kt | 114 ------------ .../allanwang/kau/imagepicker/ImagePickerBinder.kt | 34 ++++ 7 files changed, 304 insertions(+), 157 deletions(-) delete mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageHelper.kt create mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivity.kt delete mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt create mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerBinder.kt (limited to 'imagepicker/src/main/kotlin/ca/allanwang') diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt index 8fb5cf3..2ce00ba 100644 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt @@ -2,6 +2,7 @@ package ca.allanwang.kau.imagepicker import android.content.Context import android.graphics.Color +import android.support.annotation.StyleRes import android.util.AttributeSet import android.view.View import android.widget.FrameLayout @@ -17,15 +18,15 @@ import jp.wasabeef.blurry.internal.BlurTask /** * Created by Allan Wang on 2017-07-14. * - * ImageView that is can be blurred and selected + * 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, defStyleRes: Int = 0 -) : FrameLayout(context, attrs, defStyleAttr, defStyleRes), MeasureSpecContract by MeasureSpecDelegate() { + 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) @@ -38,11 +39,6 @@ class BlurredImageView @JvmOverloads constructor( imageForeground.setIcon(GoogleMaterial.Icon.gmd_check, 30) } - companion object { - const val ANIMATION_DURATION = 200L - const val ANIMATION_SCALE = 0.95f - } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { val result = onMeasure(this, widthMeasureSpec, heightMeasureSpec) super.onMeasure(result.first, result.second) @@ -73,13 +69,12 @@ class BlurredImageView @JvmOverloads constructor( val factor = BlurFactor() factor.width = width factor.height = height - val task = BlurTask(imageBase, factor) { + BlurTask(imageBase, factor) { imageBlur.setImageDrawable(it) scaleAnimate(ANIMATION_SCALE).start() imageBlur.alphaAnimate(1f).start() imageForeground.alphaAnimate(1f).start() - } - task.execute() + }.execute() } /** diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageHelper.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageHelper.kt deleted file mode 100644 index 9b45679..0000000 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageHelper.kt +++ /dev/null @@ -1,10 +0,0 @@ -package ca.allanwang.kau.imagepicker - -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.module.LibraryGlideModule - -/** - * Created by Allan Wang on 2017-07-04. - */ -@GlideModule -class KauGlide : LibraryGlideModule() \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt index 852e1e8..d258822 100644 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt @@ -25,33 +25,42 @@ class ImageItem(val data: ImageModel) : KauIItem(R.layout.kau_iitem_image, { ViewHolder(it) }) { private var failedToLoad = false + var withFade = true - fun bindEvents(fastAdapter: FastAdapter) { - fastAdapter.withMultiSelect(true) - fastAdapter.withSelectable(true) - fastAdapter.withOnClickListener { v, _, _, _ -> - val image = v as BlurredImageView - image.toggleBlur() - true + companion object { + fun bindEvents(fastAdapter: FastAdapter) { + 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?) { super.bindView(holder, payloads) - holder.container.alpha = 0f + if (withFade) holder.container.alpha = 0f Glide.with(holder.itemView) .load(data.data) .listener(object : RequestListener { override fun onLoadFailed(e: GlideException?, model: Any, target: Target, isFirstResource: Boolean): Boolean { failedToLoad = true; holder.container.setIcon(GoogleMaterial.Icon.gmd_error); - holder.container.animate().alpha(1f).start(); + if (withFade) holder.container.animate().alpha(1f).start(); return true; } override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { - holder.container.animate().alpha(1f).start(); - return false; + holder.container.imageBase.setImageDrawable(resource) + if (isSelected) holder.container.blurInstantly() + if (withFade) holder.container.animate().alpha(1f).start(); + return true; } }) .into(holder.container.imageBase) @@ -63,14 +72,13 @@ class ImageItem(val data: ImageModel) .sizePx(sizePx) .paddingPx(sizePx / 3) .color(Color.WHITE)) - //todo add background - imageBase.setBackgroundColor(ImagePickerActivityBase.accentColor) + imageBase.setBackgroundColor(ImagePickerActivity.accentColor) imageForeground.gone() } private fun computeViewSize(context: Context): Int { val screenWidthPx = context.resources.displayMetrics.widthPixels - return screenWidthPx / ImagePickerActivityBase.computeColumnCount(context) + return screenWidthPx / ImagePickerActivity.computeColumnCount(context) } override fun unbindView(holder: ViewHolder) { @@ -81,6 +89,7 @@ class ImageItem(val data: ImageModel) } else { holder.container.fullReset() } + failedToLoad = false } class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt index 26e4137..d744650 100644 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt @@ -1,6 +1,8 @@ package ca.allanwang.kau.imagepicker import android.database.Cursor +import android.os.Parcel +import android.os.Parcelable import android.provider.MediaStore import android.support.annotation.NonNull @@ -8,14 +10,45 @@ import android.support.annotation.NonNull /** * Created by Allan Wang on 2017-07-14. */ -class ImageModel(@NonNull cursor: Cursor) { - val size = cursor.getLong(MediaStore.Images.Media.SIZE) - val dateModified = cursor.getLong(MediaStore.Images.Media.DATE_MODIFIED) - val data = cursor.getString(MediaStore.Images.Media.DATA) - val displayName = cursor.getString(MediaStore.Images.Media.DISPLAY_NAME) +data class ImageModel(val size: Long, val dateModified: Long, val data: String, val displayName: String) : Parcelable { - private fun Cursor.getString(name: String) = getString(getColumnIndex(name)) - private fun Cursor.getLong(name: String) = getLong(getColumnIndex(name)) + constructor(@NonNull cursor: Cursor) : this( + cursor.getLong(MediaStore.Images.Media.SIZE), + cursor.getLong(MediaStore.Images.Media.DATE_MODIFIED), + cursor.getString(MediaStore.Images.Media.DATA), + cursor.getString(MediaStore.Images.Media.DISPLAY_NAME) + ) -} \ No newline at end of file + constructor(parcel: Parcel) : this( + parcel.readLong(), + parcel.readLong(), + parcel.readString(), + parcel.readString()) + + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeLong(this.size) + parcel.writeLong(this.dateModified) + parcel.writeString(this.data) + parcel.writeString(this.displayName) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ImageModel { + return ImageModel(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } + +} + +private fun Cursor.getString(name: String) = getString(getColumnIndex(name)) +private fun Cursor.getLong(name: String) = getLong(getColumnIndex(name)) \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivity.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivity.kt new file mode 100644 index 0000000..814cde4 --- /dev/null +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivity.kt @@ -0,0 +1,200 @@ +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.os.Bundle +import android.provider.MediaStore +import android.support.design.widget.AppBarLayout +import android.support.design.widget.CoordinatorLayout +import android.support.design.widget.FloatingActionButton +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.LinearLayoutManager +import android.support.v7.widget.RecyclerView +import android.support.v7.widget.Toolbar +import android.widget.TextView +import ca.allanwang.kau.animators.FadeScaleAnimatorAdd +import ca.allanwang.kau.animators.KauAnimator +import ca.allanwang.kau.permissions.kauRequestPermissions +import ca.allanwang.kau.utils.* +import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter +import com.mikepenz.google_material_typeface_library.GoogleMaterial + + +/** + * Created by Allan Wang on 2017-07-04. + * + * Base activity for selecting images from storage + */ +open class ImagePickerActivity : AppCompatActivity(), LoaderManager.LoaderCallbacks { + + val imageAdapter = FastItemAdapter() + + 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) + + 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 + } + + var accentColor: Int = 0xff666666.toInt() + + fun onImagePickerResult(resultCode: Int, data: Intent?): List { + if (resultCode != Activity.RESULT_OK || data == null || !data.hasExtra(IMAGE_PICKER_RESULT)) + return emptyList() + return data.getParcelableArrayListExtra(IMAGE_PICKER_RESULT) + } + } + + 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@ImagePickerActivity, 18)) + } + toolbar.setNavigationOnClickListener { onBackPressed() } + + recycler.apply { + layoutManager = GridLayoutManager(context, computeColumnCount(context)) + adapter = imageAdapter + setHasFixedSize(true) + itemAnimator = KauAnimator(FadeScaleAnimatorAdd(0.8f)) + } + + ImageItem.bindEvents(imageAdapter) + imageAdapter.withSelectionListener({ _, _ -> selectionCount.text = imageAdapter.selections.size.toString() }) + + fab.apply { + show() + setIcon(GoogleMaterial.Icon.gmd_send) + setOnClickListener { + val selection = imageAdapter.selectedItems + if (selection.isEmpty()) { + toast(R.string.kau_no_images_selected) + } else { + val intent = Intent() + val data = ArrayList(selection.map { it.data }) + intent.putParcelableArrayListExtra(IMAGE_PICKER_RESULT, data) + setResult(RESULT_OK, intent) + finish() + } + } + hideOnDownwardsScroll(recycler) + } + + loadImages() + } + + /** + * Request read permissions and load all external images + * 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 + */ + private fun loadImages() { + kauRequestPermissions(Manifest.permission.READ_EXTERNAL_STORAGE) { + granted, _ -> + if (granted) { + supportLoaderManager.initLoader(LOADER_ID, null, this) + setToolbarScrollable(true) + } else { + toast(R.string.kau_permission_denied) + setToolbarScrollable(false) + } + } + } + + /** + * 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 onCreateLoader(id: Int, args: Bundle?): Loader { + val columns = arrayOf( + MediaStore.Images.Media._ID, + MediaStore.Images.Media.TITLE, + MediaStore.Images.Media.DATA, + MediaStore.Images.Media.SIZE, + MediaStore.Images.Media.DISPLAY_NAME, + MediaStore.Images.Media.DATE_MODIFIED + ) + return CursorLoader(this, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI, + columns, + null, + null, + //Sort by descending date + MediaStore.Images.Media.DATE_MODIFIED + " DESC") + } + + override fun onLoadFinished(loader: Loader, data: Cursor?) { + reset() + if (data == null || !data.moveToFirst()) { + toast(R.string.kau_no_images_found) + setToolbarScrollable(false) + return + } + do { + val model = ImageModel(data) + if (!shouldLoad(model)) continue + imageAdapter.add(ImageItem(model)) + } while (data.moveToNext()) + setToolbarScrollable((recycler.layoutManager as LinearLayoutManager).findLastCompletelyVisibleItemPosition() < imageAdapter.getItemCount() - 1) + } + + /** + * Optional filter to decide which images get displayed + * Defaults to checking their sizes to filter out + * very small images such as lurking drawables/icons + * + * Returns true if model should be displayed, false otherwise + */ + open fun shouldLoad(model: ImageModel): Boolean = model.size > 10000L + + private fun reset() { + imageAdapter.clear(); + } + + override fun onLoaderReset(loader: Loader) = reset() + + override fun onBackPressed() { + setResult(RESULT_CANCELED) + super.onBackPressed() + } +} \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt deleted file mode 100644 index 24c2db7..0000000 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt +++ /dev/null @@ -1,114 +0,0 @@ -package ca.allanwang.kau.imagepicker - -import android.Manifest -import android.content.Context -import android.database.Cursor -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 android.support.v7.widget.Toolbar -import ca.allanwang.kau.permissions.kauRequestPermissions -import ca.allanwang.kau.ui.widgets.ElasticDragDismissFrameLayout -import ca.allanwang.kau.utils.bindView -import ca.allanwang.kau.utils.dimenPixelSize -import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter - - -/** - * Created by Allan Wang on 2017-07-04. - * - */ -abstract class ImagePickerActivityBase : AppCompatActivity(), LoaderManager.LoaderCallbacks { - - val toolbar: Toolbar by bindView(R.id.kau_toolbar) - val draggableFrame: ElasticDragDismissFrameLayout by bindView(R.id.kau_draggable) - val recycler: RecyclerView by bindView(R.id.kau_recycler) - val imageAdapter = FastItemAdapter() - - 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 - } - - var accentColor: Int = 0xff666666.toInt() - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.kau_activity_image_picker) - recycler.layoutManager = GridLayoutManager(this, computeColumnCount(this)) - recycler.adapter = imageAdapter - - with(imageAdapter) { - withPositionBasedStateManagement(false) - withMultiSelect(true) - withSelectable(true) - withOnClickListener { v, _, _, _ -> - (v as BlurredImageView).toggleBlur() - true - } - } - draggableFrame.addListener(object : ElasticDragDismissFrameLayout.SystemChromeFader(this) { - override fun onDragDismissed() { - if (draggableFrame.translationY < 0) { -// window.returnTransition = TransitionInflater.from(this@ImagePickerActivityBase) -// .inflateTransition(R.transition.kau_about_return_upwards) - } - finishAfterTransition() - } - }) - kauRequestPermissions(Manifest.permission.READ_EXTERNAL_STORAGE) { - granted, _ -> - if (granted) { - supportLoaderManager.initLoader(42, null, this) - } - } - } - - override fun onCreateLoader(id: Int, args: Bundle?): Loader { - val columns = arrayOf( - MediaStore.Images.Media._ID, - MediaStore.Images.Media.TITLE, - MediaStore.Images.Media.DATA, - MediaStore.Images.Media.SIZE, - MediaStore.Images.Media.DISPLAY_NAME, - MediaStore.Images.Media.DATE_MODIFIED - ) - return CursorLoader(this, - MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - columns, - null, - null, - MediaStore.Images.Media.DATE_MODIFIED + " DESC") - } - - override fun onLoadFinished(loader: Loader, data: Cursor?) { - reset() - if (data == null) return - if (data.moveToFirst()) { - do { - val img = ImageModel(data) - imageAdapter.add(ImageItem(img)) - } while (data.moveToNext()) - } - } - - private fun reset() { - imageAdapter.clear(); - } - - override fun onLoaderReset(loader: Loader) = reset() -} \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerBinder.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerBinder.kt new file mode 100644 index 0000000..9e63464 --- /dev/null +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerBinder.kt @@ -0,0 +1,34 @@ +package ca.allanwang.kau.imagepicker + +import android.app.Activity +import android.content.Intent + +/** + * Created by Allan Wang on 2017-07-21. + * + * Extension functions for interacting with the image picker + * as well as internal constants + */ + +/** + * Image picker launcher + */ +fun Activity.kauLaunchImagePicker(clazz: Class, requestCode: Int) { + startActivityForResult(Intent(this, clazz), requestCode) +} + +fun Activity.kauLaunchImagePicker(requestCode: Int) = kauLaunchImagePicker(ImagePickerActivity::class.java, requestCode) + +/** + * Image picker result + * call under [Activity.onActivityResult] + * and make sure that the requestCode matches first + */ +fun Activity.kauOnImagePickerResult(resultCode: Int, data: Intent?) = ImagePickerActivity.onImagePickerResult(resultCode, data) + +internal const val LOADER_ID = 42 +internal const val IMAGE_PICKER_RESULT = "image_picker_result" + +internal const val ANIMATION_DURATION = 200L +internal const val ANIMATION_SCALE = 0.95f + -- cgit v1.2.3