package com.quillstudios.pokegoalshelper.ui import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.animation.AnimatorListenerAdapter import android.animation.Animator import android.content.Context import android.graphics.PixelFormat import android.graphics.drawable.GradientDrawable import android.os.Build import android.util.TypedValue import android.view.* import android.view.animation.AccelerateDecelerateInterpolator import android.widget.* import androidx.core.content.ContextCompat import com.quillstudios.pokegoalshelper.R import com.quillstudios.pokegoalshelper.models.DetectionResult import com.quillstudios.pokegoalshelper.utils.PGHLog import java.time.format.DateTimeFormatter import kotlin.math.abs /** * Bottom drawer that slides up to display detection results immediately after capture. * * Features: * - Slides up from bottom with smooth animation * - Shows Pokemon detection results with formatted data * - Auto-dismiss after timeout or manual dismiss * - Expandable for more details * - Gesture handling for swipe dismiss */ class ResultsBottomDrawer(private val context: Context) { companion object { private const val TAG = "ResultsBottomDrawer" private const val DRAWER_HEIGHT_COLLAPSED_DP = 80 private const val DRAWER_HEIGHT_EXPANDED_DP = 400 // Increased to show all data private const val SLIDE_ANIMATION_DURATION = 300L private const val SWIPE_THRESHOLD = 100f private const val EXPAND_THRESHOLD = -50f // Negative because we're pulling up } private var windowManager: WindowManager? = null private var drawerContainer: LinearLayout? = null private var drawerParams: WindowManager.LayoutParams? = null private var isShowing = false private var isDragging = false private var isExpanded = false private var currentDetectionResult: DetectionResult? = null // Touch handling private var initialTouchY = 0f private var initialTranslationY = 0f fun show(result: DetectionResult) { if (isShowing) return try { windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager currentDetectionResult = result createDrawerView(result) isShowing = true PGHLog.d(TAG, "Bottom drawer shown for detection: ${result.id}") } catch (e: Exception) { PGHLog.e(TAG, "Failed to show bottom drawer", e) } } fun hide() { if (!isShowing) return try { // Animate out animateOut { try { drawerContainer?.let { windowManager?.removeView(it) } cleanup() } catch (e: Exception) { PGHLog.e(TAG, "Error removing drawer view", e) } } PGHLog.d(TAG, "Bottom drawer hidden") } catch (e: Exception) { PGHLog.e(TAG, "Failed to hide bottom drawer", e) } } private fun createDrawerView(result: DetectionResult) { val screenSize = getScreenSize() val drawerHeight = dpToPx(DRAWER_HEIGHT_COLLAPSED_DP) // Start collapsed // Create main container drawerContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL background = createDrawerBackground() gravity = Gravity.CENTER_HORIZONTAL setPadding(dpToPx(16), dpToPx(12), dpToPx(16), dpToPx(16)) // Add drag handle addView(createDragHandle()) // Add collapsed content (always visible) addView(createCollapsedContent(result)) // Add expanded content (initially hidden) addView(createExpandedContent(result)) // Set up touch handling for swipe and expand setOnTouchListener(createExpandableSwipeTouchListener()) } // Create window parameters drawerParams = WindowManager.LayoutParams( WindowManager.LayoutParams.MATCH_PARENT, drawerHeight, if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY } else { @Suppress("DEPRECATION") WindowManager.LayoutParams.TYPE_PHONE }, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL or WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS, PixelFormat.TRANSLUCENT ).apply { gravity = Gravity.BOTTOM y = 0 } // Add to window manager windowManager?.addView(drawerContainer, drawerParams) // Animate in animateIn() } private fun createDragHandle(): View { return View(context).apply { background = GradientDrawable().apply { setColor(ContextCompat.getColor(context, android.R.color.darker_gray)) cornerRadius = dpToPx(2).toFloat() } layoutParams = LinearLayout.LayoutParams( dpToPx(40), dpToPx(4) ).apply { setMargins(0, 0, 0, dpToPx(8)) } } } private fun createCollapsedContent(result: DetectionResult): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Status icon val statusIcon = ImageView(context).apply { setImageResource( if (result.success) android.R.drawable.ic_menu_myplaces else android.R.drawable.ic_dialog_alert ) setColorFilter( ContextCompat.getColor( context, if (result.success) android.R.color.holo_green_light else android.R.color.holo_red_light ) ) layoutParams = LinearLayout.LayoutParams( dpToPx(20), dpToPx(20) ).apply { setMargins(0, 0, dpToPx(8), 0) } } // Main content (compact) val mainContent = createCompactDataRow(result) // Dismiss button val dismissButton = ImageButton(context).apply { setImageResource(android.R.drawable.ic_menu_close_clear_cancel) background = createCircularBackground() setColorFilter(ContextCompat.getColor(context, android.R.color.white)) setOnClickListener { hide() } layoutParams = LinearLayout.LayoutParams( dpToPx(24), dpToPx(24) ).apply { setMargins(dpToPx(8), 0, 0, 0) } } addView(statusIcon) addView(mainContent) addView(dismissButton) } } private fun createCompactDataRow(result: DetectionResult): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) if (result.success && result.pokemonInfo != null) { val pokemonInfo = result.pokemonInfo val dataPoints = mutableListOf() // Collect all available data points from actual PokemonInfo structure pokemonInfo.species?.let { dataPoints.add(it) } pokemonInfo.nationalDexNumber?.let { dataPoints.add("#$it") } pokemonInfo.level?.let { dataPoints.add("Lv$it") } pokemonInfo.gender?.let { dataPoints.add(it) } if (pokemonInfo.isShiny) dataPoints.add("✨") if (pokemonInfo.isAlpha) dataPoints.add("🅰") // Add processing time dataPoints.add("${result.processingTimeMs}ms") // Create compact display val compactText = if (dataPoints.isNotEmpty()) { dataPoints.joinToString(" • ") } else { "Pokemon detected" } val textView = TextView(context).apply { text = compactText setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) maxLines = 1 setSingleLine(true) } addView(textView) } else { val textView = TextView(context).apply { text = "${if (result.success) "No Pokemon" else "Failed"} • ${result.processingTimeMs}ms" setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) } addView(textView) } } } private fun createExpandedContent(result: DetectionResult): ScrollView { return ScrollView(context).apply { visibility = View.GONE // Initially hidden tag = "expanded_content" // For easy finding // Create the scrollable content container val contentContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL setPadding(0, dpToPx(8), 0, dpToPx(16)) // Add bottom padding for scroll } if (result.success && result.pokemonInfo != null) { val pokemonInfo = result.pokemonInfo // Basic Pokemon Info Section contentContainer.addView(createSectionHeader("Pokemon Info")) contentContainer.addView(createTwoColumnRow( leftLabel = "Species", leftValue = pokemonInfo.species ?: "Unknown", rightLabel = "Dex #", rightValue = pokemonInfo.nationalDexNumber?.let { "#$it" } ?: "N/A" )) contentContainer.addView(createTwoColumnRow( leftLabel = "Nickname", leftValue = pokemonInfo.nickname ?: "None", rightLabel = "Gender", rightValue = pokemonInfo.gender ?: "Unknown" )) contentContainer.addView(createTwoColumnRow( leftLabel = "Level", leftValue = pokemonInfo.level?.toString() ?: "N/A", rightLabel = "Nature", rightValue = pokemonInfo.nature ?: "Unknown" )) // Types Section contentContainer.addView(createSectionHeader("Types")) val typeDisplay = when { pokemonInfo.primaryType != null && pokemonInfo.secondaryType != null -> "${pokemonInfo.primaryType} / ${pokemonInfo.secondaryType}" pokemonInfo.primaryType != null -> pokemonInfo.primaryType else -> "Unknown" } contentContainer.addView(createTwoColumnRow( leftLabel = "Type", leftValue = typeDisplay, rightLabel = "Tera", rightValue = pokemonInfo.teraType ?: "N/A" )) // Stats Section (if available) pokemonInfo.stats?.let { stats -> contentContainer.addView(createSectionHeader("Base Stats")) contentContainer.addView(createThreeColumnRow( leftLabel = "HP", leftValue = stats.hp?.toString() ?: "?", middleLabel = "ATK", middleValue = stats.attack?.toString() ?: "?", rightLabel = "DEF", rightValue = stats.defense?.toString() ?: "?" )) contentContainer.addView(createThreeColumnRow( leftLabel = "SP.ATK", leftValue = stats.spAttack?.toString() ?: "?", middleLabel = "SP.DEF", middleValue = stats.spDefense?.toString() ?: "?", rightLabel = "SPEED", rightValue = stats.speed?.toString() ?: "?" )) } // Special Properties Section contentContainer.addView(createSectionHeader("Properties")) contentContainer.addView(createCheckboxRow( leftLabel = "Shiny", leftChecked = pokemonInfo.isShiny, rightLabel = "Alpha", rightChecked = pokemonInfo.isAlpha )) contentContainer.addView(createMixedRow( leftLabel = "Favorited", leftChecked = pokemonInfo.isFavorited, rightLabel = "Pokeball", rightValue = pokemonInfo.pokeballType ?: "Unknown" )) // Game Origin Section contentContainer.addView(createSectionHeader("Origin")) contentContainer.addView(createTwoColumnRow( leftLabel = "Game", leftValue = pokemonInfo.gameSource ?: "Unknown", rightLabel = "Language", rightValue = pokemonInfo.language ?: "Unknown" )) pokemonInfo.originalTrainerName?.let { trainerName -> contentContainer.addView(createTwoColumnRow( leftLabel = "OT Name", leftValue = trainerName, rightLabel = "OT ID", rightValue = pokemonInfo.originalTrainerId ?: "Unknown" )) } // Ability & Moves pokemonInfo.ability?.let { ability -> contentContainer.addView(createSectionHeader("Ability & Moves")) contentContainer.addView(createDetailRow("Ability", ability)) } if (pokemonInfo.moves.isNotEmpty()) { contentContainer.addView(createDetailRow("Moves", pokemonInfo.moves.take(4).joinToString(", "))) } // Additional Data if (pokemonInfo.stamps.isNotEmpty() || pokemonInfo.labels.isNotEmpty() || pokemonInfo.marks.isNotEmpty()) { contentContainer.addView(createSectionHeader("Additional")) if (pokemonInfo.stamps.isNotEmpty()) { contentContainer.addView(createDetailRow("Stamps", pokemonInfo.stamps.joinToString(", "))) } if (pokemonInfo.labels.isNotEmpty()) { contentContainer.addView(createDetailRow("Labels", pokemonInfo.labels.joinToString(", "))) } if (pokemonInfo.marks.isNotEmpty()) { contentContainer.addView(createDetailRow("Marks", pokemonInfo.marks.joinToString(", "))) } } } else { // Show error details contentContainer.addView(createSectionHeader("Detection Failed")) contentContainer.addView(createDetailRow("Status", if (result.success) "No Pokemon detected" else "Detection failed")) result.errorMessage?.let { error -> contentContainer.addView(createDetailRow("Error", error)) } } // Technical Info Section contentContainer.addView(createSectionHeader("Technical Info")) contentContainer.addView(createTwoColumnRow( leftLabel = "Processing", leftValue = "${result.processingTimeMs}ms", rightLabel = "Detected", rightValue = "${result.detections.size} items" )) contentContainer.addView(createDetailRow("Timestamp", result.timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss")))) // Add the content container to the ScrollView addView(contentContainer) } } private fun createDetailRow(label: String, value: String): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL val labelView = TextView(context).apply { text = "$label:" setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) layoutParams = LinearLayout.LayoutParams( dpToPx(80), ViewGroup.LayoutParams.WRAP_CONTENT ) } val valueView = TextView(context).apply { text = value setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) typeface = android.graphics.Typeface.DEFAULT_BOLD layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) } addView(labelView) addView(valueView) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(2), 0, dpToPx(2)) } } } private fun createSectionHeader(title: String): TextView { return TextView(context).apply { text = title setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) setTextColor(ContextCompat.getColor(context, android.R.color.holo_blue_light)) typeface = android.graphics.Typeface.DEFAULT_BOLD layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(8), 0, dpToPx(4)) } } } private fun createTwoColumnRow( leftLabel: String, leftValue: String, rightLabel: String, rightValue: String ): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Left column val leftColumn = createColumnItem(leftLabel, leftValue) leftColumn.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) // Right column val rightColumn = createColumnItem(rightLabel, rightValue) rightColumn.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ).apply { setMargins(dpToPx(8), 0, 0, 0) } addView(leftColumn) addView(rightColumn) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(2), 0, dpToPx(2)) } } } private fun createThreeColumnRow( leftLabel: String, leftValue: String, middleLabel: String, middleValue: String, rightLabel: String, rightValue: String ): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Left column val leftColumn = createColumnItem(leftLabel, leftValue) leftColumn.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) // Middle column val middleColumn = createColumnItem(middleLabel, middleValue) middleColumn.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ).apply { setMargins(dpToPx(4), 0, dpToPx(4), 0) } // Right column val rightColumn = createColumnItem(rightLabel, rightValue) rightColumn.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) addView(leftColumn) addView(middleColumn) addView(rightColumn) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(2), 0, dpToPx(2)) } } } private fun createColumnItem(label: String, value: String): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.VERTICAL gravity = Gravity.START val labelView = TextView(context).apply { text = "$label:" setTextSize(TypedValue.COMPLEX_UNIT_SP, 9f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) } val valueView = TextView(context).apply { text = value setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) typeface = android.graphics.Typeface.DEFAULT_BOLD } addView(labelView) addView(valueView) } } private fun createCheckboxRow( leftLabel: String, leftChecked: Boolean, rightLabel: String, rightChecked: Boolean ): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Left checkbox val leftCheckbox = createCheckboxItem(leftLabel, leftChecked) leftCheckbox.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) // Right checkbox val rightCheckbox = createCheckboxItem(rightLabel, rightChecked) rightCheckbox.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ).apply { setMargins(dpToPx(8), 0, 0, 0) } addView(leftCheckbox) addView(rightCheckbox) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(2), 0, dpToPx(2)) } } } private fun createCheckboxItem(label: String, checked: Boolean): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Checkbox symbol val checkboxView = TextView(context).apply { text = if (checked) "☑" else "☐" setTextSize(TypedValue.COMPLEX_UNIT_SP, 14f) setTextColor( if (checked) ContextCompat.getColor(context, android.R.color.holo_green_light) else ContextCompat.getColor(context, android.R.color.darker_gray) ) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, 0, dpToPx(6), 0) } } // Label val labelView = TextView(context).apply { text = label setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) } addView(checkboxView) addView(labelView) } } private fun createMixedRow( leftLabel: String, leftChecked: Boolean, rightLabel: String, rightValue: String ): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.HORIZONTAL gravity = Gravity.CENTER_VERTICAL // Left checkbox val leftCheckbox = createCheckboxItem(leftLabel, leftChecked) leftCheckbox.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) // Right text item val rightItem = createColumnItem(rightLabel, rightValue) rightItem.layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ).apply { setMargins(dpToPx(8), 0, 0, 0) } addView(leftCheckbox) addView(rightItem) layoutParams = LinearLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ).apply { setMargins(0, dpToPx(2), 0, dpToPx(2)) } } } private fun createDrawerBackground(): GradientDrawable { return GradientDrawable().apply { setColor(ContextCompat.getColor(context, android.R.color.black)) alpha = (0.9f * 255).toInt() cornerRadii = floatArrayOf( dpToPx(12).toFloat(), dpToPx(12).toFloat(), // top-left dpToPx(12).toFloat(), dpToPx(12).toFloat(), // top-right 0f, 0f, // bottom-right 0f, 0f // bottom-left ) setStroke(2, ContextCompat.getColor(context, android.R.color.darker_gray)) } } private fun createCircularBackground(): GradientDrawable { return GradientDrawable().apply { setColor(ContextCompat.getColor(context, android.R.color.transparent)) shape = GradientDrawable.OVAL setStroke(1, ContextCompat.getColor(context, android.R.color.darker_gray)) } } private fun createExpandableSwipeTouchListener(): View.OnTouchListener { return View.OnTouchListener { view, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { isDragging = false initialTouchY = event.rawY initialTranslationY = view.translationY true } MotionEvent.ACTION_MOVE -> { val deltaY = event.rawY - initialTouchY if (!isDragging && abs(deltaY) > 20) { isDragging = true } if (isDragging) { if (deltaY > 0) { // Downward drag - dismissing view.translationY = initialTranslationY + deltaY } else if (deltaY < 0 && !isExpanded) { // Upward drag - expanding (only if not already expanded) // Don't move the view, just track the gesture } } true } MotionEvent.ACTION_UP -> { if (isDragging) { val deltaY = event.rawY - initialTouchY if (deltaY > SWIPE_THRESHOLD && isExpanded) { // Only collapse to minimal state if expanded, never fully dismiss collapseDrawer() ObjectAnimator.ofFloat(view, "translationY", view.translationY, initialTranslationY).apply { duration = 200L interpolator = AccelerateDecelerateInterpolator() start() } } else if (deltaY < EXPAND_THRESHOLD && !isExpanded) { // Expand if swiped up enough expandDrawer() } else if (deltaY > -EXPAND_THRESHOLD && isExpanded) { // Collapse if swiped down a bit while expanded collapseDrawer() } else { // Snap back to current state ObjectAnimator.ofFloat(view, "translationY", view.translationY, initialTranslationY).apply { duration = 200L interpolator = AccelerateDecelerateInterpolator() start() } } } else { // Simple tap - toggle expand/collapse if (isExpanded) { collapseDrawer() } else { expandDrawer() } } isDragging = false true } else -> false } } } private fun expandDrawer() { if (isExpanded) return isExpanded = true // Show expanded content drawerContainer?.findViewWithTag("expanded_content")?.let { expandedContent -> expandedContent.visibility = View.VISIBLE expandedContent.alpha = 0f ObjectAnimator.ofFloat(expandedContent, "alpha", 0f, 1f).apply { duration = SLIDE_ANIMATION_DURATION start() } } // Resize drawer window drawerParams?.let { params -> params.height = dpToPx(DRAWER_HEIGHT_EXPANDED_DP) drawerContainer?.let { container -> windowManager?.updateViewLayout(container, params) } } PGHLog.d(TAG, "Drawer expanded") } private fun collapseDrawer() { if (!isExpanded) return isExpanded = false // Hide expanded content drawerContainer?.findViewWithTag("expanded_content")?.let { expandedContent -> ObjectAnimator.ofFloat(expandedContent, "alpha", 1f, 0f).apply { duration = SLIDE_ANIMATION_DURATION addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { expandedContent.visibility = View.GONE } }) start() } } // Resize drawer window drawerParams?.let { params -> params.height = dpToPx(DRAWER_HEIGHT_COLLAPSED_DP) drawerContainer?.let { container -> windowManager?.updateViewLayout(container, params) } } PGHLog.d(TAG, "Drawer collapsed") } private fun animateIn() { drawerContainer?.let { container -> val screenHeight = getScreenSize().second container.translationY = dpToPx(DRAWER_HEIGHT_COLLAPSED_DP).toFloat() ObjectAnimator.ofFloat(container, "translationY", container.translationY, 0f).apply { duration = SLIDE_ANIMATION_DURATION interpolator = AccelerateDecelerateInterpolator() start() } } } private fun animateOut(onComplete: () -> Unit) { drawerContainer?.let { container -> val currentHeight = if (isExpanded) DRAWER_HEIGHT_EXPANDED_DP else DRAWER_HEIGHT_COLLAPSED_DP ObjectAnimator.ofFloat(container, "translationY", 0f, dpToPx(currentHeight).toFloat()).apply { duration = SLIDE_ANIMATION_DURATION interpolator = AccelerateDecelerateInterpolator() addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onComplete() } }) start() } } } private fun cleanup() { drawerContainer = null drawerParams = null windowManager = null currentDetectionResult = null isShowing = false isExpanded = false } private fun getScreenSize(): Pair { val displayMetrics = context.resources.displayMetrics return Pair(displayMetrics.widthPixels, displayMetrics.heightPixels) } private fun dpToPx(dp: Int): Int { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, dp.toFloat(), context.resources.displayMetrics ).toInt() } }