@ -34,10 +34,11 @@ class ResultsBottomDrawer(private val context: Context)
companion object
{
private const val TAG = " ResultsBottomDrawer "
private const val DRAWER_HEIGHT_DP = 120
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 AUTO_DISMISS_DELAY = 5000L // 5 seconds
private const val SWIPE_THRESHOLD = 100f
private const val EXPAND_THRESHOLD = - 50f // Negative because we're pulling up
}
private var windowManager : WindowManager ? = null
@ -45,7 +46,8 @@ class ResultsBottomDrawer(private val context: Context)
private var drawerParams : WindowManager . LayoutParams ? = null
private var isShowing = false
private var isDragging = false
private var autoDismissRunnable : Runnable ? = null
private var isExpanded = false
private var currentDetectionResult : DetectionResult ? = null
// Touch handling
private var initialTouchY = 0f
@ -58,12 +60,10 @@ class ResultsBottomDrawer(private val context: Context)
try
{
windowManager = context . getSystemService ( Context . WINDOW_SERVICE ) as WindowManager
currentDetectionResult = result
createDrawerView ( result )
isShowing = true
// Schedule auto-dismiss
scheduleAutoDismiss ( )
PGHLog . d ( TAG , " Bottom drawer shown for detection: ${result.id} " )
}
catch ( e : Exception )
@ -78,9 +78,6 @@ class ResultsBottomDrawer(private val context: Context)
try
{
// Cancel auto-dismiss
autoDismissRunnable ?. let { android . os . Handler ( ) . removeCallbacks ( it ) }
// Animate out
animateOut {
try
@ -105,7 +102,7 @@ class ResultsBottomDrawer(private val context: Context)
private fun createDrawerView ( result : DetectionResult )
{
val screenSize = getScreenSize ( )
val drawerHeight = dpToPx ( DRAWER_HEIGHT_DP )
val drawerHeight = dpToPx ( DRAWER_HEIGHT_COLLAPSED_ DP ) // Start collapsed
// Create main container
drawerContainer = LinearLayout ( context ) . apply {
@ -117,11 +114,14 @@ class ResultsBottomDrawer(private val context: Context)
// Add drag handle
addView ( createDragHandle ( ) )
// Add result content
addView ( createResultContent ( result ) )
// Add collapsed content (always visible)
addView ( createCollapsedContent ( result ) )
// Add expanded content (initially hidden)
addView ( createExpandedContent ( result ) )
// Set up touch handling for swipe dismiss
setOnTouchListener ( createSwipeTouchListener ( ) )
// Set up touch handling for swipe and expand
setOnTouchListener ( createExpandable SwipeTouchListener ( ) )
}
// Create window parameters
@ -170,7 +170,7 @@ class ResultsBottomDrawer(private val context: Context)
}
}
private fun createResult Content ( result : DetectionResult ) : LinearLayout
private fun createCollapsed Content ( result : DetectionResult ) : LinearLayout
{
return LinearLayout ( context ) . apply {
orientation = LinearLayout . HORIZONTAL
@ -189,120 +189,207 @@ class ResultsBottomDrawer(private val context: Context)
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 ( 0 , 0 , dpToPx ( 12 ) , 0 )
setMargins ( dpToPx ( 8 ) , 0 , 0 , 0 )
}
}
// Content container
val contentContainer = LinearLayout ( context ) . apply {
orientation = LinearLayout . VERTICAL
layoutParams = LinearLayout . LayoutParams (
0 ,
ViewGroup . LayoutParams . WRAP_CONTENT ,
1f
)
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 < String > ( )
// 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 )
{
// 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 ) " ) }
// Pokemon name section
pokemonInfo . name ?. let { name ->
addView ( createDetailRow ( " Name " , name ) )
}
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
// National Dex Number
pokemonInfo . nationalDexNumber ?. let { dexNum ->
addView ( createDetailRow ( " Dex # " , " # $dexNum " ) )
}
// 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 " )
// Stats section
pokemonInfo . cp ?. let { cp ->
addView ( createDetailRow ( " CP " , cp . toString ( ) ) )
}
pokemonInfo . level ?. let { level ->
addView ( createDetailRow ( " Level " , String . format ( " %.1f " , level ) ) )
}
pokemonInfo . hp ?. let { hp ->
addView ( createDetailRow ( " HP " , hp . toString ( ) ) )
}
// IV Stats
pokemonInfo . stats ?. let { stats ->
stats . perfectIV ?. let { iv ->
addView ( createDetailRow ( " IV % " , " ${String.format("%.1f", iv)} % " ) )
}
// Individual stats
stats . attack ?. let { addView ( createDetailRow ( " Attack " , it . toString ( ) ) ) }
stats . defense ?. let { addView ( createDetailRow ( " Defense " , it . toString ( ) ) ) }
stats . stamina ?. let { addView ( createDetailRow ( " Stamina " , it . toString ( ) ) ) }
}
val detailsView = TextView ( context ) . apply {
text = detailsText
setTextSize ( TypedValue . COMPLEX_UNIT_SP , 12f )
setTextColor ( ContextCompat . getColor ( context , android . R . color . darker_gray ) )
// Other info
pokemonInfo . gender ?. let { gender ->
addView ( createDetailRow ( " Gender " , gender ) )
}
contentContainer . addView ( titleView )
if ( detailsText . isNotEmpty ( ) )
{
contentContainer . addView ( detailsView )
pokemonInfo . form ?. let { form ->
addView ( createDetailRow ( " Form " , form ) )
}
}
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
// Show error details
addView ( createDetailRow ( " Status " , if ( result . success ) " No Pokemon detected " else " Detection failed " ) )
result . errorMessage ?. let { error ->
addView ( createDetailRow ( " Error " , error ) )
}
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 " ) ) )
}
// Always show technical info
addView ( createDetailRow ( " Processing Time " , " ${result.processingTimeMs} ms " ) )
addView ( createDetailRow ( " Timestamp " , result . timestamp . format ( DateTimeFormatter . ofPattern ( " HH:mm:ss " ) ) ) )
addView ( createDetailRow ( " Detections Found " , result . detections . size . toString ( ) ) )
}
}
private fun createDetailRow ( label : String , value : String ) : LinearLayout
{
return LinearLayout ( context ) . apply {
orientation = LinearLayout . HORIZONTAL
gravity = Gravity . CENTER_VERTICAL
val metaView = TextView ( context ) . apply {
text = metaText
setTextSize ( TypedValue . COMPLEX_UNIT_SP , 10f )
val label View = TextView ( context ) . apply {
text = " $label : "
setTextSize ( TypedValue . COMPLEX_UNIT_SP , 11 f )
setTextColor ( ContextCompat . getColor ( context , android . R . color . darker_gray ) )
layoutParams = LinearLayout . LayoutParams (
dpToPx ( 80 ) ,
ViewGroup . LayoutParams . WRAP_CONTENT
)
}
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 ( ) }
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 (
dpToPx ( 32 ) ,
dpToPx ( 32 )
) . apply {
setMargins ( dpToPx ( 12 ) , 0 , 0 , 0 )
}
0 ,
ViewGroup . LayoutParams . WRAP_CONTENT ,
1f
)
}
addView ( statusIcon )
addView ( contentContainer )
addView ( dismissButton )
addView ( labelView )
addView ( valueView )
layoutParams = LinearLayout . LayoutParams (
ViewGroup . LayoutParams . MATCH_PARENT ,
ViewGroup . LayoutParams . WRAP_CONTENT
) . apply {
setMargins ( 0 , dpToPx ( 2 ) , 0 , dpToPx ( 2 ) )
}
}
}
@ -330,7 +417,7 @@ class ResultsBottomDrawer(private val context: Context)
}
}
private fun createSwipeTouchListener ( ) : View . OnTouchListener
private fun createExpandable SwipeTouchListener ( ) : View . OnTouchListener
{
return View . OnTouchListener { view , event ->
when ( event . action )
@ -340,9 +427,6 @@ class ResultsBottomDrawer(private val context: Context)
isDragging = false
initialTouchY = event . rawY
initialTranslationY = view . translationY
// Cancel auto-dismiss while user is interacting
autoDismissRunnable ?. let { android . os . Handler ( ) . removeCallbacks ( it ) }
true
}
@ -355,10 +439,18 @@ class ResultsBottomDrawer(private val context: Context)
isDragging = true
}
if ( isDragging && deltaY > 0 )
if ( isDragging )
{
// Only allow downward drag (dismissing)
view . translationY = initialTranslationY + deltaY
if ( deltaY > 0 )
{
// Downward drag - dismissing
view . translationY = initialTranslationY + deltaY
}
else if ( deltaY < 0 && !is Expanded )
{
// Upward drag - expanding (only if not already expanded)
// Don't move the view, just track the gesture
}
}
true
}
@ -368,28 +460,43 @@ class ResultsBottomDrawer(private val context: Context)
if ( isDragging )
{
val deltaY = event . rawY - initialTouchY
if ( deltaY > SWIPE_THRESHOLD )
{
// Dismiss if swiped down enough
hide ( )
}
else if ( deltaY < EXPAND_THRESHOLD && !is Expanded )
{
// Expand if swiped up enough
expandDrawer ( )
}
else if ( deltaY > - EXPAND_THRESHOLD && isExpanded )
{
// Collapse if swiped down a bit while expanded
collapseDrawer ( )
}
else
{
// Snap back
// Snap back to current state
ObjectAnimator . ofFloat ( view , " translationY " , view . translationY , initialTranslationY ) . apply {
duration = 200L
interpolator = AccelerateDecelerateInterpolator ( )
start ( )
}
// Restart auto-dismiss
scheduleAutoDismiss ( )
}
}
else
{
// Restart auto-dismiss on tap
scheduleAutoDismiss ( )
// Simple tap - toggle expand/collapse
if ( isExpanded )
{
collapseDrawer ( )
}
else
{
expandDrawer ( )
}
}
isDragging = false
@ -401,11 +508,69 @@ class ResultsBottomDrawer(private val context: Context)
}
}
private fun expandDrawer ( )
{
if ( isExpanded ) return
isExpanded = true
// Show expanded content
drawerContainer ?. findViewWithTag < LinearLayout > ( " 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 ( !is Expanded ) return
isExpanded = false
// Hide expanded content
drawerContainer ?. findViewWithTag < LinearLayout > ( " 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_DP ) . toFloat ( )
container . translationY = dpToPx ( DRAWER_HEIGHT_COLLAPSED_ DP ) . toFloat ( )
ObjectAnimator . ofFloat ( container , " translationY " , container . translationY , 0f ) . apply {
duration = SLIDE_ANIMATION_DURATION
@ -418,7 +583,8 @@ class ResultsBottomDrawer(private val context: Context)
private fun animateOut ( onComplete : ( ) -> Unit )
{
drawerContainer ?. let { container ->
ObjectAnimator . ofFloat ( container , " translationY " , 0f , dpToPx ( DRAWER_HEIGHT_DP ) . toFloat ( ) ) . apply {
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 ( ) {
@ -431,22 +597,14 @@ class ResultsBottomDrawer(private val context: Context)
}
}
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
currentDetectionResult = null
isShowing = false
autoDismissRunnable ?. let { android . os . Handler ( ) . removeCallbacks ( it ) }
autoDismissRunnable = null
isExpanded = false
}
private fun getScreenSize ( ) : Pair < Int , Int >