H-PixelDroid/PixelDroid

View on GitHub
app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt

Summary

Maintainability
D
2 days
Test Coverage
package org.pixeldroid.app.postCreation.carousel

import android.content.Context
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.*
import androidx.annotation.Dimension
import androidx.annotation.IdRes
import androidx.annotation.LayoutRes
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.*
import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.ImageCarouselBinding
import me.relex.circleindicator.CircleIndicator2
import org.pixeldroid.common.dpToPx
import org.pixeldroid.common.getSnapPosition
import org.pixeldroid.common.spToPx

class ImageCarousel(
        context: Context,
        private var attributeSet: AttributeSet?
) : ConstraintLayout(context, attributeSet), OnItemClickListener {

    private var adapter: CarouselAdapter? = null

    private lateinit var binding: ImageCarouselBinding

    private val scaleTypeArray = arrayOf(
            ImageView.ScaleType.MATRIX,
            ImageView.ScaleType.FIT_XY,
            ImageView.ScaleType.FIT_START,
            ImageView.ScaleType.FIT_CENTER,
            ImageView.ScaleType.FIT_END,
            ImageView.ScaleType.CENTER,
            ImageView.ScaleType.CENTER_CROP,
            ImageView.ScaleType.CENTER_INSIDE
    )

    private lateinit var recyclerView: RecyclerView
    private var snapHelper: SnapHelper = PagerSnapHelper()

    var indicator: CircleIndicator2? = null
        set(newIndicator) {
            indicator?.apply {
                // if we remove it from the view, then the caption textView constraint won't work.
                this.visibility = View.GONE

                isBuiltInIndicator = false
            }

            field = newIndicator

            initIndicator()
        }
    
    private var isBuiltInIndicator = false
    private var data: MutableList<CarouselItem>? = null

    var onItemClickListener: OnItemClickListener? = this
        set(value) {
            field = value

            adapter?.listener = onItemClickListener
        }

    var onScrollListener: CarouselOnScrollListener? = null
        set(value) {
            field = value

            initOnScrollStateChange()
        }

    /**
     * Get or set current item position
     */
    var currentPosition = RecyclerView.NO_POSITION
        get() {
            return snapHelper.getSnapPosition(recyclerView.layoutManager)
        }
        set(value) {
            val position = when (value) {
                !in 0..((data?.size?.minus(1)) ?: 0) -> RecyclerView.NO_POSITION
                else -> value
            }

            if (position != RecyclerView.NO_POSITION && field != position) {
                updateProgress()
            } else if(position == RecyclerView.NO_POSITION) binding.encodeInfoCard.visibility = GONE

            if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) {
                recyclerView.smoothScrollToPosition(position)
            }
            field = position
        }

    /**
     * ****************************************************************
     * Attributes
     * ****************************************************************
     */

    var showCaption = false
        set(value) {
            field = value

            binding.tvCaption.visibility = if (showCaption) View.VISIBLE else View.GONE
        }

    @Dimension(unit = Dimension.PX)
    var captionTextSize: Int = 0
        set(value) {
            field = value

            binding.tvCaption.setTextSize(TypedValue.COMPLEX_UNIT_PX, captionTextSize.toFloat())
        }

    var showIndicator = false
        set(value) {
            field = value

            when {
                indicator == null -> initIndicator()
                value -> indicator?.visibility = View.VISIBLE
                else -> indicator?.visibility = View.INVISIBLE
            }
        }

    var showNavigationButtons = false
        set(value) {
            field = value

            binding.btnPrevious.visibility =
                    if (showNavigationButtons) View.VISIBLE else View.GONE
            binding.btnNext.visibility =
                    if (showNavigationButtons) View.VISIBLE else View.GONE
        }

    var imageScaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_INSIDE
        set(value) {
            field = value

            initAdapter()
        }

    var carouselBackground: Drawable? = null
        set(value) {
            field = value

            recyclerView.background = carouselBackground
        }

    var imagePlaceholder: Drawable? = null
        set(value) {
            field = value

            initAdapter()
        }

    @LayoutRes
    var itemLayout: Int = R.layout.item_carousel
        set(value) {
            field = value

            initAdapter()
        }

    @IdRes
    var imageViewId: Int = R.id.img
        set(value) {
            field = value

            initAdapter()
        }

    @Dimension(unit = Dimension.PX)
    var previousButtonMargin: Int = 0
        set(value) {
            field = value

            val previousButtonParams = binding.btnPrevious.layoutParams as LayoutParams
            previousButtonParams.setMargins(
                    previousButtonMargin,
                    0,
                    0,
                    0
            )
            binding.btnPrevious.layoutParams = previousButtonParams
        }

    @Dimension(unit = Dimension.PX)
    var nextButtonMargin: Int = 0
        set(value) {
            field = value

            val nextButtonParams = binding.btnNext.layoutParams as LayoutParams
            nextButtonParams.setMargins(
                    0,
                    0,
                    nextButtonMargin,
                    0
            )
            binding.btnNext.layoutParams = nextButtonParams
        }

    var showLayoutSwitchButton: Boolean = true
        set(value) {
            field = value

            binding.switchToGridButton.setOnClickListener {
                layoutCarousel = false
            }
            binding.switchToCarouselButton.setOnClickListener {
                layoutCarousel = true
            }

            if(value){
                if(layoutCarousel){
                    binding.switchToGridButton.visibility = VISIBLE
                    binding.switchToCarouselButton.visibility = GONE
                } else {
                    binding.switchToGridButton.visibility = GONE
                    binding.switchToCarouselButton.visibility = VISIBLE
                }
            } else {
                binding.switchToGridButton.visibility = GONE
                binding.switchToCarouselButton.visibility = GONE
            }
        }

    var layoutCarouselCallback: ((Boolean) -> Unit)? = null

    var updateDescriptionCallback: ((position: Int, description: String) -> Unit)? = null


    var layoutCarousel: Boolean = true
        set(value) {
            field = value

            if(value){
                recyclerView.layoutManager = CarouselLinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false)
                showNavigationButtons = showNavigationButtons

                binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE
                binding.tvCaption.visibility = if(editingMediaDescription || !showCaption) INVISIBLE else VISIBLE
            } else {
                recyclerView.layoutManager = GridLayoutManager(context, 3)
                binding.btnNext.visibility = GONE
                binding.btnPrevious.visibility = GONE

                binding.editMediaDescriptionLayout.visibility = INVISIBLE
                binding.tvCaption.visibility = INVISIBLE
            }
            showIndicator = value

            layoutCarouselCallback?.let { it(value) }

            //update layout switch button to make it take into account the change
            showLayoutSwitchButton = showLayoutSwitchButton

            initAdapter()
        }

    var addPhotoButtonCallback: (() -> Unit)? = null

    var editingMediaDescription: Boolean = false
        set(value){

            if(layoutCarousel){
                field = value
                if(value) binding.editTextMediaDescription.setText(currentDescription)
                else {
                    val description = binding.editTextMediaDescription.text.toString()
                    currentDescription = description
                    adapter?.updateDescription(currentPosition, description)
                    updateDescriptionCallback?.invoke(currentPosition, description)
                }
                binding.editMediaDescriptionLayout.visibility = if(value) VISIBLE else INVISIBLE
                binding.tvCaption.visibility = if(value || !showCaption) INVISIBLE else VISIBLE
            }

        }

    var currentDescription: String? = null
        set(value) {
            if(!value.isNullOrEmpty()) {
                field = value
                binding.tvCaption.text = value
            } else {
                field = null
                binding.tvCaption.text = context.getText(R.string.no_media_description)
            }

        }

    var maxEntries: Int? = null
        set(value){
            field = value
            adapter?.maxEntries = value
        }



    init {
        initViews()
        initAttributes()
        initAdapter()
        initListeners()
    }


    private fun initViews() {
        binding = ImageCarouselBinding.inflate(LayoutInflater.from(context),this,  true)

        recyclerView = binding.recyclerView

        recyclerView.setHasFixedSize(true)

        // For marquee effect
        binding.tvCaption.isSelected = true
    }


    private fun initAttributes() {
        context.theme.obtainStyledAttributes(
                attributeSet,
                R.styleable.ImageCarousel,
                0,
                0
        ).apply {

            try {

                showCaption = getBoolean(
                        R.styleable.ImageCarousel_showCaption,
                        true
                )

                captionTextSize = getDimension(
                        R.styleable.ImageCarousel_captionTextSize,
                        14.spToPx(context).toFloat()
                ).toInt()

                showIndicator = getBoolean(
                        R.styleable.ImageCarousel_showIndicator,
                        true
                )

                imageScaleType = scaleTypeArray[
                        getInteger(
                                R.styleable.ImageCarousel_imageScaleType,
                                ImageView.ScaleType.CENTER_INSIDE.ordinal
                        )
                ]

                carouselBackground = ColorDrawable(Color.parseColor("#333333"))

                imagePlaceholder = getDrawable(
                        R.styleable.ImageCarousel_imagePlaceholder
                ) ?: ContextCompat.getDrawable(context, R.drawable.ic_picture_fallback)

                itemLayout = getResourceId(
                        R.styleable.ImageCarousel_itemLayout,
                        R.layout.item_carousel
                )

                imageViewId = getResourceId(
                        R.styleable.ImageCarousel_imageViewId,
                        R.id.img
                )

                previousButtonMargin = 4.dpToPx(context)

                nextButtonMargin = 4.dpToPx(context)

                showNavigationButtons = getBoolean(
                        R.styleable.ImageCarousel_showNavigationButtons,
                        false
                )

                layoutCarousel = getBoolean(
                    R.styleable.ImageCarousel_layoutCarousel,
                    true
                )

                showLayoutSwitchButton = getBoolean(
                    R.styleable.ImageCarousel_showLayoutSwitchButton,
                    true
                )

            } finally {
                recycle()
            }

        }
    }


    private fun initAdapter() {
        adapter = CarouselAdapter(
                itemLayout = itemLayout,
                imageViewId = imageViewId,
                listener = onItemClickListener,
                imageScaleType = imageScaleType,
                imagePlaceholder = imagePlaceholder,
                carousel = layoutCarousel,
                maxEntries = maxEntries
        )
        recyclerView.adapter = adapter

        data?.apply {
            adapter?.addAll(this)
        }

        indicator?.apply {
            try {
                adapter?.registerAdapterDataObserver(this.adapterDataObserver)
            } catch (e: IllegalStateException) {
                e.printStackTrace()
            }
        }
        initIndicator()
    }


    private fun initListeners() {
        recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
            override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
                val position = currentPosition

                if (showCaption) {
                    if (position >= 0) {
                        val dataItem = adapter?.getItem(position)

                        dataItem?.apply {
                            caption.apply {
                                if(layoutCarousel){
                                    binding.editMediaDescriptionLayout.visibility = INVISIBLE
                                    showCaption = true
                                }
                                currentDescription = this
                            }
                        }
                    }
                }

                if(dx !=0 || dy != 0) currentPosition = position

                onScrollListener?.onScrolled(recyclerView, dx, dy)

            }

            override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) {

                onScrollListener?.apply {
                    val position = snapHelper.getSnapPosition(recyclerView.layoutManager)
                    val carouselItem = data?.getOrNull(position)

                    onScrollStateChanged(
                            recyclerView,
                            newState,
                            position,
                            carouselItem
                    )
                }

            }
        })

        binding.tvCaption.setOnClickListener {
            editingMediaDescription = true
        }

        binding.btnNext.setOnClickListener {
            next()
        }

        binding.btnPrevious.setOnClickListener {
            previous()
        }
        binding.imageDescriptionButton.setOnClickListener {
            editingMediaDescription = false
        }
    }

    private fun initIndicator() {
        // If no custom indicator added, then default indicator will be shown.
        if (indicator == null) {
            indicator = binding.indicator
            isBuiltInIndicator = true
        }

        snapHelper.apply {
            try {
                attachToRecyclerView(recyclerView)
            } catch (e: IllegalStateException) {
                e.printStackTrace()
            }
        }

        indicator?.apply {
            if (isBuiltInIndicator) {
                // Indicator visibility
                this.visibility = if (showIndicator) View.VISIBLE else View.INVISIBLE
            }

            // Attach to recyclerview
            attachToRecyclerView(recyclerView, snapHelper)

            // Observe the adapter
            adapter?.let { carouselAdapter ->
                try {
                    carouselAdapter.registerAdapterDataObserver(this.adapterDataObserver)
                } catch (e: IllegalStateException) {
                    e.printStackTrace()
                }
            }
        }
    }

    private fun initOnScrollStateChange() {
        data?.apply {
            if (isNotEmpty()) {
                onScrollListener?.onScrollStateChanged(
                        recyclerView,
                        RecyclerView.SCROLL_STATE_IDLE,
                        0,
                        this[0]
                )
            }
        }
    }

    /**
     * Add data to the carousel.
     */
    fun addData(data: List<CarouselItem>) {
        adapter?.apply {
            addAll(data)

            this@ImageCarousel.data = data.toMutableList()

            updateProgress()
            initOnScrollStateChange()
        }
        showNavigationButtons = data.size != 1
    }

    private fun updateProgress(){

        val currentItem = data?.getOrNull(currentPosition)

        currentItem?.let {
            if(it.encodeError){
                binding.encodeInfoCard.visibility = VISIBLE
                binding.encodeProgress.visibility = GONE
                binding.encodeInfoText.setText(R.string.encode_error)
                binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error),
                    null, null, null)
            } else if(it.encodeComplete == true){
                binding.encodeInfoCard.visibility = VISIBLE
                binding.encodeProgress.visibility = GONE
                binding.encodeInfoText.setText(R.string.encode_success)
                binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24),
                    null, null, null)
            } else if(it.encodeProgress != null){
                binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null)
                binding.encodeProgress.visibility = VISIBLE
                binding.encodeInfoCard.visibility = VISIBLE
                binding.encodeProgress.progress = it.encodeProgress ?: 0
                binding.encodeInfoText.text = (if(it.stabilizationFirstPass == true){
                    context.getString(R.string.analyzing_stabilization)
                } else context.getString(R.string.encode_progress)).format(it.encodeProgress ?: 0)
            } else {
                binding.encodeInfoCard.visibility = GONE
            }
        }

    }

    /**
     * Goto previous item.
     */
    fun previous() {
        currentPosition--
    }

    /**
     * Goto next item.
     */
    fun next() {
        currentPosition++
    }

    override fun onClick(position: Int) {
        if(position == (data?.size ?: 0) ){
            addPhotoButtonCallback?.invoke()
        } else {
            if (!layoutCarousel) layoutCarousel = true
            currentPosition = position
        }
    }

    override fun onLongClick(position: Int) {
        //if(!layoutCarousel && position != (data?.size ?: 0) ) {
            //TODO Highlight selected, show toolbar?
            // Enable "long click mode?"
        //}
    }
}

interface OnItemClickListener {

    fun onClick(position: Int)

    fun onLongClick(position: Int)

}

interface CarouselOnScrollListener {

    fun onScrollStateChanged(
            recyclerView: RecyclerView,
            newState: Int,
            position: Int,
            carouselItem: CarouselItem?
    ) {}

    fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {}

}