package com.margelo.nitro.lunardatepicker.ui.viewholders import android.annotation.SuppressLint import android.graphics.Typeface import android.graphics.drawable.GradientDrawable import android.view.Gravity import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import android.widget.LinearLayout import android.widget.TextView import com.kizitonwose.calendar.core.CalendarDay import com.kizitonwose.calendar.core.DayPosition import com.kizitonwose.calendar.view.ViewContainer import com.margelo.nitro.lunardatepicker.LDP_PriceData import com.margelo.nitro.lunardatepicker.constants.DataConstants import com.margelo.nitro.lunardatepicker.constants.LayoutConstants import com.margelo.nitro.lunardatepicker.constants.UIConstants import com.margelo.nitro.lunardatepicker.models.PickerConfig import com.margelo.nitro.lunardatepicker.utils.ContinuousSelectionHelper.isInDateBetweenSelection import com.margelo.nitro.lunardatepicker.utils.ContinuousSelectionHelper.isOutDateBetweenSelection import com.margelo.nitro.lunardatepicker.utils.DateConverter import com.margelo.nitro.lunardatepicker.utils.DimensionUtils.dpToPx import com.margelo.nitro.lunardatepicker.utils.ObjectPoolManager import java.time.LocalDate import java.time.ZoneId /** * ViewHolder for day cells in the calendar */ class DayViewHolder( view: View, private val config: PickerConfig, private val dateConverter: DateConverter, private val timeZone: ZoneId, private val isDateEnabled: (LocalDate) -> Boolean, private val formatPrice: (Double) -> String, private val onDateClick: (LocalDate) -> Unit ) : ViewContainer(view) { private lateinit var dayText: TextView private lateinit var lunarText: TextView private lateinit var priceText: TextView private lateinit var rootFrame: FrameLayout private lateinit var leftRangeView: View private lateinit var rightRangeView: View private lateinit var selectionContainer: LinearLayout var selectedFromDate: LocalDate? = null var selectedToDate: LocalDate? = null var pricesByDate: Map = emptyMap() var shouldShowPrices: Boolean = false // Object pooling for memory optimization private var currentDrawable: GradientDrawable? = null companion object { private val today = LocalDate.now() } init { setupViews() } private fun setupViews() { val context = view.context rootFrame = FrameLayout(context).apply { layoutParams = FrameLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) } val rangeContainer = LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL setPadding( 0, dpToPx(LayoutConstants.Padding.CELL_VERTICAL), 0, dpToPx(LayoutConstants.Padding.CELL_VERTICAL) ) layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, UIConstants.DayCell.SELECTED_CIRCLE_SIZE ) } leftRangeView = View(context).apply { layoutParams = LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, LayoutConstants.Dimensions.WEIGHT_EQUAL ) } rightRangeView = View(context).apply { layoutParams = LinearLayout.LayoutParams( 0, LinearLayout.LayoutParams.MATCH_PARENT, LayoutConstants.Dimensions.WEIGHT_EQUAL ) } rangeContainer.addView(leftRangeView) rangeContainer.addView(rightRangeView) val cellVertical = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER layoutParams = FrameLayout.LayoutParams( FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.WRAP_CONTENT ) } dayText = TextView(context).apply { gravity = Gravity.CENTER textSize = LayoutConstants.TextSize.DAY_TEXT } lunarText = TextView(context).apply { gravity = Gravity.CENTER textSize = LayoutConstants.TextSize.LUNAR_TEXT } priceText = TextView(context).apply { gravity = Gravity.CENTER textSize = LayoutConstants.TextSize.PRICE_TEXT } selectionContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.CENTER layoutParams = LinearLayout.LayoutParams( UIConstants.DayCell.SELECTED_CIRCLE_SIZE, UIConstants.DayCell.SELECTED_CIRCLE_SIZE ) } selectionContainer.addView(dayText) selectionContainer.addView(lunarText) cellVertical.addView(selectionContainer) cellVertical.addView(priceText) rootFrame.addView(rangeContainer) rootFrame.addView(cellVertical) (view as? ViewGroup)?.let { viewGroup -> viewGroup.removeAllViews() viewGroup.addView(rootFrame) } } fun bind(data: CalendarDay) { resetViews() when (data.position) { DayPosition.MonthDate -> bindMonthDate(data) DayPosition.InDate -> bindInDateAndOutDate(data) DayPosition.OutDate -> bindOutDateAndInDate(data) } setupClickListener(data) } private fun resetViews() { // Release drawable back to pool before clearing currentDrawable?.let { drawable -> ObjectPoolManager.gradientDrawablePool.release(drawable) currentDrawable = null } selectionContainer.background = null rightRangeView.setBackgroundColor(UIConstants.Colors.TRANSPARENT) leftRangeView.setBackgroundColor(UIConstants.Colors.TRANSPARENT) } private fun bindMonthDate(data: CalendarDay) { dayText.apply { setTextColor(config.dayCell.dateLabelColor) typeface = Typeface.DEFAULT text = data.date.dayOfMonth.toString() alpha = UIConstants.Alpha.FULLY_VISIBLE } // Calculate lunar date info once and use for both text and color val lunarInfo = getLunarDateInfo(data.date) lunarText.apply { text = lunarInfo.text setTextColor(lunarInfo.color) } bindPriceData(data.date) if (!isDateEnabled(data.date)) { dayText.alpha = UIConstants.Alpha.DISABLED_DATE } else { applyDateSelection(data.date) } } @SuppressLint("DefaultLocale") private fun bindPriceData(date: LocalDate) { if (shouldShowPrices) { val dateKey = String.format( DataConstants.Format.DATE_KEY, date.year, date.monthValue, date.dayOfMonth ) pricesByDate[dateKey]?.let { priceData -> priceText.apply { text = formatPrice(priceData.price) setTextColor( if (priceData.isCheapest == true) config.dayCell.cheapestPriceLabelColor else config.dayCell.priceLabelColor ) } } ?: run { priceText.text = "" } } else { priceText.text = "" } } private fun applyDateSelection(date: LocalDate) { when { (date == selectedFromDate && date == selectedToDate) -> { applySelectedCircle() } selectedFromDate == date && selectedToDate == null -> { applySelectedCircle() } date == selectedFromDate -> { applySelectedCircle() rightRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) } selectedFromDate != null && selectedToDate != null && (date > selectedFromDate && date < selectedToDate) -> { rightRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) leftRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) } date == selectedToDate -> { applySelectedCircle() leftRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) } date == today -> { dayText.apply { setTextColor(UIConstants.Colors.TODAY_COLOR) typeface = Typeface.DEFAULT_BOLD } } } } private fun bindInDateAndOutDate(data: CalendarDay) { dayText.text = "" lunarText.text = "" priceText.text = "" selectionContainer.background = null if (selectedFromDate != null && selectedToDate != null) { when (data.position) { DayPosition.InDate -> { if (isInDateBetweenSelection(data.date, selectedFromDate!!, selectedToDate!!)) { rightRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) leftRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) } } DayPosition.OutDate -> { if (isOutDateBetweenSelection(data.date, selectedFromDate!!, selectedToDate!!)) { rightRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) leftRangeView.setBackgroundColor(config.dayCell.rangeBackgroundColor) } } else -> {} } } } private fun bindOutDateAndInDate(data: CalendarDay) { bindInDateAndOutDate(data) } private fun applySelectedCircle() { // Use object pooling to reduce memory allocation val circle = ObjectPoolManager.gradientDrawablePool.acquire().apply { shape = GradientDrawable.OVAL setColor(config.calendar.selectedBackgroundColor) } // Keep reference to release later currentDrawable = circle selectionContainer.background = circle dayText.setTextColor(config.calendar.selectedTextColor) lunarText.setTextColor(config.calendar.selectedTextColor) } private fun setupClickListener(data: CalendarDay) { if (isDateEnabled(data.date)) { rootFrame.setOnClickListener { onDateClick(data.date) } } else { rootFrame.setOnClickListener(null) } } /** * Data class to hold lunar date display information */ private data class LunarDateInfo( val text: String, val color: Int ) /** * Calculates lunar date info (text and color) in a single call for optimal performance * Returns special color for lunar days 1 and 15, normal color otherwise */ @SuppressLint("DefaultLocale") private fun getLunarDateInfo(date: LocalDate): LunarDateInfo { return try { val lunarDate = dateConverter.getVietnameseLunarDate(date, timeZone) // Generate text val text = if (lunarDate.day == DataConstants.Numeric.FIRST_DAY_OF_MONTH) { String.format(DataConstants.Format.LUNAR_FIRST_DAY, lunarDate.day, lunarDate.month) } else { String.format(DataConstants.Format.LUNAR_OTHER_DAY, lunarDate.day) } // Determine color based on special days val color = when (lunarDate.day) { DataConstants.Numeric.FIRST_DAY_OF_MONTH, DataConstants.Numeric.FIFTEENTH_DAY_OF_MONTH -> config.dayCell.specialDateLabelColor else -> config.dayCell.lunarDateLabelColor } LunarDateInfo(text, color) } catch (_: Exception) { // Fallback calculation if lunar converter fails val fallbackText = "${date.dayOfMonth % DataConstants.Numeric.LUNAR_DAY_MOD}" LunarDateInfo(fallbackText, config.dayCell.lunarDateLabelColor) } } /** * Cleanup method to release pooled resources * Should be called when ViewHolder is no longer needed */ fun cleanup() { currentDrawable?.let { drawable -> ObjectPoolManager.gradientDrawablePool.release(drawable) currentDrawable = null } } }