From 8f2b5ac043f47cc44f43c3788d1377083fb339a2 Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Tue, 18 Jul 2017 20:16:23 -0700 Subject: Dev 2.1 (#8) * Rewrite animation interfaces * Update changelog * Add scale factor for slide * Remove margins in iitems and replace with decorators * Remove mutable list * Switch cardiitem to use lambdas for click * status * Utils update and imagepicker fixes * Remove stringholder * Add fade in fade out * Increment about version * Rename fromedge to direction in javadocs * More logging * Add logging and docs * Make card icons visible * Update email builder and icon padding * Create elastic recycler activity * Fix card iitem * Add lint check and plurals * Inline all the things * Format and sort xml * Update dependencies and increment version --- .../allanwang/kau/imagepicker/BlurredImageView.kt | 166 +++++++++++++++++++++ .../ca/allanwang/kau/imagepicker/ImageItem.kt | 79 +++++++++- .../ca/allanwang/kau/imagepicker/ImageModel.kt | 21 +++ .../kau/imagepicker/ImagePickerActivityBase.kt | 80 +++++++--- .../src/main/res/layout/kau_blurred_imageview.xml | 32 ++++ .../src/main/res/layout/kau_iitem_image.xml | 18 +-- imagepicker/src/main/res/values/colors.xml | 6 + imagepicker/src/main/res/values/dimens.xml | 6 + imagepicker/src/main/res/values/styles.xml | 5 +- 9 files changed, 372 insertions(+), 41 deletions(-) create mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt create mode 100644 imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt create mode 100644 imagepicker/src/main/res/layout/kau_blurred_imageview.xml create mode 100644 imagepicker/src/main/res/values/colors.xml create mode 100644 imagepicker/src/main/res/values/dimens.xml (limited to 'imagepicker/src') diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt new file mode 100644 index 0000000..8fb5cf3 --- /dev/null +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt @@ -0,0 +1,166 @@ +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 com.mikepenz.iconics.IconicsDrawable +import jp.wasabeef.blurry.internal.BlurFactor +import jp.wasabeef.blurry.internal.BlurTask + +/** + * Created by Allan Wang on 2017-07-14. + * + * ImageView that is 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() { + + 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) + } + + 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) + } + + override fun clearAnimation() { + super.clearAnimation() + imageBase.clearAnimation() + imageBlur.clearAnimation() + imageForeground.clearAnimation() + } + + private fun View.scaleAnimate(scale: Float) = animate().scaleX(scale).scaleY(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 + val task = BlurTask(imageBase, factor) { + imageBlur.setImageDrawable(it) + scaleAnimate(ANIMATION_SCALE).start() + imageBlur.alphaAnimate(1f).start() + imageForeground.alphaAnimate(1f).start() + } + task.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) + scaleX = ANIMATION_SCALE + scaleY = 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/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt index 0b13a30..852e1e8 100644 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageItem.kt @@ -1,18 +1,89 @@ package ca.allanwang.kau.imagepicker +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable import android.support.v7.widget.RecyclerView import android.view.View -import android.widget.ImageView import ca.allanwang.kau.iitems.KauIItem import ca.allanwang.kau.utils.bindView +import ca.allanwang.kau.utils.gone +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 +import com.mikepenz.google_material_typeface_library.GoogleMaterial +import com.mikepenz.iconics.IconicsDrawable +import com.mikepenz.iconics.typeface.IIcon /** * Created by Allan Wang on 2017-07-04. */ -class ImageItem(data:String) - : KauIItem(R.layout.kau_iitem_card, { ViewHolder(it) }) { +class ImageItem(val data: ImageModel) + : KauIItem(R.layout.kau_iitem_image, { ViewHolder(it) }) { + + private var failedToLoad = false + + fun bindEvents(fastAdapter: FastAdapter) { + fastAdapter.withMultiSelect(true) + fastAdapter.withSelectable(true) + fastAdapter.withOnClickListener { v, _, _, _ -> + val image = v as BlurredImageView + image.toggleBlur() + true + } + } + + override fun bindView(holder: ViewHolder, payloads: List?) { + super.bindView(holder, payloads) + 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(); + return true; + } + + override fun onResourceReady(resource: Drawable, model: Any, target: Target, dataSource: DataSource, isFirstResource: Boolean): Boolean { + holder.container.animate().alpha(1f).start(); + return false; + } + }) + .into(holder.container.imageBase) + } + + private fun BlurredImageView.setIcon(icon: IIcon) { + val sizePx = computeViewSize(context) + imageBase.setImageDrawable(IconicsDrawable(context, icon) + .sizePx(sizePx) + .paddingPx(sizePx / 3) + .color(Color.WHITE)) + //todo add background + imageBase.setBackgroundColor(ImagePickerActivityBase.accentColor) + imageForeground.gone() + } + + private fun computeViewSize(context: Context): Int { + val screenWidthPx = context.resources.displayMetrics.widthPixels + return screenWidthPx / ImagePickerActivityBase.computeColumnCount(context) + } + + override fun unbindView(holder: ViewHolder) { + super.unbindView(holder) + if (!failedToLoad) { + Glide.with(holder.itemView).clear(holder.container.imageBase) + holder.container.removeBlurInstantly() + } else { + holder.container.fullReset() + } + } class ViewHolder(v: View) : RecyclerView.ViewHolder(v) { - val image: ImageView by bindView(R.id.kau_image) + val container: BlurredImageView by bindView(R.id.kau_image) } } \ No newline at end of file diff --git a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt new file mode 100644 index 0000000..26e4137 --- /dev/null +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImageModel.kt @@ -0,0 +1,21 @@ +package ca.allanwang.kau.imagepicker + +import android.database.Cursor +import android.provider.MediaStore +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) + + 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/ImagePickerActivityBase.kt b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt index 8dfbeab..24c2db7 100644 --- a/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt +++ b/imagepicker/src/main/kotlin/ca/allanwang/kau/imagepicker/ImagePickerActivityBase.kt @@ -1,5 +1,7 @@ package ca.allanwang.kau.imagepicker +import android.Manifest +import android.content.Context import android.database.Cursor import android.os.Bundle import android.provider.MediaStore @@ -10,9 +12,10 @@ 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.logging.KL +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 @@ -27,14 +30,37 @@ abstract class ImagePickerActivityBase : AppCompatActivity(), LoaderManager.Load 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) - with(recycler) { - layoutManager = GridLayoutManager(this@ImagePickerActivityBase, 3) - adapter = this@ImagePickerActivityBase.imageAdapter + recycler.layoutManager = GridLayoutManager(this, computeColumnCount(this)) + recycler.adapter = imageAdapter + + with(imageAdapter) { + withPositionBasedStateManagement(false) + withMultiSelect(true) + withSelectable(true) + withOnClickListener { v, _, _, _ -> + (v as BlurredImageView).toggleBlur() + true + } } - imageAdapter.add(arrayOf("a", "b", "c").map { ImageItem(it) }) draggableFrame.addListener(object : ElasticDragDismissFrameLayout.SystemChromeFader(this) { override fun onDragDismissed() { if (draggableFrame.translationY < 0) { @@ -44,33 +70,45 @@ abstract class ImagePickerActivityBase : AppCompatActivity(), LoaderManager.Load 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, null) + 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) { - val dataIndex = data.getColumnIndex(MediaStore.Images.Media.DATA) - val alstPhotos = mutableListOf() - - data.moveToLast() - while (!data.isBeforeFirst) { - val photoPath = data.getString(dataIndex) - KL.d(photoPath) - alstPhotos.add(photoPath) - data.moveToPrevious() + 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()) } - imageAdapter.add(alstPhotos.map { ImageItem(it) }) } - override fun onLoaderReset(loader: Loader) { - imageAdapter.clear() + private fun reset() { + imageAdapter.clear(); } + + override fun onLoaderReset(loader: Loader) = reset() } \ No newline at end of file diff --git a/imagepicker/src/main/res/layout/kau_blurred_imageview.xml b/imagepicker/src/main/res/layout/kau_blurred_imageview.xml new file mode 100644 index 0000000..e28cb9a --- /dev/null +++ b/imagepicker/src/main/res/layout/kau_blurred_imageview.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/imagepicker/src/main/res/layout/kau_iitem_image.xml b/imagepicker/src/main/res/layout/kau_iitem_image.xml index 22cc998..9d51d77 100644 --- a/imagepicker/src/main/res/layout/kau_iitem_image.xml +++ b/imagepicker/src/main/res/layout/kau_iitem_image.xml @@ -1,15 +1,9 @@ - - - - - \ No newline at end of file + 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/imagepicker/src/main/res/values/colors.xml b/imagepicker/src/main/res/values/colors.xml new file mode 100644 index 0000000..ebaa3f7 --- /dev/null +++ b/imagepicker/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + + #30000000 + + \ No newline at end of file diff --git a/imagepicker/src/main/res/values/dimens.xml b/imagepicker/src/main/res/values/dimens.xml new file mode 100644 index 0000000..3ff2dd7 --- /dev/null +++ b/imagepicker/src/main/res/values/dimens.xml @@ -0,0 +1,6 @@ + + + + 120dp + + \ No newline at end of file diff --git a/imagepicker/src/main/res/values/styles.xml b/imagepicker/src/main/res/values/styles.xml index e2d7280..1fbb184 100644 --- a/imagepicker/src/main/res/values/styles.xml +++ b/imagepicker/src/main/res/values/styles.xml @@ -1,8 +1,5 @@ - +