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_DP = 120 private const val SLIDE_ANIMATION_DURATION = 300L private const val AUTO_DISMISS_DELAY = 5000L // 5 seconds private const val SWIPE_THRESHOLD = 100f } 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 autoDismissRunnable: Runnable? = 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 createDrawerView(result) isShowing = true // Schedule auto-dismiss scheduleAutoDismiss() 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 { // Cancel auto-dismiss autoDismissRunnable?.let { android.os.Handler().removeCallbacks(it) } // 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_DP) // 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 result content addView(createResultContent(result)) // Set up touch handling for swipe dismiss setOnTouchListener(createSwipeTouchListener()) } // 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 createResultContent(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(24), dpToPx(24) ).apply { setMargins(0, 0, dpToPx(12), 0) } } // Content container val contentContainer = LinearLayout(context).apply { orientation = LinearLayout.VERTICAL layoutParams = LinearLayout.LayoutParams( 0, ViewGroup.LayoutParams.WRAP_CONTENT, 1f ) } if (result.success && result.pokemonInfo != null) { // Pokemon found - show details val pokemonInfo = result.pokemonInfo // Pokemon name and CP val titleText = buildString { append(pokemonInfo.name ?: "Unknown Pokemon") pokemonInfo.cp?.let { append(" (CP $it)") } } val titleView = TextView(context).apply { text = titleText setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) typeface = android.graphics.Typeface.DEFAULT_BOLD } // Additional details val detailsText = buildString { pokemonInfo.level?.let { append("Level ${String.format("%.1f", it)}") } pokemonInfo.stats?.perfectIV?.let { if (isNotEmpty()) append(" • ") append("${String.format("%.1f", it)}% IV") } pokemonInfo.hp?.let { if (isNotEmpty()) append(" • ") append("${it} HP") } } val detailsView = TextView(context).apply { text = detailsText setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) } contentContainer.addView(titleView) if (detailsText.isNotEmpty()) { contentContainer.addView(detailsView) } } else { // Detection failed or no Pokemon found val titleView = TextView(context).apply { text = if (result.success) "No Pokemon detected" else "Detection failed" setTextSize(TypedValue.COMPLEX_UNIT_SP, 16f) setTextColor(ContextCompat.getColor(context, android.R.color.white)) typeface = android.graphics.Typeface.DEFAULT_BOLD } val detailsView = TextView(context).apply { text = result.errorMessage ?: "Try again with a clearer view" setTextSize(TypedValue.COMPLEX_UNIT_SP, 12f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) } contentContainer.addView(titleView) contentContainer.addView(detailsView) } // Processing time and timestamp val metaText = buildString { append("${result.processingTimeMs}ms") append(" • ") append(result.timestamp.format(DateTimeFormatter.ofPattern("HH:mm:ss"))) } val metaView = TextView(context).apply { text = metaText setTextSize(TypedValue.COMPLEX_UNIT_SP, 10f) setTextColor(ContextCompat.getColor(context, android.R.color.darker_gray)) } contentContainer.addView(metaView) // 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(32), dpToPx(32) ).apply { setMargins(dpToPx(12), 0, 0, 0) } } addView(statusIcon) addView(contentContainer) addView(dismissButton) } } 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 createSwipeTouchListener(): View.OnTouchListener { return View.OnTouchListener { view, event -> when (event.action) { MotionEvent.ACTION_DOWN -> { isDragging = false initialTouchY = event.rawY initialTranslationY = view.translationY // Cancel auto-dismiss while user is interacting autoDismissRunnable?.let { android.os.Handler().removeCallbacks(it) } true } MotionEvent.ACTION_MOVE -> { val deltaY = event.rawY - initialTouchY if (!isDragging && abs(deltaY) > 20) { isDragging = true } if (isDragging && deltaY > 0) { // Only allow downward drag (dismissing) view.translationY = initialTranslationY + deltaY } true } MotionEvent.ACTION_UP -> { if (isDragging) { val deltaY = event.rawY - initialTouchY if (deltaY > SWIPE_THRESHOLD) { // Dismiss if swiped down enough hide() } else { // Snap back ObjectAnimator.ofFloat(view, "translationY", view.translationY, initialTranslationY).apply { duration = 200L interpolator = AccelerateDecelerateInterpolator() start() } // Restart auto-dismiss scheduleAutoDismiss() } } else { // Restart auto-dismiss on tap scheduleAutoDismiss() } isDragging = false true } else -> false } } } private fun animateIn() { drawerContainer?.let { container -> val screenHeight = getScreenSize().second container.translationY = dpToPx(DRAWER_HEIGHT_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 -> ObjectAnimator.ofFloat(container, "translationY", 0f, dpToPx(DRAWER_HEIGHT_DP).toFloat()).apply { duration = SLIDE_ANIMATION_DURATION interpolator = AccelerateDecelerateInterpolator() addListener(object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { onComplete() } }) start() } } } private fun scheduleAutoDismiss() { autoDismissRunnable?.let { android.os.Handler().removeCallbacks(it) } autoDismissRunnable = Runnable { hide() } android.os.Handler().postDelayed(autoDismissRunnable!!, AUTO_DISMISS_DELAY) } private fun cleanup() { drawerContainer = null drawerParams = null windowManager = null isShowing = false autoDismissRunnable?.let { android.os.Handler().removeCallbacks(it) } autoDismissRunnable = null } 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() } }