PixelDroid-App-Android/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt

636 lines
19 KiB
Kotlin

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) {}
}