You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

466 lines
16 KiB

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<Int, Int>
{
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()
}
}