Android Image Viewer With ViewPager2 and Fresco (Kotlin)

April 26, 2020

This is an image viewer which support multiple images, where only one image is shown at any time, where you can swipe to navigate.

ImageFragment

NOTE: I am using Jetpack Navigation, thus I am using a Fragment instead of Activity.

class ImageFragment : Fragment() {
    // private val viewModel: ImageViewModel by viewModels()
    private lateinit var adapter: LocalAdapter
    private val args: ImageFragmentArgs by navArgs()

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return inflater.inflate(R.layout.image, container, false)
    }

    override fun onActivityCreated(savedInstanceState: Bundle?) {
        super.onActivityCreated(savedInstanceState)

        init()
    }

    private fun init() {
        adapter = LocalAdapter(viewModel)

        viewPager.orientation = ViewPager2.ORIENTATION_HORIZONTAL
        viewPager.adapter = adapter

        val items = args.default.items?.map {
            ViewItem.ImageItem(id = it.id, date = it.date, uri = it.uri, content = it.content)
        }
        adapter.submitList(items)

        args.default.position?.also { position ->
            viewPager.doOnLayout {
                viewPager.currentItem = position
            }
        }

    }

    sealed class ViewItem(open val id: String, val resource: Int) {
        data class ImageItem(override val id: String, val date: LocalDateTime, val uri: Uri, val content: String?) : ViewItem(id, R.layout.image_item)
    }

    class LocalAdapter(val viewModel: ImageViewModel): ListAdapter<ViewItem, LocalAdapter.ViewHolder>(DiffCallback()) {
        override fun getItemViewType(position: Int): Int {
            return getItem(position).resource
        }

        override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
            val view = LayoutInflater.from(parent.context)
                .inflate(viewType, parent, false)
            return ViewHolder(view)
        }

        override fun onBindViewHolder(holder: ViewHolder, position: Int) {
            val item = getItem(position)

            when(item) {
                is ViewItem.ImageItem -> {
                    bindImage(holder, item)
                }
            }
        }

        private fun bindImage(holder: ViewHolder, item: ViewItem.ImageItem) {
            holder.apply {
                imageTextView.text = item.date.toString()
                imageDraweeView.setImageURI(item.uri)

                val lines = listOf(
                    item.id,
                    item.date.toString(),
                    item.content
                ).filter {
                    it?.isEmpty() == false
                }

                imageTextView.text = lines.joinToString("\n")
            }
        }

        private class DiffCallback : DiffUtil.ItemCallback<ViewItem>() {
            override fun areItemsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean {
                if (oldItem.resource != newItem.resource) return false
                // check if id is the same
                return oldItem.id == newItem.id
            }

            @SuppressLint("DiffUtilEquals")
            override fun areContentsTheSame(oldItem: ViewItem, newItem: ViewItem): Boolean {
                // check if content is the same
                // equals using data class
                return oldItem == newItem
            }
        }

        inner class ViewHolder(override val containerView: View) :
            RecyclerView.ViewHolder(containerView), LayoutContainer {

        }
    }
}

NOTE: For zoomable image, refer to Zoomable PhotoView With Glide or ZoomableDraweeView

ImageArgs

@Parcelize
class ImageArgs(val items: List<Item>? = null, val position: Int): Parcelable {
    @Parcelize
    class Item(val id: String, val date: LocalDateTime, val uri: Uri, val content: String?): Parcelable
}

NOTE: Refer to Jetpack Navigation

image.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".view.support.ImageFragment">

    <androidx.viewpager2.widget.ViewPager2
        android:id="@+id/viewPager"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

</FrameLayout>

image_item.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:fresco="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    xmlns:tools="http://schemas.android.com/tools">

    <com.facebook.drawee.view.SimpleDraweeView
        android:id="@+id/imageDraweeView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        fresco:actualImageScaleType="fitCenter"
        fresco:placeholderImage="@drawable/placeholder_100"
        />

    <TextView
        android:id="@+id/imageTextView"
        tools:text="Hello"
        android:layout_gravity="bottom"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textColor="#fff"
        android:shadowColor="#000"
        android:shadowDx="2"
        android:shadowDy="2"
        android:shadowRadius="1.5"
        />

</FrameLayout>

Usage

val images = mutableListOf<ImageArgs.Item>()

// get image
// item = ...

var position = 0
images.add(ImageArgs.Item(id = item.id, uri = item.uri, date = item.date, content = item.content))

// refer to jetpack navigation
val args = ImageArgs(items = images, position = position)
val action = BatchFragmentDirections.actionBatchNavToImageNav(args)
findNavController().navigate(action)
This work is licensed under a
Creative Commons Attribution-NonCommercial 4.0 International License.