aboutsummaryrefslogtreecommitdiff
path: root/mediapicker/src/main
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-07-31 23:02:01 -0700
committerGitHub <noreply@github.com>2017-07-31 23:02:01 -0700
commit48213d0b427c478865c75fee912ff1ae8bbaffb5 (patch)
tree7aef1d8400fc3403ee5a40aba945f33a95319359 /mediapicker/src/main
parent8a4e9fd44dfbcf58aa7ab63167dcbdf8752db7d0 (diff)
downloadkau-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/src/main')
-rw-r--r--mediapicker/src/main/AndroidManifest.xml1
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/BlurredImageView.kt158
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItem.kt69
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaItemBasic.kt71
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaModel.kt78
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityBase.kt100
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerActivityOverlayBase.kt51
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerBinder.kt33
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaPickerCore.kt182
-rw-r--r--mediapicker/src/main/kotlin/ca/allanwang/kau/imagepicker/MediaType.kt13
-rw-r--r--mediapicker/src/main/res-public/values-v21/styles.xml9
-rw-r--r--mediapicker/src/main/res-public/values/colors.xml6
-rw-r--r--mediapicker/src/main/res-public/values/dimens.xml6
-rw-r--r--mediapicker/src/main/res-public/values/public.xml7
-rw-r--r--mediapicker/src/main/res-public/values/styles.xml12
-rw-r--r--mediapicker/src/main/res/layout-v21/kau_activity_image_picker_overlay.xml17
-rw-r--r--mediapicker/src/main/res/layout/kau_activity_image_picker.xml55
-rw-r--r--mediapicker/src/main/res/layout/kau_blurred_imageview.xml32
-rw-r--r--mediapicker/src/main/res/layout/kau_iitem_image.xml9
-rw-r--r--mediapicker/src/main/res/layout/kau_iitem_image_basic.xml10
-rw-r--r--mediapicker/src/main/res/transition-v21/kau_image_enter.xml16
-rw-r--r--mediapicker/src/main/res/transition-v21/kau_image_exit_bottom.xml16
-rw-r--r--mediapicker/src/main/res/transition-v21/kau_image_exit_top.xml16
-rw-r--r--mediapicker/src/main/res/values/strings.xml7
24 files changed, 974 insertions, 0 deletions
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