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 = 240 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 pokemonInfo.name?.let { dataPoints.add(it) } pokemonInfo.nationalDexNumber?.let { dataPoints.add("#$it") } pokemonInfo.cp?.let { dataPoints.add("CP $it") } pokemonInfo.level?.let { dataPoints.add("Lv${String.format("%.1f", it)}") } pokemonInfo.hp?.let { dataPoints.add("${it}HP") } pokemonInfo.stats?.perfectIV?.let { dataPoints.add("${String.format("%.1f", it)}%") } pokemonInfo.gender?.let { dataPoints.add(it) } // 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): LinearLayout { return LinearLayout(context).apply { orientation = LinearLayout.VERTICAL visibility = View.GONE // Initially hidden tag = "expanded_content" // For easy finding // Add some spacing setPadding(0, dpToPx(8), 0, 0) if (result.success && result.pokemonInfo != null) { val pokemonInfo = result.pokemonInfo // Pokemon Basic Info Section addView(createSectionHeader("Pokemon Info")) addView(createTwoColumnRow( leftLabel = "Name", leftValue = pokemonInfo.name ?: "Unknown", rightLabel = "Dex #", rightValue = pokemonInfo.nationalDexNumber?.let { "#$it" } ?: "N/A" )) addView(createTwoColumnRow( leftLabel = "Gender", leftValue = pokemonInfo.gender ?: "Unknown", rightLabel = "Form", rightValue = pokemonInfo.form ?: "Normal" )) // Combat Stats Section addView(createSectionHeader("Combat Stats")) addView(createTwoColumnRow( leftLabel = "CP", leftValue = pokemonInfo.cp?.toString() ?: "N/A", rightLabel = "HP", rightValue = pokemonInfo.hp?.toString() ?: "N/A" )) addView(createTwoColumnRow( leftLabel = "Level", leftValue = pokemonInfo.level?.let { String.format("%.1f", it) } ?: "N/A", rightLabel = "IV %", rightValue = pokemonInfo.stats?.perfectIV?.let { "${String.format("%.1f", it)}%" } ?: "N/A" )) // Individual Stats Section (if available) pokemonInfo.stats?.let { stats -> if (stats.attack != null || stats.defense != null || stats.stamina != null) { addView(createSectionHeader("Individual Stats")) addView(createThreeColumnRow( leftLabel = "ATK", leftValue = stats.attack?.toString() ?: "?", middleLabel = "DEF", middleValue = stats.defense?.toString() ?: "?", rightLabel = "STA", rightValue = stats.stamina?.toString() ?: "?" )) } } // Special Properties Section addView(createSectionHeader("Properties")) addView(createCheckboxRow( leftLabel = "Shiny", leftChecked = false, // TODO: get from pokemonInfo when available rightLabel = "Alpha", rightChecked = false // TODO: get from pokemonInfo when available )) } else { // Show error details addView(createSectionHeader("Detection Failed")) addView(createDetailRow("Status", if (result.success) "No Pokemon detected" else "Detection failed")) result.errorMessage?.let { error -> addView(createDetailRow("Error", error)) } } // Technical Info Section addView(createSectionHeader("Technical Info")) addView(createTwoColumnRow( leftLabel = "Processing", leftValue = "${result.processingTimeMs}ms", rightLabel = "Detected", rightValue = "${result.detections.size} items" )) addView(createDetailRow("Timestamp", result.timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss")))) } } 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 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) { // Dismiss if swiped down enough hide() } 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() } }