Browse Source
- Create FloatingUIActivity with true Material 3 FloatingActionButton - Implement Binder communication between UI Activity and Service - Remove UI handling from ScreenCaptureService (now pure background service) - Add proper Material 3 animations and haptic feedback - Update MainActivity to launch FloatingUIActivity on capture start - Register FloatingUIActivity in AndroidManifest with transparent theme Architecture: FloatingUIActivity (Compose UI) ↔ Binder ↔ ScreenCaptureService (Detection) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>feature/modern-capture-ui
9 changed files with 1196 additions and 10 deletions
@ -0,0 +1,187 @@ |
|||||
|
# UI Modernization Tasks - Capture Mode |
||||
|
|
||||
|
## Overview |
||||
|
This document outlines the planned modernization of the capture mode UI, transforming it from a basic floating button system to a modern, feature-rich interface. |
||||
|
|
||||
|
## Current State |
||||
|
- Basic floating "orb" (actually just a styled Button) |
||||
|
- Simple expandable menu with 5 options |
||||
|
- Basic visual state feedback |
||||
|
- MVC architecture with proper event handling |
||||
|
|
||||
|
## Phase 1: Core Modernization ⚡ |
||||
|
|
||||
|
### 1.1 Material 3 Floating Action Button |
||||
|
**Priority: HIGH** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
Convert the current basic floating orb to a proper Material 3 FAB: |
||||
|
- Use Material 3 FAB component with proper elevation and shadows |
||||
|
- Smooth expand/collapse animations using AnimatedVisibility |
||||
|
- Material 3 color theming and dynamic colors |
||||
|
- Proper ripple effects and touch feedback |
||||
|
- Size variants (mini FAB for menu items) |
||||
|
|
||||
|
**Technical Details:** |
||||
|
- Replace `Button` with `FloatingActionButton` in Jetpack Compose |
||||
|
- Implement `AnimatedVisibility` for menu expansion |
||||
|
- Use `Material3` theme colors and elevation tokens |
||||
|
- Add haptic feedback for interactions |
||||
|
|
||||
|
### 1.2 Visual Design Modernization |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- **Glass-morphism Effects**: Semi-transparent backgrounds with blur |
||||
|
- **Smooth Animations**: Choreographed transitions between states |
||||
|
- **Modern Icons**: Vector icons with proper scaling |
||||
|
- **Color Schemes**: Material You dynamic theming |
||||
|
- **Typography**: Material 3 typography scale |
||||
|
|
||||
|
**Technical Details:** |
||||
|
- Implement blur effects using `Modifier.blur()` |
||||
|
- Custom animation specs for fluid motion |
||||
|
- Vector Drawable icons with animated state changes |
||||
|
- Dynamic color extraction from wallpaper (Android 12+) |
||||
|
|
||||
|
## Phase 2: Enhanced Functionality 📊 |
||||
|
|
||||
|
### 2.1 Detection Results Overlay |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
Real-time overlay showing detection results: |
||||
|
- Bounding boxes with confidence scores |
||||
|
- Class labels with color coding |
||||
|
- Performance metrics (FPS, inference time) |
||||
|
- Detection count by category |
||||
|
|
||||
|
**Technical Details:** |
||||
|
- Custom Canvas drawing for overlays |
||||
|
- WindowManager overlay with proper Z-ordering |
||||
|
- Real-time data binding from detection controller |
||||
|
- Optimized rendering to avoid performance impact |
||||
|
|
||||
|
### 2.2 Live Preview & Status |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- Mini preview window showing current screen region |
||||
|
- Real-time detection status indicators |
||||
|
- Processing queue visualization |
||||
|
- Error state handling with retry options |
||||
|
|
||||
|
### 2.3 Settings Panel Integration |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
Built-in settings accessible from floating UI: |
||||
|
- Detection sensitivity sliders |
||||
|
- Class filter toggles with visual preview |
||||
|
- Coordinate transformation mode selection |
||||
|
- Debug options panel |
||||
|
|
||||
|
## Phase 3: Advanced UX Patterns 🎯 |
||||
|
|
||||
|
### 3.1 Gesture-Based Interactions |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- **Long Press**: Context menu with advanced options |
||||
|
- **Swipe Gestures**: Quick filter switching |
||||
|
- **Drag**: Repositioning FAB location |
||||
|
- **Double Tap**: Quick detection trigger |
||||
|
|
||||
|
**Technical Details:** |
||||
|
- Custom gesture detection using `Modifier.pointerInput()` |
||||
|
- Haptic feedback patterns for different gestures |
||||
|
- Visual feedback during gesture recognition |
||||
|
- Gesture customization settings |
||||
|
|
||||
|
### 3.2 Contextual Intelligence |
||||
|
**Priority: MEDIUM** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
Smart behavior based on current context: |
||||
|
- Auto-hide during active detection |
||||
|
- Context-aware menu options |
||||
|
- Smart positioning to avoid UI occlusion |
||||
|
- Adaptive timeout based on usage patterns |
||||
|
|
||||
|
### 3.3 Accessibility Enhancements |
||||
|
**Priority: HIGH** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- Screen reader compatibility |
||||
|
- Voice commands integration |
||||
|
- High contrast mode support |
||||
|
- Large text scaling support |
||||
|
- Keyboard navigation |
||||
|
|
||||
|
## Phase 4: Advanced Features 🚀 |
||||
|
|
||||
|
### 4.1 Detection Analytics Dashboard |
||||
|
**Priority: LOW** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- Mini-map showing detection regions |
||||
|
- Historical detection data |
||||
|
- Performance trend graphs |
||||
|
- Export detection logs |
||||
|
|
||||
|
### 4.2 Smart Automation |
||||
|
**Priority: LOW** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- Auto-detection based on screen content changes |
||||
|
- Smart filtering based on user behavior |
||||
|
- Predictive UI adaptation |
||||
|
- Background processing optimization |
||||
|
|
||||
|
### 4.3 Integration Features |
||||
|
**Priority: LOW** |
||||
|
**Status: PENDING** |
||||
|
|
||||
|
- Screenshot annotation and sharing |
||||
|
- Detection result export (JSON, CSV) |
||||
|
- Cloud sync for settings |
||||
|
- Integration with external Pokemon databases |
||||
|
|
||||
|
## Technical Architecture |
||||
|
|
||||
|
### Component Structure |
||||
|
``` |
||||
|
FloatingActionButtonUI (Compose) |
||||
|
├── FABCore (Material 3 FAB) |
||||
|
├── ExpandableMenu (AnimatedVisibility) |
||||
|
├── DetectionOverlay (Canvas) |
||||
|
├── SettingsPanel (BottomSheet/Drawer) |
||||
|
└── StatusIndicators (Badges/Chips) |
||||
|
``` |
||||
|
|
||||
|
### Performance Considerations |
||||
|
- Lazy composition for menu items |
||||
|
- Efficient recomposition boundaries |
||||
|
- Background thread processing |
||||
|
- Memory management for overlays |
||||
|
- Battery optimization strategies |
||||
|
|
||||
|
## Implementation Timeline |
||||
|
|
||||
|
**Sprint 1 (Week 1-2)**: Material 3 FAB conversion |
||||
|
**Sprint 2 (Week 3-4)**: Visual design modernization |
||||
|
**Sprint 3 (Week 5-6)**: Detection results overlay |
||||
|
**Sprint 4 (Week 7-8)**: Enhanced UX patterns |
||||
|
**Sprint 5+ (Future)**: Advanced features as needed |
||||
|
|
||||
|
## Success Metrics |
||||
|
|
||||
|
- **User Experience**: Smooth 60fps animations, <100ms interaction response |
||||
|
- **Functionality**: All current features preserved + new capabilities |
||||
|
- **Performance**: No degradation in detection accuracy or speed |
||||
|
- **Accessibility**: WCAG 2.1 AA compliance |
||||
|
- **Code Quality**: Maintainable Compose architecture |
||||
|
|
||||
|
--- |
||||
|
|
||||
|
**Note**: This is a living document that will be updated as features are implemented and new requirements emerge. |
||||
@ -0,0 +1,580 @@ |
|||||
|
package com.quillstudios.pokegoalshelper.ui |
||||
|
|
||||
|
import android.animation.AnimatorSet |
||||
|
import android.animation.ObjectAnimator |
||||
|
import android.animation.ValueAnimator |
||||
|
import android.content.Context |
||||
|
import android.graphics.* |
||||
|
import android.graphics.drawable.GradientDrawable |
||||
|
import android.os.Build |
||||
|
import android.os.VibrationEffect |
||||
|
import android.os.Vibrator |
||||
|
import android.util.Log |
||||
|
import android.view.Gravity |
||||
|
import android.view.View |
||||
|
import android.view.ViewGroup |
||||
|
import android.view.WindowManager |
||||
|
import android.view.animation.AccelerateDecelerateInterpolator |
||||
|
import android.view.animation.OvershootInterpolator |
||||
|
import android.widget.FrameLayout |
||||
|
import android.widget.ImageView |
||||
|
import android.widget.LinearLayout |
||||
|
import android.widget.TextView |
||||
|
import androidx.core.content.ContextCompat |
||||
|
// import com.quillstudios.pokegoalshelper.R // Not needed for now |
||||
|
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUIEvents |
||||
|
import com.quillstudios.pokegoalshelper.ui.interfaces.DetectionUICallbacks |
||||
|
|
||||
|
/** |
||||
|
* Modern Material 3 Floating Action Button UI for capture mode. |
||||
|
* Replaces the basic floating orb with a proper FAB implementation. |
||||
|
* |
||||
|
* Features: |
||||
|
* - Material 3 design with proper elevation and shadows |
||||
|
* - Smooth animations for state changes |
||||
|
* - Haptic feedback for interactions |
||||
|
* - Glass-morphism effects |
||||
|
* - Contextual menu expansion |
||||
|
*/ |
||||
|
class FloatingActionButtonUI( |
||||
|
private val context: Context, |
||||
|
private val detectionEvents: DetectionUIEvents |
||||
|
) : DetectionUICallbacks { |
||||
|
|
||||
|
companion object { |
||||
|
private const val TAG = "FloatingActionButtonUI" |
||||
|
|
||||
|
// Material 3 FAB dimensions (dp converted to px) |
||||
|
private const val FAB_SIZE_LARGE = 56 // Main FAB |
||||
|
private const val FAB_SIZE_SMALL = 40 // Menu item FABs |
||||
|
private const val FAB_MARGIN = 16 |
||||
|
|
||||
|
// Animation durations |
||||
|
private const val ANIMATION_DURATION_FAST = 150L |
||||
|
private const val ANIMATION_DURATION_MEDIUM = 300L |
||||
|
|
||||
|
// Material 3 elevation levels |
||||
|
private const val ELEVATION_RESTING = 6f |
||||
|
private const val ELEVATION_PRESSED = 12f |
||||
|
private const val ELEVATION_MENU = 8f |
||||
|
} |
||||
|
|
||||
|
private var windowManager: WindowManager? = null |
||||
|
private var fabContainer: FrameLayout? = null |
||||
|
private var mainFab: View? = null |
||||
|
private var expandedMenu: LinearLayout? = null |
||||
|
private var scrimView: View? = null |
||||
|
|
||||
|
private var isMenuExpanded = false |
||||
|
private var isProcessing = false |
||||
|
private var currentAnimator: AnimatorSet? = null |
||||
|
|
||||
|
private val vibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as? Vibrator |
||||
|
|
||||
|
/** |
||||
|
* Initialize and show the floating action button |
||||
|
*/ |
||||
|
fun show() { |
||||
|
try { |
||||
|
if (fabContainer != null) return // Already shown |
||||
|
|
||||
|
windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager |
||||
|
createFloatingActionButton() |
||||
|
Log.d(TAG, "✅ Modern FAB UI shown") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ Error showing FAB", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Hide and cleanup the floating action button |
||||
|
*/ |
||||
|
fun hide() { |
||||
|
try { |
||||
|
currentAnimator?.cancel() |
||||
|
|
||||
|
if (isMenuExpanded) { |
||||
|
collapseMenu(animate = false) |
||||
|
} |
||||
|
|
||||
|
fabContainer?.let { |
||||
|
windowManager?.removeView(it) |
||||
|
fabContainer = null |
||||
|
} |
||||
|
|
||||
|
mainFab = null |
||||
|
expandedMenu = null |
||||
|
windowManager = null |
||||
|
Log.d(TAG, "🗑️ FAB UI hidden") |
||||
|
|
||||
|
} catch (e: Exception) { |
||||
|
Log.e(TAG, "❌ Error hiding FAB", e) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// === DetectionUICallbacks Implementation === |
||||
|
|
||||
|
override fun onDetectionStarted() { |
||||
|
isProcessing = true |
||||
|
updateFabState() |
||||
|
|
||||
|
// Auto-collapse menu during processing |
||||
|
if (isMenuExpanded) { |
||||
|
collapseMenu() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onDetectionCompleted(detectionCount: Int) { |
||||
|
isProcessing = false |
||||
|
updateFabState() |
||||
|
|
||||
|
// Provide haptic feedback for completion |
||||
|
provideFeedback(VibrationEffect.EFFECT_TICK) |
||||
|
Log.d(TAG, "🎯 Detection completed: $detectionCount objects") |
||||
|
} |
||||
|
|
||||
|
override fun onDetectionFailed(error: String) { |
||||
|
isProcessing = false |
||||
|
updateFabState() |
||||
|
|
||||
|
// Provide error feedback |
||||
|
provideFeedback(VibrationEffect.EFFECT_DOUBLE_CLICK) |
||||
|
Log.e(TAG, "❌ Detection failed: $error") |
||||
|
} |
||||
|
|
||||
|
override fun onSettingsChanged(filterClass: String?, debugMode: Boolean, coordinateMode: String) { |
||||
|
Log.d(TAG, "⚙️ Settings updated - Filter: $filterClass, Debug: $debugMode, Mode: $coordinateMode") |
||||
|
// Update menu items based on current settings |
||||
|
updateMenuItems() |
||||
|
} |
||||
|
|
||||
|
// === Private UI Methods === |
||||
|
|
||||
|
private fun createFloatingActionButton() { |
||||
|
// Create main container |
||||
|
fabContainer = FrameLayout(context).apply { |
||||
|
layoutParams = FrameLayout.LayoutParams( |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Create main FAB |
||||
|
mainFab = createMainFab() |
||||
|
fabContainer?.addView(mainFab) |
||||
|
|
||||
|
// Set up window parameters |
||||
|
val params = WindowManager.LayoutParams( |
||||
|
WindowManager.LayoutParams.WRAP_CONTENT, |
||||
|
WindowManager.LayoutParams.WRAP_CONTENT, |
||||
|
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, |
||||
|
PixelFormat.TRANSLUCENT |
||||
|
).apply { |
||||
|
gravity = Gravity.BOTTOM or Gravity.END |
||||
|
x = dpToPx(FAB_MARGIN) |
||||
|
y = dpToPx(FAB_MARGIN + 80) // Account for potential system UI |
||||
|
} |
||||
|
|
||||
|
windowManager?.addView(fabContainer, params) |
||||
|
} |
||||
|
|
||||
|
private fun createMainFab(): View { |
||||
|
return FrameLayout(context).apply { |
||||
|
val fabSize = dpToPx(FAB_SIZE_LARGE) |
||||
|
layoutParams = FrameLayout.LayoutParams(fabSize, fabSize) |
||||
|
|
||||
|
// Create FAB background with Material 3 styling |
||||
|
background = createFabBackground( |
||||
|
color = getColor(android.R.color.holo_blue_bright), |
||||
|
elevation = ELEVATION_RESTING |
||||
|
) |
||||
|
|
||||
|
// Add icon |
||||
|
val iconView = ImageView(context).apply { |
||||
|
layoutParams = FrameLayout.LayoutParams( |
||||
|
dpToPx(24), dpToPx(24), Gravity.CENTER |
||||
|
) |
||||
|
setImageResource(android.R.drawable.ic_menu_camera) // Using target icon |
||||
|
setColorFilter(Color.WHITE) |
||||
|
scaleType = ImageView.ScaleType.FIT_CENTER |
||||
|
} |
||||
|
addView(iconView) |
||||
|
|
||||
|
// Set up click handling |
||||
|
setOnClickListener { handleFabClick() } |
||||
|
|
||||
|
// Add touch feedback |
||||
|
isClickable = true |
||||
|
isFocusable = true |
||||
|
|
||||
|
// Apply initial elevation |
||||
|
elevation = ELEVATION_RESTING |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun createFabBackground(color: Int, elevation: Float): GradientDrawable { |
||||
|
return GradientDrawable().apply { |
||||
|
shape = GradientDrawable.OVAL |
||||
|
setColor(color) |
||||
|
|
||||
|
// Add subtle gradient for depth |
||||
|
colors = intArrayOf( |
||||
|
lightenColor(color, 0.1f), |
||||
|
color, |
||||
|
darkenColor(color, 0.1f) |
||||
|
) |
||||
|
gradientType = GradientDrawable.RADIAL_GRADIENT |
||||
|
gradientRadius = dpToPx(FAB_SIZE_LARGE).toFloat() / 2 |
||||
|
|
||||
|
// Material 3 shadow simulation (since we can't use real elevation in overlays) |
||||
|
setStroke(dpToPx(1), Color.argb(30, 0, 0, 0)) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun handleFabClick() { |
||||
|
provideFeedback(VibrationEffect.EFFECT_CLICK) |
||||
|
|
||||
|
if (isProcessing) { |
||||
|
Log.d(TAG, "⚠️ Ignoring click - detection in progress") |
||||
|
return |
||||
|
} |
||||
|
|
||||
|
if (isMenuExpanded) { |
||||
|
collapseMenu() |
||||
|
} else { |
||||
|
expandMenu() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun expandMenu() { |
||||
|
if (isMenuExpanded || isProcessing) return |
||||
|
|
||||
|
// Create scrim for backdrop |
||||
|
createScrim() |
||||
|
|
||||
|
// Create expanded menu |
||||
|
expandedMenu = createExpandedMenu() |
||||
|
fabContainer?.addView(expandedMenu) |
||||
|
|
||||
|
// Animate expansion |
||||
|
animateMenuExpansion(true) |
||||
|
|
||||
|
isMenuExpanded = true |
||||
|
updateFabState() |
||||
|
} |
||||
|
|
||||
|
private fun collapseMenu(animate: Boolean = true) { |
||||
|
if (!isMenuExpanded) return |
||||
|
|
||||
|
if (animate) { |
||||
|
animateMenuExpansion(false) { |
||||
|
removeMenuViews() |
||||
|
} |
||||
|
} else { |
||||
|
removeMenuViews() |
||||
|
} |
||||
|
|
||||
|
isMenuExpanded = false |
||||
|
updateFabState() |
||||
|
} |
||||
|
|
||||
|
private fun createScrim() { |
||||
|
scrimView = View(context).apply { |
||||
|
layoutParams = ViewGroup.LayoutParams( |
||||
|
dpToPx(200), dpToPx(300) |
||||
|
) |
||||
|
setBackgroundColor(Color.argb(80, 0, 0, 0)) // Semi-transparent scrim |
||||
|
alpha = 0f |
||||
|
setOnClickListener { collapseMenu() } |
||||
|
} |
||||
|
fabContainer?.addView(scrimView, 0) // Add as first child (background) |
||||
|
} |
||||
|
|
||||
|
private fun createExpandedMenu(): LinearLayout { |
||||
|
return LinearLayout(context).apply { |
||||
|
orientation = LinearLayout.VERTICAL |
||||
|
layoutParams = FrameLayout.LayoutParams( |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, |
||||
|
Gravity.BOTTOM or Gravity.END |
||||
|
).apply { |
||||
|
bottomMargin = dpToPx(FAB_SIZE_LARGE + 16) |
||||
|
rightMargin = dpToPx(8) |
||||
|
} |
||||
|
|
||||
|
// Create menu items |
||||
|
val menuItems = listOf( |
||||
|
FabMenuItem("🔍", "DETECT", getColor(android.R.color.holo_green_dark)) { |
||||
|
detectionEvents.onDetectionRequested() |
||||
|
}, |
||||
|
FabMenuItem("✨", "SHINY", getColor(android.R.color.holo_orange_light)) { |
||||
|
detectionEvents.onClassFilterChanged("shiny_icon") |
||||
|
detectionEvents.onDetectionRequested() |
||||
|
}, |
||||
|
FabMenuItem("⚪", "BALL", getColor(android.R.color.holo_red_dark)) { |
||||
|
detectionEvents.onClassFilterChanged("ball_icon_cherishball") |
||||
|
detectionEvents.onDetectionRequested() |
||||
|
}, |
||||
|
FabMenuItem("🎯", "ALL", getColor(android.R.color.darker_gray)) { |
||||
|
detectionEvents.onClassFilterChanged(null) |
||||
|
detectionEvents.onDetectionRequested() |
||||
|
}, |
||||
|
FabMenuItem("🔧", "DEBUG", getColor(android.R.color.holo_purple)) { |
||||
|
detectionEvents.onDebugModeToggled() |
||||
|
detectionEvents.onDetectionRequested() |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
menuItems.forEachIndexed { index, item -> |
||||
|
val fabItem = createMenuFab(item) |
||||
|
addView(fabItem) |
||||
|
|
||||
|
// Add spacing between items |
||||
|
if (index < menuItems.size - 1) { |
||||
|
val spacer = View(context).apply { |
||||
|
layoutParams = LinearLayout.LayoutParams( |
||||
|
ViewGroup.LayoutParams.MATCH_PARENT, dpToPx(8) |
||||
|
) |
||||
|
} |
||||
|
addView(spacer) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun createMenuFab(item: FabMenuItem): View { |
||||
|
return LinearLayout(context).apply { |
||||
|
orientation = LinearLayout.HORIZONTAL |
||||
|
gravity = Gravity.CENTER_VERTICAL |
||||
|
layoutParams = LinearLayout.LayoutParams( |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT, |
||||
|
ViewGroup.LayoutParams.WRAP_CONTENT |
||||
|
) |
||||
|
|
||||
|
// Add label |
||||
|
val label = TextView(context).apply { |
||||
|
text = item.label |
||||
|
setTextColor(Color.WHITE) |
||||
|
textSize = 12f |
||||
|
setPadding(dpToPx(8), dpToPx(4), dpToPx(8), dpToPx(4)) |
||||
|
setBackgroundResource(android.R.drawable.btn_default) |
||||
|
background.setTint(Color.argb(200, 0, 0, 0)) |
||||
|
alpha = 0f // Start invisible for animation |
||||
|
} |
||||
|
addView(label) |
||||
|
|
||||
|
// Add small FAB |
||||
|
val miniFab = FrameLayout(context).apply { |
||||
|
val size = dpToPx(FAB_SIZE_SMALL) |
||||
|
layoutParams = LinearLayout.LayoutParams(size, size).apply { |
||||
|
leftMargin = dpToPx(8) |
||||
|
} |
||||
|
|
||||
|
background = createFabBackground(item.color, ELEVATION_MENU) |
||||
|
elevation = ELEVATION_MENU |
||||
|
alpha = 0f // Start invisible for animation |
||||
|
|
||||
|
// Add icon |
||||
|
val iconView = TextView(context).apply { |
||||
|
text = item.icon |
||||
|
textSize = 16f |
||||
|
gravity = Gravity.CENTER |
||||
|
layoutParams = FrameLayout.LayoutParams( |
||||
|
ViewGroup.LayoutParams.MATCH_PARENT, |
||||
|
ViewGroup.LayoutParams.MATCH_PARENT |
||||
|
) |
||||
|
} |
||||
|
addView(iconView) |
||||
|
|
||||
|
setOnClickListener { |
||||
|
provideFeedback(VibrationEffect.EFFECT_CLICK) |
||||
|
item.action() |
||||
|
collapseMenu() |
||||
|
} |
||||
|
|
||||
|
isClickable = true |
||||
|
isFocusable = true |
||||
|
} |
||||
|
addView(miniFab) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun animateMenuExpansion(expand: Boolean, onComplete: (() -> Unit)? = null) { |
||||
|
currentAnimator?.cancel() |
||||
|
|
||||
|
val animatorSet = AnimatorSet() |
||||
|
val animators = mutableListOf<ObjectAnimator>() |
||||
|
|
||||
|
// Animate scrim |
||||
|
scrimView?.let { scrim -> |
||||
|
animators.add(ObjectAnimator.ofFloat( |
||||
|
scrim, "alpha", |
||||
|
if (expand) 0f else 1f, |
||||
|
if (expand) 1f else 0f |
||||
|
)) |
||||
|
} |
||||
|
|
||||
|
// Animate menu items |
||||
|
expandedMenu?.let { menu -> |
||||
|
for (i in 0 until menu.childCount) { |
||||
|
val child = menu.getChildAt(i) |
||||
|
if (child is LinearLayout) { |
||||
|
// Animate each FAB item |
||||
|
val delay = if (expand) i * 50L else (menu.childCount - i) * 30L |
||||
|
|
||||
|
val scaleX = ObjectAnimator.ofFloat( |
||||
|
child, "scaleX", |
||||
|
if (expand) 0f else 1f, |
||||
|
if (expand) 1f else 0f |
||||
|
) |
||||
|
val scaleY = ObjectAnimator.ofFloat( |
||||
|
child, "scaleY", |
||||
|
if (expand) 0f else 1f, |
||||
|
if (expand) 1f else 0f |
||||
|
) |
||||
|
val alpha = ObjectAnimator.ofFloat( |
||||
|
child, "alpha", |
||||
|
if (expand) 0f else 1f, |
||||
|
if (expand) 1f else 0f |
||||
|
) |
||||
|
|
||||
|
scaleX.startDelay = delay |
||||
|
scaleY.startDelay = delay |
||||
|
alpha.startDelay = delay |
||||
|
|
||||
|
animators.addAll(listOf(scaleX, scaleY, alpha)) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Configure animation set |
||||
|
animatorSet.playTogether(animators) |
||||
|
animatorSet.duration = ANIMATION_DURATION_MEDIUM |
||||
|
animatorSet.interpolator = if (expand) OvershootInterpolator() else AccelerateDecelerateInterpolator() |
||||
|
|
||||
|
animatorSet.addListener(object : android.animation.AnimatorListenerAdapter() { |
||||
|
override fun onAnimationEnd(animation: android.animation.Animator) { |
||||
|
onComplete?.invoke() |
||||
|
currentAnimator = null |
||||
|
} |
||||
|
}) |
||||
|
|
||||
|
currentAnimator = animatorSet |
||||
|
animatorSet.start() |
||||
|
} |
||||
|
|
||||
|
private fun removeMenuViews() { |
||||
|
scrimView?.let { fabContainer?.removeView(it) } |
||||
|
expandedMenu?.let { fabContainer?.removeView(it) } |
||||
|
scrimView = null |
||||
|
expandedMenu = null |
||||
|
} |
||||
|
|
||||
|
private fun updateFabState() { |
||||
|
val iconView = (mainFab as? FrameLayout)?.getChildAt(0) as? ImageView |
||||
|
val fab = mainFab as? FrameLayout |
||||
|
|
||||
|
when { |
||||
|
isProcessing -> { |
||||
|
iconView?.setImageResource(android.R.drawable.ic_popup_sync) |
||||
|
fab?.background = createFabBackground( |
||||
|
getColor(android.R.color.holo_orange_light), |
||||
|
ELEVATION_PRESSED |
||||
|
) |
||||
|
// Add rotation animation for processing state |
||||
|
animateProcessing(iconView) |
||||
|
} |
||||
|
isMenuExpanded -> { |
||||
|
iconView?.setImageResource(android.R.drawable.ic_menu_close_clear_cancel) |
||||
|
fab?.background = createFabBackground( |
||||
|
getColor(android.R.color.holo_red_light), |
||||
|
ELEVATION_PRESSED |
||||
|
) |
||||
|
} |
||||
|
else -> { |
||||
|
iconView?.setImageResource(android.R.drawable.ic_menu_camera) |
||||
|
fab?.background = createFabBackground( |
||||
|
getColor(android.R.color.holo_blue_bright), |
||||
|
ELEVATION_RESTING |
||||
|
) |
||||
|
iconView?.clearAnimation() |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun updateMenuItems() { |
||||
|
// This could update menu item states based on current filter/debug settings |
||||
|
// For now, just log that we received the update |
||||
|
Log.d(TAG, "Menu items updated based on current settings") |
||||
|
} |
||||
|
|
||||
|
private fun animateProcessing(view: View?) { |
||||
|
view?.let { |
||||
|
val rotation = ObjectAnimator.ofFloat(it, "rotation", 0f, 360f) |
||||
|
rotation.duration = 1000L |
||||
|
rotation.repeatCount = ValueAnimator.INFINITE |
||||
|
rotation.interpolator = AccelerateDecelerateInterpolator() |
||||
|
rotation.start() |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun provideFeedback(effect: Int) { |
||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { |
||||
|
vibrator?.vibrate(VibrationEffect.createPredefined(effect)) |
||||
|
} else { |
||||
|
@Suppress("DEPRECATION") |
||||
|
vibrator?.vibrate(50) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// === Utility Methods === |
||||
|
|
||||
|
private fun dpToPx(dp: Int): Int { |
||||
|
return (dp * context.resources.displayMetrics.density).toInt() |
||||
|
} |
||||
|
|
||||
|
private fun getColor(colorRes: Int): Int { |
||||
|
return ContextCompat.getColor(context, colorRes) |
||||
|
} |
||||
|
|
||||
|
private fun lightenColor(color: Int, factor: Float): Int { |
||||
|
val red = Color.red(color) |
||||
|
val green = Color.green(color) |
||||
|
val blue = Color.blue(color) |
||||
|
|
||||
|
return Color.rgb( |
||||
|
(red + (255 - red) * factor).toInt().coerceAtMost(255), |
||||
|
(green + (255 - green) * factor).toInt().coerceAtMost(255), |
||||
|
(blue + (255 - blue) * factor).toInt().coerceAtMost(255) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
private fun darkenColor(color: Int, factor: Float): Int { |
||||
|
val red = Color.red(color) |
||||
|
val green = Color.green(color) |
||||
|
val blue = Color.blue(color) |
||||
|
|
||||
|
return Color.rgb( |
||||
|
(red * (1 - factor)).toInt().coerceAtLeast(0), |
||||
|
(green * (1 - factor)).toInt().coerceAtLeast(0), |
||||
|
(blue * (1 - factor)).toInt().coerceAtLeast(0) |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Data class for FAB menu items |
||||
|
*/ |
||||
|
private data class FabMenuItem( |
||||
|
val icon: String, |
||||
|
val label: String, |
||||
|
val color: Int, |
||||
|
val action: () -> Unit |
||||
|
) |
||||
|
} |
||||
@ -0,0 +1,368 @@ |
|||||
|
package com.quillstudios.pokegoalshelper.ui |
||||
|
|
||||
|
import android.app.Activity |
||||
|
import android.content.* |
||||
|
import android.os.Bundle |
||||
|
import android.os.IBinder |
||||
|
import android.util.Log |
||||
|
import android.view.WindowManager |
||||
|
import androidx.activity.ComponentActivity |
||||
|
import androidx.activity.compose.setContent |
||||
|
import androidx.compose.animation.* |
||||
|
import androidx.compose.animation.core.* |
||||
|
import androidx.compose.foundation.background |
||||
|
import androidx.compose.foundation.layout.* |
||||
|
import androidx.compose.foundation.shape.CircleShape |
||||
|
import androidx.compose.material.icons.Icons |
||||
|
import androidx.compose.material.icons.filled.* |
||||
|
import androidx.compose.material3.* |
||||
|
import androidx.compose.runtime.* |
||||
|
import androidx.compose.ui.Alignment |
||||
|
import androidx.compose.ui.Modifier |
||||
|
import androidx.compose.ui.draw.rotate |
||||
|
import androidx.compose.ui.graphics.Color |
||||
|
import androidx.compose.ui.graphics.vector.ImageVector |
||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType |
||||
|
import androidx.compose.ui.platform.LocalHapticFeedback |
||||
|
import androidx.compose.ui.unit.dp |
||||
|
import androidx.compose.ui.unit.sp |
||||
|
import androidx.compose.ui.window.Dialog |
||||
|
import androidx.compose.ui.window.DialogProperties |
||||
|
import com.quillstudios.pokegoalshelper.ScreenCaptureService |
||||
|
import com.quillstudios.pokegoalshelper.ui.theme.PokeGoalsHelperTheme |
||||
|
|
||||
|
/** |
||||
|
* Transparent Activity that hosts the floating Material 3 FAB UI. |
||||
|
* Communicates with ScreenCaptureService via Binder for detection functionality. |
||||
|
* |
||||
|
* This approach allows us to use true Jetpack Compose with Material 3 components |
||||
|
* while keeping the detection logic in a background service. |
||||
|
*/ |
||||
|
class FloatingUIActivity : ComponentActivity() { |
||||
|
|
||||
|
companion object { |
||||
|
private const val TAG = "FloatingUIActivity" |
||||
|
const val ACTION_SHOW_FAB = "SHOW_FAB" |
||||
|
const val ACTION_HIDE_FAB = "HIDE_FAB" |
||||
|
} |
||||
|
|
||||
|
private var screenCaptureService: ScreenCaptureService? = null |
||||
|
private var serviceBound = false |
||||
|
|
||||
|
// Service connection to communicate with ScreenCaptureService |
||||
|
private val serviceConnection = object : ServiceConnection { |
||||
|
override fun onServiceConnected(className: ComponentName, service: IBinder) { |
||||
|
Log.d(TAG, "Connected to ScreenCaptureService") |
||||
|
val binder = service as ScreenCaptureService.LocalBinder |
||||
|
screenCaptureService = binder.getService() |
||||
|
serviceBound = true |
||||
|
} |
||||
|
|
||||
|
override fun onServiceDisconnected(arg0: ComponentName) { |
||||
|
Log.d(TAG, "Disconnected from ScreenCaptureService") |
||||
|
screenCaptureService = null |
||||
|
serviceBound = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||
|
super.onCreate(savedInstanceState) |
||||
|
|
||||
|
// Make activity transparent and overlay-capable |
||||
|
setupTransparentOverlay() |
||||
|
|
||||
|
// Bind to the ScreenCaptureService |
||||
|
bindToScreenCaptureService() |
||||
|
|
||||
|
setContent { |
||||
|
PokeGoalsHelperTheme { |
||||
|
FloatingFABInterface( |
||||
|
onDetectionRequested = { requestDetection() }, |
||||
|
onClassFilterRequested = { className -> requestClassFilter(className) }, |
||||
|
onDebugToggled = { toggleDebugMode() }, |
||||
|
onClose = { finish() } |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
override fun onDestroy() { |
||||
|
super.onDestroy() |
||||
|
if (serviceBound) { |
||||
|
unbindService(serviceConnection) |
||||
|
serviceBound = false |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun setupTransparentOverlay() { |
||||
|
// Make the activity transparent |
||||
|
window.apply { |
||||
|
setFlags( |
||||
|
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL, |
||||
|
WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
||||
|
) |
||||
|
setFlags( |
||||
|
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, |
||||
|
WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
||||
|
) |
||||
|
|
||||
|
// Make it show over other apps |
||||
|
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { |
||||
|
setType(WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY) |
||||
|
} else { |
||||
|
@Suppress("DEPRECATION") |
||||
|
setType(WindowManager.LayoutParams.TYPE_PHONE) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private fun bindToScreenCaptureService() { |
||||
|
val intent = Intent(this, ScreenCaptureService::class.java) |
||||
|
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) |
||||
|
} |
||||
|
|
||||
|
// === Communication with Service === |
||||
|
|
||||
|
private fun requestDetection() { |
||||
|
screenCaptureService?.triggerManualDetection() |
||||
|
Log.d(TAG, "Requested detection from service") |
||||
|
} |
||||
|
|
||||
|
private fun requestClassFilter(className: String?) { |
||||
|
screenCaptureService?.setClassFilter(className) |
||||
|
Log.d(TAG, "Set class filter to: $className") |
||||
|
} |
||||
|
|
||||
|
private fun toggleDebugMode() { |
||||
|
screenCaptureService?.toggleDebugMode() |
||||
|
Log.d(TAG, "Toggled debug mode") |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun FloatingFABInterface( |
||||
|
onDetectionRequested: () -> Unit, |
||||
|
onClassFilterRequested: (String?) -> Unit, |
||||
|
onDebugToggled: () -> Unit, |
||||
|
onClose: () -> Unit |
||||
|
) { |
||||
|
var isMenuExpanded by remember { mutableStateOf(false) } |
||||
|
var isProcessing by remember { mutableStateOf(false) } |
||||
|
val hapticFeedback = LocalHapticFeedback.current |
||||
|
|
||||
|
Box( |
||||
|
modifier = Modifier |
||||
|
.fillMaxSize() |
||||
|
.background(Color.Transparent), |
||||
|
contentAlignment = Alignment.BottomEnd |
||||
|
) { |
||||
|
// Main content area - transparent to allow touches through |
||||
|
Spacer(modifier = Modifier.fillMaxSize()) |
||||
|
|
||||
|
// FAB and menu area |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.End, |
||||
|
verticalArrangement = Arrangement.Bottom, |
||||
|
modifier = Modifier.padding(16.dp) |
||||
|
) { |
||||
|
|
||||
|
// Expanded Menu Items |
||||
|
AnimatedVisibility( |
||||
|
visible = isMenuExpanded, |
||||
|
enter = fadeIn(animationSpec = tween(300)) + |
||||
|
slideInVertically( |
||||
|
initialOffsetY = { it / 2 }, |
||||
|
animationSpec = spring( |
||||
|
dampingRatio = Spring.DampingRatioMediumBouncy, |
||||
|
stiffness = Spring.StiffnessLow |
||||
|
) |
||||
|
), |
||||
|
exit = fadeOut(animationSpec = tween(200)) + |
||||
|
slideOutVertically( |
||||
|
targetOffsetY = { it / 2 }, |
||||
|
animationSpec = tween(200) |
||||
|
) |
||||
|
) { |
||||
|
Column( |
||||
|
horizontalAlignment = Alignment.End, |
||||
|
verticalArrangement = Arrangement.spacedBy(8.dp), |
||||
|
modifier = Modifier.padding(bottom = 16.dp) |
||||
|
) { |
||||
|
// Menu FAB items |
||||
|
MenuFABItem( |
||||
|
icon = Icons.Default.Search, |
||||
|
label = "DETECT", |
||||
|
containerColor = MaterialTheme.colorScheme.primary, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onDetectionRequested() |
||||
|
isMenuExpanded = false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
MenuFABItem( |
||||
|
icon = Icons.Default.Star, |
||||
|
label = "SHINY", |
||||
|
containerColor = MaterialTheme.colorScheme.tertiary, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onClassFilterRequested("shiny_icon") |
||||
|
onDetectionRequested() |
||||
|
isMenuExpanded = false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
MenuFABItem( |
||||
|
icon = Icons.Default.FiberManualRecord, |
||||
|
label = "POKEBALL", |
||||
|
containerColor = MaterialTheme.colorScheme.error, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onClassFilterRequested("ball_icon_cherishball") |
||||
|
onDetectionRequested() |
||||
|
isMenuExpanded = false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
MenuFABItem( |
||||
|
icon = Icons.Default.SelectAll, |
||||
|
label = "ALL", |
||||
|
containerColor = MaterialTheme.colorScheme.secondary, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onClassFilterRequested(null) |
||||
|
onDetectionRequested() |
||||
|
isMenuExpanded = false |
||||
|
} |
||||
|
) |
||||
|
|
||||
|
MenuFABItem( |
||||
|
icon = Icons.Default.BugReport, |
||||
|
label = "DEBUG", |
||||
|
containerColor = MaterialTheme.colorScheme.outline, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onDebugToggled() |
||||
|
onDetectionRequested() |
||||
|
isMenuExpanded = false |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// Main FAB |
||||
|
MainFloatingActionButton( |
||||
|
isProcessing = isProcessing, |
||||
|
isMenuExpanded = isMenuExpanded, |
||||
|
onClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
if (!isProcessing) { |
||||
|
isMenuExpanded = !isMenuExpanded |
||||
|
} |
||||
|
}, |
||||
|
onLongClick = { |
||||
|
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) |
||||
|
onClose() |
||||
|
} |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun MainFloatingActionButton( |
||||
|
isProcessing: Boolean, |
||||
|
isMenuExpanded: Boolean, |
||||
|
onClick: () -> Unit, |
||||
|
onLongClick: () -> Unit |
||||
|
) { |
||||
|
val rotation by animateFloatAsState( |
||||
|
targetValue = if (isMenuExpanded) 45f else 0f, |
||||
|
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy), |
||||
|
label = "FAB rotation" |
||||
|
) |
||||
|
|
||||
|
val processingRotation by animateFloatAsState( |
||||
|
targetValue = if (isProcessing) 360f else 0f, |
||||
|
animationSpec = infiniteRepeatable( |
||||
|
animation = tween(1000, easing = LinearEasing), |
||||
|
repeatMode = RepeatMode.Restart |
||||
|
), |
||||
|
label = "Processing rotation" |
||||
|
) |
||||
|
|
||||
|
FloatingActionButton( |
||||
|
onClick = onClick, |
||||
|
modifier = Modifier |
||||
|
.size(56.dp) |
||||
|
.rotate(if (isProcessing) processingRotation else rotation), |
||||
|
containerColor = when { |
||||
|
isProcessing -> MaterialTheme.colorScheme.tertiary |
||||
|
isMenuExpanded -> MaterialTheme.colorScheme.error |
||||
|
else -> MaterialTheme.colorScheme.primary |
||||
|
}, |
||||
|
elevation = FloatingActionButtonDefaults.elevation( |
||||
|
defaultElevation = 6.dp, |
||||
|
pressedElevation = 12.dp |
||||
|
) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = when { |
||||
|
isProcessing -> Icons.Default.Sync |
||||
|
isMenuExpanded -> Icons.Default.Close |
||||
|
else -> Icons.Default.CameraAlt |
||||
|
}, |
||||
|
contentDescription = when { |
||||
|
isProcessing -> "Processing..." |
||||
|
isMenuExpanded -> "Close menu" |
||||
|
else -> "Open detection menu" |
||||
|
}, |
||||
|
tint = MaterialTheme.colorScheme.onPrimary |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Composable |
||||
|
fun MenuFABItem( |
||||
|
icon: ImageVector, |
||||
|
label: String, |
||||
|
containerColor: Color, |
||||
|
onClick: () -> Unit |
||||
|
) { |
||||
|
Row( |
||||
|
verticalAlignment = Alignment.CenterVertically, |
||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp) |
||||
|
) { |
||||
|
// Label |
||||
|
Surface( |
||||
|
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f), |
||||
|
shape = MaterialTheme.shapes.small, |
||||
|
shadowElevation = 2.dp |
||||
|
) { |
||||
|
Text( |
||||
|
text = label, |
||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp), |
||||
|
color = MaterialTheme.colorScheme.onSurface, |
||||
|
fontSize = 12.sp |
||||
|
) |
||||
|
} |
||||
|
|
||||
|
// Mini FAB |
||||
|
FloatingActionButton( |
||||
|
onClick = onClick, |
||||
|
modifier = Modifier.size(40.dp), |
||||
|
containerColor = containerColor, |
||||
|
elevation = FloatingActionButtonDefaults.elevation( |
||||
|
defaultElevation = 4.dp, |
||||
|
pressedElevation = 8.dp |
||||
|
) |
||||
|
) { |
||||
|
Icon( |
||||
|
imageVector = icon, |
||||
|
contentDescription = label, |
||||
|
tint = Color.White, |
||||
|
modifier = Modifier.size(20.dp) |
||||
|
) |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
After Width: | Height: | Size: 306 KiB |
|
After Width: | Height: | Size: 555 KiB |
Loading…
Reference in new issue