Migrate scaffold to Jetpack Compose
This commit is contained in:
parent
0aed749d31
commit
b346c69a65
@ -23,7 +23,7 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun NavBarEffects(
|
fun NavBarEffects(
|
||||||
modifier: Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@ -91,7 +91,7 @@ fun NavBarEffects(
|
|||||||
val bubble = newBubbles[i]
|
val bubble = newBubbles[i]
|
||||||
val oldBubble = bubbles[i]
|
val oldBubble = bubbles[i]
|
||||||
if (oldBubble.lifetime <= 0f) {
|
if (oldBubble.lifetime <= 0f) {
|
||||||
bubble.posX = (Math.random() * 64 - 32).toFloat() * dp
|
bubble.posX = (Math.random() * 48 - 24).toFloat() * dp
|
||||||
bubble.posY = 0f
|
bubble.posY = 0f
|
||||||
bubble.deltaX = (Math.random() - 0.5).toFloat() * 1f * dp
|
bubble.deltaX = (Math.random() - 0.5).toFloat() * 1f * dp
|
||||||
bubble.deltaY = Math.random().toFloat() * 3f * dp
|
bubble.deltaY = Math.random().toFloat() * 3f * dp
|
||||||
|
|||||||
@ -1,25 +1,36 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
import android.app.WallpaperManager
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.res.Configuration
|
import android.content.res.Configuration
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.LayoutInflater
|
import androidx.activity.compose.setContent
|
||||||
import android.view.View
|
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.core.view.*
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.systemBarsPadding
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import com.afollestad.materialdialogs.LayoutMode
|
import com.afollestad.materialdialogs.LayoutMode
|
||||||
import com.afollestad.materialdialogs.MaterialDialog
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||||
import com.afollestad.materialdialogs.customview.customView
|
import com.afollestad.materialdialogs.customview.customView
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
import de.mm20.launcher2.icons.DynamicIconController
|
import de.mm20.launcher2.icons.DynamicIconController
|
||||||
import de.mm20.launcher2.icons.IconRepository
|
|
||||||
import de.mm20.launcher2.ui.R
|
import de.mm20.launcher2.ui.R
|
||||||
import de.mm20.launcher2.ui.base.BaseActivity
|
import de.mm20.launcher2.ui.base.BaseActivity
|
||||||
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
import de.mm20.launcher2.ui.base.ProvideSettings
|
||||||
|
import de.mm20.launcher2.ui.component.NavBarEffects
|
||||||
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
||||||
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
|
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsSheet
|
||||||
|
import de.mm20.launcher2.ui.theme.LauncherTheme
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
|
|
||||||
@ -27,8 +38,6 @@ class LauncherActivity : BaseActivity() {
|
|||||||
|
|
||||||
private val viewModel: LauncherActivityVM by viewModels()
|
private val viewModel: LauncherActivityVM by viewModels()
|
||||||
|
|
||||||
private lateinit var binding: ActivityLauncherBinding
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -36,46 +45,49 @@ class LauncherActivity : BaseActivity() {
|
|||||||
|
|
||||||
viewModel.setDarkMode(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
viewModel.setDarkMode(resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
|
||||||
binding = ActivityLauncherBinding.inflate(LayoutInflater.from(this))
|
setContent {
|
||||||
setContentView(binding.root)
|
LauncherTheme {
|
||||||
|
ProvideSettings {
|
||||||
|
val lightStatus by viewModel.lightStatusBar.observeAsState(false)
|
||||||
|
val lightNav by viewModel.lightNavBar.observeAsState(false)
|
||||||
|
val hideStatus by viewModel.hideStatusBar.observeAsState(false)
|
||||||
|
val hideNav by viewModel.hideNavBar.observeAsState(false)
|
||||||
|
val dimBackground by viewModel.dimBackground.observeAsState(false)
|
||||||
|
|
||||||
viewModel.dimBackground.observe(this) { dim ->
|
val systemUiController = rememberSystemUiController()
|
||||||
window.attributes = window.attributes.also {
|
|
||||||
if (dim) {
|
LaunchedEffect(hideStatus) {
|
||||||
binding.rootView.setBackgroundColor(0x4C000000)
|
systemUiController.isStatusBarVisible = !hideStatus
|
||||||
} else {
|
}
|
||||||
binding.rootView.setBackgroundColor(0)
|
LaunchedEffect(hideNav) {
|
||||||
|
systemUiController.isNavigationBarVisible = !hideNav
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(if (dimBackground) Color.Black.copy(alpha = 0.30f) else Color.Transparent),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
NavBarEffects(modifier = Modifier.fillMaxSize())
|
||||||
|
PullDownScaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.systemBarsPadding(),
|
||||||
|
darkStatusBarIcons = lightStatus,
|
||||||
|
darkNavBarIcons = lightNav,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val showHiddenItems by viewModel.isHiddenItemsShown.observeAsState(false)
|
||||||
|
if (showHiddenItems) {
|
||||||
|
HiddenItemsSheet(onDismiss = {
|
||||||
|
viewModel.hideHiddenItems()
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.lightStatusBar.observe(this) {
|
|
||||||
val windowController = WindowCompat.getInsetsController(window, binding.rootView)
|
|
||||||
windowController.isAppearanceLightStatusBars = it
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.lightNavBar.observe(this) {
|
|
||||||
val windowController = WindowCompat.getInsetsController(window, binding.rootView)
|
|
||||||
windowController.isAppearanceLightNavigationBars = it
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.hideStatusBar.observe(this) {
|
|
||||||
val windowController = WindowCompat.getInsetsController(window, binding.rootView)
|
|
||||||
if (it) {
|
|
||||||
windowController.hide(WindowInsetsCompat.Type.statusBars())
|
|
||||||
} else {
|
|
||||||
windowController.show(WindowInsetsCompat.Type.statusBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.hideNavBar.observe(this) {
|
|
||||||
val windowController = WindowCompat.getInsetsController(window, binding.rootView)
|
|
||||||
if (it) {
|
|
||||||
windowController.hide(WindowInsetsCompat.Type.navigationBars())
|
|
||||||
} else {
|
|
||||||
windowController.show(WindowInsetsCompat.Type.navigationBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var editFavoritesDialog: MaterialDialog? = null
|
var editFavoritesDialog: MaterialDialog? = null
|
||||||
viewModel.isEditFavoritesShown.observe(this) {
|
viewModel.isEditFavoritesShown.observe(this) {
|
||||||
if (it) {
|
if (it) {
|
||||||
@ -99,23 +111,6 @@ class LauncherActivity : BaseActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var hiddenItemsView: HiddenItemsView? = null
|
|
||||||
viewModel.isHiddenItemsShown.observe(this) {
|
|
||||||
if (it) {
|
|
||||||
if (hiddenItemsView != null) return@observe
|
|
||||||
hiddenItemsView = HiddenItemsView(this).apply {
|
|
||||||
onDismiss = {
|
|
||||||
viewModel.hideHiddenItems()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.rootView.addView(hiddenItemsView)
|
|
||||||
} else {
|
|
||||||
if (hiddenItemsView == null) return@observe
|
|
||||||
binding.rootView.removeView(hiddenItemsView)
|
|
||||||
hiddenItemsView = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val dynamicIconController: DynamicIconController by inject()
|
val dynamicIconController: DynamicIconController by inject()
|
||||||
|
|
||||||
lifecycle.addObserver(dynamicIconController)
|
lifecycle.addObserver(dynamicIconController)
|
||||||
@ -123,18 +118,17 @@ class LauncherActivity : BaseActivity() {
|
|||||||
|
|
||||||
override fun onAttachedToWindow() {
|
override fun onAttachedToWindow() {
|
||||||
super.onAttachedToWindow()
|
super.onAttachedToWindow()
|
||||||
val windowController = WindowCompat.getInsetsController(window, binding.rootView)
|
val windowController = WindowCompat.getInsetsController(window, window.decorView.rootView)
|
||||||
windowController.systemBarsBehavior =
|
windowController.systemBarsBehavior =
|
||||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
binding.activityStartOverlay.visibility = View.INVISIBLE
|
|
||||||
|
|
||||||
binding.container.doOnNextLayout {
|
/*binding.container.doOnNextLayout {
|
||||||
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onNewIntent(intent: Intent?) {
|
override fun onNewIntent(intent: Intent?) {
|
||||||
|
|||||||
@ -2,63 +2,40 @@ package de.mm20.launcher2.ui.launcher
|
|||||||
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.asLiveData
|
import androidx.lifecycle.viewModelScope
|
||||||
import de.mm20.launcher2.ktx.isBrightColor
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
import de.mm20.launcher2.preferences.LauncherDataStore
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
import kotlinx.coroutines.flow.first
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import org.koin.core.component.KoinComponent
|
import org.koin.core.component.KoinComponent
|
||||||
import org.koin.core.component.inject
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
||||||
val isSearchOpen = MutableLiveData(false)
|
val isSearchOpen = MutableLiveData(false)
|
||||||
val blurBackground = MutableLiveData(false)
|
val isWidgetEditMode = MutableLiveData(false)
|
||||||
|
|
||||||
val statusBarColor = MutableLiveData(0)
|
val searchBarFocused = MutableLiveData(false)
|
||||||
val darkStatusBarIcons = MutableLiveData(false)
|
|
||||||
|
|
||||||
val searchBarLevel = MutableLiveData(SearchBarLevel.Resting)
|
|
||||||
|
|
||||||
val dataStore: LauncherDataStore by inject()
|
val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
val hideStatusBar = dataStore.data.map { it.systemBars.hideStatusBar }.asLiveData()
|
private val autoFocusSearch = dataStore.data.map { it.searchBar.autoFocus }
|
||||||
val hideNavBar = dataStore.data.map { it.systemBars.hideNavBar }.asLiveData()
|
|
||||||
|
|
||||||
val autoFocus = dataStore.data.map { it.searchBar.autoFocus }.asLiveData()
|
fun setSearchbarFocus(focused: Boolean) {
|
||||||
|
if (searchBarFocused.value != focused) searchBarFocused.value = focused
|
||||||
var scrollY = 0
|
}
|
||||||
set(value) {
|
|
||||||
if (value == 0 && field != 0) {
|
|
||||||
if (isSearchOpen.value == true) {
|
|
||||||
searchBarLevel.value = SearchBarLevel.Active
|
|
||||||
blurBackground.value = true
|
|
||||||
} else {
|
|
||||||
searchBarLevel.value = SearchBarLevel.Resting
|
|
||||||
blurBackground.value = false
|
|
||||||
}
|
|
||||||
} else if (value > 0 && field == 0) {
|
|
||||||
searchBarLevel.value = SearchBarLevel.Raised
|
|
||||||
blurBackground.value = true
|
|
||||||
}
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun openSearch() {
|
fun openSearch() {
|
||||||
if (isSearchOpen.value == true) return
|
if (isSearchOpen.value == true) return
|
||||||
isSearchOpen.value = true
|
isSearchOpen.value = true
|
||||||
if (scrollY == 0) {
|
viewModelScope.launch {
|
||||||
searchBarLevel.value = SearchBarLevel.Active
|
if (autoFocusSearch.first()) setSearchbarFocus(true)
|
||||||
blurBackground.value = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun closeSearch() {
|
fun closeSearch() {
|
||||||
if (isSearchOpen.value == false) return
|
if (isSearchOpen.value == false) return
|
||||||
isSearchOpen.value = false
|
isSearchOpen.value = false
|
||||||
if (scrollY == 0) {
|
setSearchbarFocus(false)
|
||||||
searchBarLevel.value = SearchBarLevel.Resting
|
|
||||||
blurBackground.value = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun toggleSearch() {
|
fun toggleSearch() {
|
||||||
@ -66,8 +43,8 @@ class LauncherScaffoldVM : ViewModel(), KoinComponent {
|
|||||||
else openSearch()
|
else openSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setStatusBarColor(color: Int) {
|
fun setWidgetEditMode(editMode: Boolean) {
|
||||||
statusBarColor.value = color
|
isSearchOpen.value = false
|
||||||
darkStatusBarIcons.value = color.isBrightColor()
|
isWidgetEditMode.value = editMode
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,351 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher
|
|
||||||
|
|
||||||
import android.animation.AnimatorSet
|
|
||||||
import android.animation.ObjectAnimator
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.util.Log
|
|
||||||
import android.util.TypedValue
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.MotionEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowManager
|
|
||||||
import android.view.inputmethod.InputMethodManager
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.activity.addCallback
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.animation.doOnEnd
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.content.getSystemService
|
|
||||||
import androidx.core.view.*
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import de.mm20.launcher2.ktx.dp
|
|
||||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
|
||||||
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.databinding.ViewLauncherScaffoldBinding
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchBarVM
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetsVM
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
class LauncherScaffoldView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs) {
|
|
||||||
|
|
||||||
private val binding = ViewLauncherScaffoldBinding.inflate(LayoutInflater.from(context), this)
|
|
||||||
|
|
||||||
private val viewModel: LauncherScaffoldVM by (context as AppCompatActivity).viewModels()
|
|
||||||
private val widgetsViewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
|
|
||||||
private val searchViewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
|
||||||
private val searchBarViewModel: SearchBarVM by (context as AppCompatActivity).viewModels()
|
|
||||||
|
|
||||||
private var autoFocus = false
|
|
||||||
|
|
||||||
private val scrollViewOnTouchListener = object : OnTouchListener {
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
|
||||||
override fun onTouch(v: View, event: MotionEvent): Boolean {
|
|
||||||
when (event.action) {
|
|
||||||
MotionEvent.ACTION_DOWN -> return true
|
|
||||||
MotionEvent.ACTION_MOVE -> {
|
|
||||||
when {
|
|
||||||
binding.scrollView.scrollY == 0 -> {
|
|
||||||
if (event.historySize > 0) {
|
|
||||||
val dY = event.y - event.getHistoricalY(0)
|
|
||||||
val newTransY = 0.4f * dY + translationY
|
|
||||||
if (newTransY > 0 && newTransY < 48 * dp) {
|
|
||||||
translationY = newTransY
|
|
||||||
} else if (newTransY <= 0) {
|
|
||||||
translationY = 0f
|
|
||||||
} else {
|
|
||||||
translationY = 48 * dp
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (translationY == 0f) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.scrollView.scrollY == binding.scrollContainer.height - binding.scrollView.height && viewModel.isSearchOpen.value == true -> {
|
|
||||||
if (event.historySize > 0) {
|
|
||||||
val dY = event.y - event.getHistoricalY(0)
|
|
||||||
val newTransY = 0.4f * dY + translationY
|
|
||||||
|
|
||||||
if (newTransY <= 0 && newTransY > -48 * dp) {
|
|
||||||
translationY = newTransY
|
|
||||||
} else if (newTransY > 0) {
|
|
||||||
translationY = 0f
|
|
||||||
} else {
|
|
||||||
translationY = -48 * dp
|
|
||||||
}
|
|
||||||
|
|
||||||
if (translationY == 0f) return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
MotionEvent.ACTION_UP -> {
|
|
||||||
if (translationY >= 48 * dp * 0.6) viewModel.toggleSearch()
|
|
||||||
if (translationY <= -48 * dp) viewModel.closeSearch()
|
|
||||||
animate().translationY(0f).setDuration(200).start()
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
else -> return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
context as AppCompatActivity
|
|
||||||
|
|
||||||
context.onBackPressedDispatcher.addCallback {
|
|
||||||
viewModel.closeSearch()
|
|
||||||
widgetsViewModel.setEditMode(false)
|
|
||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
|
||||||
}
|
|
||||||
|
|
||||||
context.lifecycleScope.launch {
|
|
||||||
context.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
|
||||||
if (viewModel.isSearchOpen.value == true && autoFocus) {
|
|
||||||
searchBarViewModel.setFocused(true)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
awaitCancellation()
|
|
||||||
} finally {
|
|
||||||
searchBarViewModel.setFocused(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.scrollView.scrollY = viewModel.scrollY
|
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
|
||||||
|
|
||||||
binding.scrollView.setOnScrollChangeListener { _, _, scrollY, _, oldScrollY: Int ->
|
|
||||||
viewModel.scrollY = scrollY
|
|
||||||
when {
|
|
||||||
/* Hide searchbar*/
|
|
||||||
scrollY > oldScrollY && ((scrollY > 48 * dp)) -> {
|
|
||||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
|
||||||
if (newTransY < -112 * dp) {
|
|
||||||
newTransY = -112 * dp
|
|
||||||
}
|
|
||||||
binding.searchBar.translationY = newTransY
|
|
||||||
}
|
|
||||||
/* Show searchbar*/
|
|
||||||
scrollY < oldScrollY -> {
|
|
||||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
|
||||||
if (newTransY > 0f) {
|
|
||||||
newTransY = 0f
|
|
||||||
}
|
|
||||||
binding.searchBar.translationY = newTransY
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.isSearchOpen.observe(context) {
|
|
||||||
if (it) showSearch()
|
|
||||||
else hideSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.searchBarLevel.observe(context) {
|
|
||||||
binding.searchBar.level = it
|
|
||||||
}
|
|
||||||
|
|
||||||
searchViewModel.websearchResults.observe(context) {
|
|
||||||
binding.searchContainer.setPadding(
|
|
||||||
0,
|
|
||||||
(if (it.isEmpty()) 48 * dp else 96 * dp).toInt(),
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.autoFocus.observe(context) {
|
|
||||||
autoFocus = it == true
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetsViewModel.isEditMode.observe(context) {
|
|
||||||
//OneShotLayoutTransition.run(binding.scrollContainer)
|
|
||||||
if (it) {
|
|
||||||
binding.scrollView.setOnTouchListener(null)
|
|
||||||
binding.searchBar.visibility = View.INVISIBLE
|
|
||||||
binding.editWidgetToolbar
|
|
||||||
.animate()
|
|
||||||
.translationY(0f)
|
|
||||||
.alpha(1f)
|
|
||||||
.withStartAction {
|
|
||||||
binding.editWidgetToolbar.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
.start()
|
|
||||||
binding.widgetContainer.setPadding(0, (56 * dp).toInt(), 0, 0)
|
|
||||||
val colorSurface = TypedValue()
|
|
||||||
context.theme.resolveAttribute(R.attr.colorSurface, colorSurface, true)
|
|
||||||
context.window.statusBarColor = colorSurface.data
|
|
||||||
viewModel.setStatusBarColor(colorSurface.data)
|
|
||||||
} else {
|
|
||||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
|
||||||
|
|
||||||
binding.searchBar.visibility = View.VISIBLE
|
|
||||||
binding.editWidgetToolbar
|
|
||||||
.animate()
|
|
||||||
.translationY(-binding.editWidgetToolbar.height.toFloat())
|
|
||||||
.alpha(0f)
|
|
||||||
.withEndAction {
|
|
||||||
binding.editWidgetToolbar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
.start()
|
|
||||||
binding.widgetContainer.setPadding(0)
|
|
||||||
viewModel.setStatusBarColor(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.editWidgetToolbar.apply {
|
|
||||||
navigationIcon =
|
|
||||||
ContextCompat.getDrawable(context, R.drawable.ic_done)?.apply {
|
|
||||||
setTint(ContextCompat.getColor(context, R.color.icon_color))
|
|
||||||
}
|
|
||||||
setNavigationOnClickListener {
|
|
||||||
widgetsViewModel.setEditMode(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
searchBarViewModel.focused.observe(context) {
|
|
||||||
if (it) {
|
|
||||||
viewModel.openSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.blurBackground.observe(context) { blur ->
|
|
||||||
if (!isAtLeastApiLevel(31)) return@observe
|
|
||||||
context.window.attributes = context.window.attributes.also {
|
|
||||||
if (blur) {
|
|
||||||
it.blurBehindRadius = (32 * dp).toInt()
|
|
||||||
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
|
||||||
} else {
|
|
||||||
it.blurBehindRadius = 0
|
|
||||||
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.statusBarColor.observe(context) {
|
|
||||||
context.window.statusBarColor = it
|
|
||||||
}
|
|
||||||
viewModel.darkStatusBarIcons.observe(context) {
|
|
||||||
WindowCompat.getInsetsController(context.window, this).isAppearanceLightStatusBars = it
|
|
||||||
}
|
|
||||||
|
|
||||||
viewModel.hideNavBar.observe(context) {
|
|
||||||
updateInsetPaddings()
|
|
||||||
}
|
|
||||||
viewModel.hideStatusBar.observe(context) {
|
|
||||||
updateInsetPaddings()
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewCompat.setOnApplyWindowInsetsListener(this) { view, insets ->
|
|
||||||
updateInsetPaddings()
|
|
||||||
insets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAttachedToWindow() {
|
|
||||||
super.onAttachedToWindow()
|
|
||||||
updateInsetPaddings()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateInsetPaddings() {
|
|
||||||
val windowInsets = ViewCompat.getRootWindowInsets(this) ?: return
|
|
||||||
val hideStatusBar = viewModel.hideStatusBar.value == true
|
|
||||||
val hideNavBar = viewModel.hideNavBar.value == true
|
|
||||||
|
|
||||||
var topPadding = 0
|
|
||||||
var leftPadding = 0
|
|
||||||
var rightPadding = 0
|
|
||||||
var bottomPadding = 0
|
|
||||||
|
|
||||||
if (!hideStatusBar) {
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.statusBars())
|
|
||||||
topPadding += insets.top
|
|
||||||
leftPadding += insets.left
|
|
||||||
rightPadding += insets.right
|
|
||||||
bottomPadding += insets.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!hideNavBar) {
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
|
||||||
topPadding += insets.top
|
|
||||||
leftPadding += insets.left
|
|
||||||
rightPadding += insets.right
|
|
||||||
bottomPadding += insets.bottom
|
|
||||||
}
|
|
||||||
|
|
||||||
setPadding(leftPadding, topPadding, rightPadding, bottomPadding)
|
|
||||||
binding.widgetContainer.setClockWidgetHeight(height - paddingTop - paddingBottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
|
|
||||||
if (changed) {
|
|
||||||
binding.widgetContainer.setClockWidgetHeight(bottom - top - paddingTop - paddingBottom)
|
|
||||||
}
|
|
||||||
super.onLayout(changed, left, top, right, bottom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hideSearch() {
|
|
||||||
val set = AnimatorSet()
|
|
||||||
set.duration = 300
|
|
||||||
set.doOnEnd {
|
|
||||||
binding.searchContainer.visibility = View.GONE
|
|
||||||
binding.widgetContainer.animate().alpha(1f).setDuration(500).start()
|
|
||||||
binding.widgetContainer.visibility = View.VISIBLE
|
|
||||||
}
|
|
||||||
set.playTogether(
|
|
||||||
ObjectAnimator.ofFloat(binding.widgetContainer, "translationY", 0f),
|
|
||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.searchContainer, "translationY", 0f,
|
|
||||||
if (binding.scrollView.scrollY > binding.searchContainer.height / 2f) -binding.searchContainer.height.toFloat() else binding.scrollView.height.toFloat()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set.doOnEnd {
|
|
||||||
searchViewModel.search("")
|
|
||||||
}
|
|
||||||
set.start()
|
|
||||||
binding.scrollView.scrollTo(0, 0)
|
|
||||||
context.getSystemService<InputMethodManager>()?.hideSoftInputFromWindow(
|
|
||||||
binding.searchBar.windowToken,
|
|
||||||
0
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showSearch() {
|
|
||||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
|
||||||
binding.searchContainer.visibility = View.VISIBLE
|
|
||||||
binding.widgetContainer.animate().alpha(0f).setDuration(500).start()
|
|
||||||
binding.widgetContainer.visibility = View.GONE
|
|
||||||
val set = AnimatorSet()
|
|
||||||
set.duration = 300
|
|
||||||
set.playTogether(
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.widgetContainer,
|
|
||||||
"translationY",
|
|
||||||
binding.scrollView.height.toFloat()
|
|
||||||
),
|
|
||||||
ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0),
|
|
||||||
ObjectAnimator.ofFloat(
|
|
||||||
binding.searchContainer,
|
|
||||||
"translationY",
|
|
||||||
binding.scrollView.height.toFloat(),
|
|
||||||
0f
|
|
||||||
)
|
|
||||||
)
|
|
||||||
set.start()
|
|
||||||
if (autoFocus) searchBarViewModel.setFocused(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,300 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.slideIn
|
||||||
|
import androidx.compose.animation.slideOut
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Done
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clipToBounds
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.TransformOrigin
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollDispatcher
|
||||||
|
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.onSizeChanged
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.*
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||||
|
import com.google.accompanist.pager.VerticalPager
|
||||||
|
import com.google.accompanist.pager.calculateCurrentOffsetForPage
|
||||||
|
import com.google.accompanist.pager.rememberPagerState
|
||||||
|
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||||
|
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.ktx.toDp
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchColumn
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||||
|
import de.mm20.launcher2.ui.launcher.widgets.WidgetColumn
|
||||||
|
import de.mm20.launcher2.ui.modifier.scale
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.math.absoluteValue
|
||||||
|
import kotlin.ranges.coerceIn
|
||||||
|
|
||||||
|
@OptIn(ExperimentalPagerApi::class)
|
||||||
|
@Composable
|
||||||
|
fun PullDownScaffold(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
darkStatusBarIcons: Boolean = false,
|
||||||
|
darkNavBarIcons: Boolean = false,
|
||||||
|
) {
|
||||||
|
val viewModel: LauncherScaffoldVM = viewModel()
|
||||||
|
val searchVM: SearchVM = viewModel()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val isSearchOpen by viewModel.isSearchOpen.observeAsState(false)
|
||||||
|
val isWidgetEditMode by viewModel.isWidgetEditMode.observeAsState(false)
|
||||||
|
|
||||||
|
val pagerState = rememberPagerState()
|
||||||
|
val widgetsScrollState = rememberScrollState()
|
||||||
|
val searchScrollState = rememberScrollState()
|
||||||
|
|
||||||
|
val systemUiController = rememberSystemUiController()
|
||||||
|
|
||||||
|
val colorSurface = MaterialTheme.colorScheme.surface
|
||||||
|
LaunchedEffect(isWidgetEditMode, darkStatusBarIcons, colorSurface) {
|
||||||
|
if (isWidgetEditMode) {
|
||||||
|
systemUiController.setStatusBarColor(
|
||||||
|
colorSurface
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
systemUiController.setStatusBarColor(
|
||||||
|
Color.Transparent,
|
||||||
|
darkIcons = darkStatusBarIcons
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(darkNavBarIcons) {
|
||||||
|
systemUiController.setNavigationBarColor(
|
||||||
|
Color.Transparent,
|
||||||
|
darkIcons = darkNavBarIcons,
|
||||||
|
navigationBarContrastEnforced = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val dp = LocalDensity.current.density
|
||||||
|
|
||||||
|
val offsetY = remember { Animatable(0.dp, Dp.VectorConverter) }
|
||||||
|
var searchBarOffset by remember { mutableStateOf(0.dp) }
|
||||||
|
|
||||||
|
val blurWallpaper = isSearchOpen || offsetY.value > 48.dp || widgetsScrollState.value > 0
|
||||||
|
|
||||||
|
LaunchedEffect(blurWallpaper) {
|
||||||
|
if (!isAtLeastApiLevel(31)) return@LaunchedEffect
|
||||||
|
(context as Activity).window.attributes = context.window.attributes.also {
|
||||||
|
if (blurWallpaper) {
|
||||||
|
it.blurBehindRadius = (32 * dp).toInt()
|
||||||
|
it.flags = it.flags or WindowManager.LayoutParams.FLAG_BLUR_BEHIND
|
||||||
|
} else {
|
||||||
|
it.blurBehindRadius = 0
|
||||||
|
it.flags = it.flags and WindowManager.LayoutParams.FLAG_BLUR_BEHIND.inv()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
val nestedScrollDispatcher = remember { NestedScrollDispatcher() }
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val nestedScrollConnection = remember {
|
||||||
|
object : NestedScrollConnection {
|
||||||
|
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
|
||||||
|
val consumed = when {
|
||||||
|
isSearchOpen && (offsetY.value > 0.dp || source == NestedScrollSource.Drag && searchScrollState.value - available.y < 0) -> {
|
||||||
|
val consumed = available.y - searchScrollState.value
|
||||||
|
scope.launch {
|
||||||
|
offsetY.snapTo((offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(0.dp..64.dp))
|
||||||
|
}
|
||||||
|
consumed
|
||||||
|
}
|
||||||
|
isSearchOpen && (offsetY.value < 0.dp || source == NestedScrollSource.Drag && searchScrollState.value - available.y > searchScrollState.maxValue) -> {
|
||||||
|
val consumed =
|
||||||
|
available.y - (searchScrollState.maxValue - searchScrollState.value)
|
||||||
|
scope.launch {
|
||||||
|
offsetY.snapTo((offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(-64.dp..0.dp))
|
||||||
|
}
|
||||||
|
consumed
|
||||||
|
}
|
||||||
|
!isSearchOpen && (offsetY.value > 0.dp || source == NestedScrollSource.Drag && widgetsScrollState.value - available.y < 0) -> {
|
||||||
|
val consumed = available.y - widgetsScrollState.value
|
||||||
|
scope.launch {
|
||||||
|
offsetY.snapTo((offsetY.value + (consumed * 0.5f / dp).dp).coerceIn(0.dp..64.dp))
|
||||||
|
}
|
||||||
|
consumed
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
0f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBarOffset =
|
||||||
|
((searchBarOffset) + ((available.y - consumed) / dp).dp).coerceIn(-128.dp, 0.dp)
|
||||||
|
|
||||||
|
return Offset(0f, consumed)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun onPreFling(available: Velocity): Velocity {
|
||||||
|
if (offsetY.value > 48.dp || offsetY.value < -48.dp) {
|
||||||
|
viewModel.toggleSearch()
|
||||||
|
}
|
||||||
|
if (offsetY.value != 0.dp) {
|
||||||
|
offsetY.animateTo(0.dp)
|
||||||
|
return available
|
||||||
|
}
|
||||||
|
return Velocity.Zero
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isSearchOpen) {
|
||||||
|
searchScrollState.scrollTo(0)
|
||||||
|
if (!isSearchOpen) searchVM.search("")
|
||||||
|
searchBarOffset = 0.dp
|
||||||
|
pagerState.animateScrollToPage(if (isSearchOpen) 1 else 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isWidgetEditMode) {
|
||||||
|
if (!isWidgetEditMode) searchBarOffset = 0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
BackHandler {
|
||||||
|
when {
|
||||||
|
isSearchOpen -> {
|
||||||
|
viewModel.closeSearch()
|
||||||
|
searchVM.search("")
|
||||||
|
}
|
||||||
|
isWidgetEditMode -> {
|
||||||
|
viewModel.setWidgetEditMode(false)
|
||||||
|
}
|
||||||
|
widgetsScrollState.value != 0 -> {
|
||||||
|
scope.launch {
|
||||||
|
widgetsScrollState.animateScrollTo(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var size by remember { mutableStateOf(IntSize.Zero) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clipToBounds()
|
||||||
|
.onSizeChanged {
|
||||||
|
size = it
|
||||||
|
}
|
||||||
|
.offset(y = offsetY.value),
|
||||||
|
contentAlignment = Alignment.TopCenter
|
||||||
|
) {
|
||||||
|
VerticalPager(
|
||||||
|
state = pagerState,
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(nestedScrollConnection, nestedScrollDispatcher),
|
||||||
|
count = 2,
|
||||||
|
reverseLayout = true,
|
||||||
|
userScrollEnabled = false,
|
||||||
|
) {
|
||||||
|
if (it == 1) {
|
||||||
|
val websearches by searchVM.websearchResults.observeAsState(emptyList())
|
||||||
|
val webSearchPadding by animateDpAsState(
|
||||||
|
if (websearches.isEmpty()) 0.dp else 48.dp
|
||||||
|
)
|
||||||
|
val offset = calculateCurrentOffsetForPage(1).absoluteValue
|
||||||
|
SearchColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(1 - offset)
|
||||||
|
.scale(1 - offset, TransformOrigin.Center)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(searchScrollState)
|
||||||
|
.padding(8.dp)
|
||||||
|
.padding(top = 56.dp)
|
||||||
|
.padding(top = webSearchPadding)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (it == 0) {
|
||||||
|
val offset = calculateCurrentOffsetForPage(0).absoluteValue
|
||||||
|
val editModePadding by animateDpAsState(if (isWidgetEditMode) 56.dp else 0.dp)
|
||||||
|
WidgetColumn(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.alpha(1 - offset)
|
||||||
|
.scale(1 - offset, TransformOrigin.Center)
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(widgetsScrollState)
|
||||||
|
.padding(8.dp)
|
||||||
|
.padding(top = editModePadding),
|
||||||
|
clockHeight = size.height.toDp(),
|
||||||
|
editMode = isWidgetEditMode,
|
||||||
|
onEditModeChange = {
|
||||||
|
viewModel.setWidgetEditMode(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchBarLevel = when {
|
||||||
|
offsetY.value != 0.dp -> SearchBarLevel.Raised
|
||||||
|
isSearchOpen && searchScrollState.value == 0 -> SearchBarLevel.Active
|
||||||
|
isSearchOpen && searchScrollState.value > 0 -> SearchBarLevel.Raised
|
||||||
|
widgetsScrollState.value > 0 -> SearchBarLevel.Raised
|
||||||
|
else -> SearchBarLevel.Resting
|
||||||
|
}
|
||||||
|
val searchBarFocused by viewModel.searchBarFocused.observeAsState(false)
|
||||||
|
|
||||||
|
|
||||||
|
val editModeSearchBarOffset by animateDpAsState(
|
||||||
|
if (isWidgetEditMode) -128.dp else 0.dp
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = isWidgetEditMode,
|
||||||
|
enter = slideIn { IntOffset(0, -it.height) },
|
||||||
|
exit = slideOut { IntOffset(0, -it.height) }
|
||||||
|
) {
|
||||||
|
CenterAlignedTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.menu_edit_widgets))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = { viewModel.setWidgetEditMode(false) }) {
|
||||||
|
Icon(imageVector = Icons.Rounded.Done, contentDescription = null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SearchBar(
|
||||||
|
level = searchBarLevel,
|
||||||
|
modifier = Modifier.offset(y = searchBarOffset + editModeSearchBarOffset),
|
||||||
|
focused = searchBarFocused,
|
||||||
|
onFocusChange = {
|
||||||
|
if (it) viewModel.openSearch()
|
||||||
|
viewModel.setSearchbarFocus(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,116 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.modals
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
|
import androidx.compose.animation.slideIn
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.FractionalThreshold
|
||||||
|
import androidx.compose.material.rememberSwipeableState
|
||||||
|
import androidx.compose.material.swipeable
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.ktx.toPixels
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class, ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun HiddenItemsSheet(
|
||||||
|
onDismiss: () -> Unit
|
||||||
|
) {
|
||||||
|
val viewModel: HiddenItemsVM = viewModel()
|
||||||
|
|
||||||
|
Dialog(
|
||||||
|
properties = DialogProperties(usePlatformDefaultWidth = false),
|
||||||
|
onDismissRequest = { onDismiss() }) {
|
||||||
|
val animationState = remember {
|
||||||
|
MutableTransitionState(false).apply {
|
||||||
|
targetState = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(
|
||||||
|
animationState,
|
||||||
|
enter = slideIn { IntOffset(0, it.height) }
|
||||||
|
) {
|
||||||
|
val swipeState =
|
||||||
|
rememberSwipeableState(initialValue = SwipeState.Default) {
|
||||||
|
if (it == SwipeState.Dismiss) onDismiss()
|
||||||
|
return@rememberSwipeableState true
|
||||||
|
}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.swipeable(
|
||||||
|
swipeState,
|
||||||
|
mapOf(
|
||||||
|
0f to SwipeState.Default,
|
||||||
|
600.dp.toPixels() to SwipeState.Dismiss
|
||||||
|
),
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
||||||
|
)
|
||||||
|
.offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
|
||||||
|
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.menu_hidden_items),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(24.dp)
|
||||||
|
|
||||||
|
)
|
||||||
|
val items by viewModel.hiddenItems.collectAsState(emptyList())
|
||||||
|
SearchResultGrid(
|
||||||
|
items,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(8.dp)
|
||||||
|
.verticalScroll(rememberScrollState())
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
contentAlignment = Alignment.CenterEnd
|
||||||
|
) {
|
||||||
|
TextButton(onClick = { onDismiss() }) {
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.close),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class SwipeState {
|
||||||
|
Default, Dismiss
|
||||||
|
}
|
||||||
@ -1,133 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.modals
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.MutableTransitionState
|
|
||||||
import androidx.compose.animation.slideIn
|
|
||||||
import androidx.compose.foundation.gestures.Orientation
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.rememberScrollState
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
|
||||||
import androidx.compose.material.FractionalThreshold
|
|
||||||
import androidx.compose.material.rememberSwipeableState
|
|
||||||
import androidx.compose.material.swipeable
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.ExperimentalComposeUiApi
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
|
||||||
import de.mm20.launcher2.ui.ktx.toPixels
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.common.grid.SearchResultGrid
|
|
||||||
import kotlin.math.roundToInt
|
|
||||||
|
|
||||||
@OptIn(ExperimentalComposeUiApi::class, androidx.compose.material.ExperimentalMaterialApi::class)
|
|
||||||
class HiddenItemsView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs) {
|
|
||||||
private val viewModel: HiddenItemsVM by (context as AppCompatActivity).viewModels()
|
|
||||||
|
|
||||||
init {
|
|
||||||
val composeView = ComposeView(context)
|
|
||||||
|
|
||||||
composeView.setContent {
|
|
||||||
LauncherTheme {
|
|
||||||
ProvideSettings {
|
|
||||||
Dialog(
|
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
|
||||||
onDismissRequest = { onDismiss() }) {
|
|
||||||
val animationState = remember {
|
|
||||||
MutableTransitionState(false).apply {
|
|
||||||
targetState = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AnimatedVisibility(
|
|
||||||
animationState,
|
|
||||||
enter = slideIn { IntOffset(0, it.height) }
|
|
||||||
) {
|
|
||||||
val swipeState =
|
|
||||||
rememberSwipeableState(initialValue = SwipeState.Default) {
|
|
||||||
if (it == SwipeState.Dismiss) onDismiss()
|
|
||||||
return@rememberSwipeableState true
|
|
||||||
}
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.swipeable(
|
|
||||||
swipeState,
|
|
||||||
mapOf(
|
|
||||||
0f to SwipeState.Default,
|
|
||||||
600.dp.toPixels() to SwipeState.Dismiss
|
|
||||||
),
|
|
||||||
orientation = Orientation.Vertical,
|
|
||||||
thresholds = { _, _ -> FractionalThreshold(0.5f) },
|
|
||||||
)
|
|
||||||
.offset { IntOffset(0, swipeState.offset.value.roundToInt()) }
|
|
||||||
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.menu_hidden_items),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(24.dp)
|
|
||||||
|
|
||||||
)
|
|
||||||
val items by viewModel.hiddenItems.collectAsState(emptyList())
|
|
||||||
SearchResultGrid(
|
|
||||||
items,
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(8.dp)
|
|
||||||
.verticalScroll(rememberScrollState())
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(12.dp),
|
|
||||||
contentAlignment = Alignment.CenterEnd
|
|
||||||
) {
|
|
||||||
TextButton(onClick = { onDismiss() }) {
|
|
||||||
Text(
|
|
||||||
stringResource(id = R.string.close),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
addView(composeView)
|
|
||||||
}
|
|
||||||
|
|
||||||
var onDismiss: () -> Unit = {}
|
|
||||||
}
|
|
||||||
|
|
||||||
private enum class SwipeState {
|
|
||||||
Default, Dismiss
|
|
||||||
}
|
|
||||||
@ -55,34 +55,43 @@ import java.io.File
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(
|
||||||
level: SearchBarLevel
|
modifier: Modifier = Modifier,
|
||||||
|
level: SearchBarLevel,
|
||||||
|
focused: Boolean,
|
||||||
|
onFocusChange: (Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
val searchViewModel: SearchVM = viewModel()
|
val searchViewModel: SearchVM = viewModel()
|
||||||
val activityViewModel: LauncherActivityVM = viewModel()
|
val activityViewModel: LauncherActivityVM = viewModel()
|
||||||
val viewModel: SearchBarVM = viewModel()
|
|
||||||
|
|
||||||
val dataStore: LauncherDataStore by inject()
|
val dataStore: LauncherDataStore by inject()
|
||||||
|
|
||||||
val style by remember { dataStore.data.map { it.searchBar.searchBarStyle } }
|
val style by remember { dataStore.data.map { it.searchBar.searchBarStyle } }
|
||||||
.collectAsState(SearchBarSettings.SearchBarStyle.Hidden)
|
.collectAsState(SearchBarSettings.SearchBarStyle.Hidden)
|
||||||
|
|
||||||
val focused by viewModel.focused.observeAsState(false)
|
|
||||||
|
|
||||||
val focusManager = LocalFocusManager.current
|
val focusManager = LocalFocusManager.current
|
||||||
val focusRequester = remember { FocusRequester() }
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
LaunchedEffect(focused) {
|
|
||||||
if (focused) focusRequester.requestFocus()
|
|
||||||
else focusManager.clearFocus()
|
|
||||||
}
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
|
||||||
|
LaunchedEffect(focused) {
|
||||||
|
val f = focused
|
||||||
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.RESUMED) {
|
||||||
|
try {
|
||||||
|
if (f) focusRequester.requestFocus()
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
focusManager.clearFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val query by searchViewModel.searchQuery.observeAsState("")
|
val query by searchViewModel.searchQuery.observeAsState("")
|
||||||
|
|
||||||
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
||||||
|
|
||||||
SearchBar(
|
SearchBar(
|
||||||
|
modifier,
|
||||||
level,
|
level,
|
||||||
websearches,
|
websearches,
|
||||||
value = query,
|
value = query,
|
||||||
@ -137,10 +146,10 @@ fun SearchBar(
|
|||||||
},
|
},
|
||||||
focusRequester = focusRequester,
|
focusRequester = focusRequester,
|
||||||
onFocus = {
|
onFocus = {
|
||||||
viewModel.setFocused(true)
|
onFocusChange(true)
|
||||||
},
|
},
|
||||||
onUnfocus = {
|
onUnfocus = {
|
||||||
viewModel.setFocused(false)
|
onFocusChange(false)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -148,6 +157,7 @@ fun SearchBar(
|
|||||||
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SearchBar(
|
fun SearchBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
level: SearchBarLevel,
|
level: SearchBarLevel,
|
||||||
websearches: List<Websearch>,
|
websearches: List<Websearch>,
|
||||||
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
||||||
@ -230,7 +240,7 @@ fun SearchBar(
|
|||||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||||
|
|
||||||
LauncherCard(
|
LauncherCard(
|
||||||
modifier = Modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.wrapContentHeight()
|
.wrapContentHeight()
|
||||||
.alpha(opacity)
|
.alpha(opacity)
|
||||||
@ -259,7 +269,6 @@ fun SearchBar(
|
|||||||
color = contentColor
|
color = contentColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val focusManager = LocalFocusManager.current
|
|
||||||
LaunchedEffect(level) {
|
LaunchedEffect(level) {
|
||||||
if (level == SearchBarLevel.Resting) onUnfocus()
|
if (level == SearchBarLevel.Resting) onUnfocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import de.mm20.launcher2.preferences.LauncherDataStore
|
|
||||||
import de.mm20.launcher2.preferences.Settings
|
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.locals.LocalCardStyle
|
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
import org.koin.core.component.inject
|
|
||||||
|
|
||||||
class SearchBarView @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null,
|
|
||||||
defStyleAttr: Int = 0
|
|
||||||
) : FrameLayout(context, attrs, defStyleAttr), KoinComponent {
|
|
||||||
|
|
||||||
var level: SearchBarLevel = SearchBarLevel.Resting
|
|
||||||
set(value) {
|
|
||||||
levelState.value = value
|
|
||||||
field = value
|
|
||||||
}
|
|
||||||
|
|
||||||
private val dataStore: LauncherDataStore by inject()
|
|
||||||
private val levelState = MutableLiveData(level)
|
|
||||||
|
|
||||||
init {
|
|
||||||
val view = ComposeView(context)
|
|
||||||
view.setContent {
|
|
||||||
val level by levelState.observeAsState(SearchBarLevel.Resting)
|
|
||||||
val cardStyle by remember {
|
|
||||||
dataStore.data.map { it.cards }.distinctUntilChanged()
|
|
||||||
}.collectAsState(
|
|
||||||
Settings.CardSettings.getDefaultInstance()
|
|
||||||
)
|
|
||||||
CompositionLocalProvider(
|
|
||||||
LocalCardStyle provides cardStyle
|
|
||||||
) {
|
|
||||||
LauncherTheme {
|
|
||||||
Box(contentAlignment = Alignment.TopCenter) {
|
|
||||||
SearchBar(
|
|
||||||
level
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addView(view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.search
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.apps.AppResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.appshortcuts.AppShortcutResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.favorites.FavoritesResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.files.FileResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.website.WebsiteResults
|
||||||
|
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaResults
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SearchColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
FavoritesResults()
|
||||||
|
AppResults()
|
||||||
|
AppShortcutResults()
|
||||||
|
UnitConverterResults()
|
||||||
|
CalculatorResults()
|
||||||
|
CalendarResults()
|
||||||
|
ContactResults()
|
||||||
|
WikipediaResults()
|
||||||
|
WebsiteResults()
|
||||||
|
FileResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,62 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.search
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.apps.AppResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.appshortcuts.AppShortcutResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.calculator.CalculatorResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.calendar.CalendarResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.contacts.ContactResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.favorites.FavoritesResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.files.FileResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.unitconverter.UnitConverterResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.website.WebsiteResults
|
|
||||||
import de.mm20.launcher2.ui.launcher.search.wikipedia.WikipediaResults
|
|
||||||
import org.koin.core.component.KoinComponent
|
|
||||||
|
|
||||||
class SearchView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs), KoinComponent {
|
|
||||||
|
|
||||||
init {
|
|
||||||
val view = ComposeView(context)
|
|
||||||
view.layoutParams = LayoutParams(
|
|
||||||
LayoutParams.MATCH_PARENT,
|
|
||||||
LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
view.setContent {
|
|
||||||
LauncherTheme {
|
|
||||||
ProvideSettings {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
FavoritesResults()
|
|
||||||
AppResults()
|
|
||||||
AppShortcutResults()
|
|
||||||
UnitConverterResults()
|
|
||||||
CalculatorResults()
|
|
||||||
CalendarResults()
|
|
||||||
ContactResults()
|
|
||||||
WikipediaResults()
|
|
||||||
WebsiteResults()
|
|
||||||
FileResults()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addView(view)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -0,0 +1,306 @@
|
|||||||
|
package de.mm20.launcher2.ui.launcher.widgets
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.appwidget.AppWidgetHost
|
||||||
|
import android.appwidget.AppWidgetManager
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
|
||||||
|
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||||
|
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||||
|
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.rounded.Add
|
||||||
|
import androidx.compose.material3.*
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.onPlaced
|
||||||
|
import androidx.compose.ui.layout.positionInParent
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalLifecycleOwner
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import de.mm20.launcher2.ui.ClockWidget
|
||||||
|
import de.mm20.launcher2.ui.R
|
||||||
|
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
|
||||||
|
import de.mm20.launcher2.widgets.ExternalWidget
|
||||||
|
import kotlinx.coroutines.awaitCancellation
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
||||||
|
@Composable
|
||||||
|
fun WidgetColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
clockHeight: Dp = 0.dp,
|
||||||
|
editMode: Boolean = false,
|
||||||
|
onEditModeChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val viewModel: WidgetsVM = viewModel()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val lifecycleOwner = LocalLifecycleOwner.current
|
||||||
|
val widgetHost = remember { AppWidgetHost(context.applicationContext, 44203) }
|
||||||
|
|
||||||
|
LaunchedEffect(null) {
|
||||||
|
lifecycleOwner.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||||
|
widgetHost.startListening()
|
||||||
|
try {
|
||||||
|
awaitCancellation()
|
||||||
|
} finally {
|
||||||
|
widgetHost.stopListening()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val pickWidgetLauncher = rememberLauncherForActivityResult(contract = ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
val data = it.data ?: return@rememberLauncherForActivityResult
|
||||||
|
val widgetId = data.getIntExtra(
|
||||||
|
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||||
|
AppWidgetManager.INVALID_APPWIDGET_ID
|
||||||
|
)
|
||||||
|
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@rememberLauncherForActivityResult
|
||||||
|
if (it.resultCode == Activity.RESULT_OK) {
|
||||||
|
viewModel.addAppWidget(context, widgetId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
AnimatedVisibility(!editMode) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(clockHeight),
|
||||||
|
contentAlignment = Alignment.BottomCenter
|
||||||
|
) {
|
||||||
|
ClockWidget(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val widgets by viewModel.widgets.observeAsState(emptyList())
|
||||||
|
Column {
|
||||||
|
val swapThresholds = remember(widgets) {
|
||||||
|
Array(widgets.size) { floatArrayOf(0f, 0f) }
|
||||||
|
}
|
||||||
|
for ((i, widget) in widgets.withIndex()) {
|
||||||
|
key(if (widget is ExternalWidget) widget.widgetId else widget) {
|
||||||
|
var dragOffsetAfterSwap = remember<Float?> { null }
|
||||||
|
val offsetY = remember(widgets) { Animatable(dragOffsetAfterSwap ?: 0f) }
|
||||||
|
|
||||||
|
LaunchedEffect(widgets) {
|
||||||
|
dragOffsetAfterSwap = null
|
||||||
|
}
|
||||||
|
|
||||||
|
WidgetItem(
|
||||||
|
widget = widget,
|
||||||
|
appWidgetHost = widgetHost,
|
||||||
|
editMode = editMode,
|
||||||
|
onWidgetRemove = {
|
||||||
|
if (widget is ExternalWidget) {
|
||||||
|
widgetHost.deleteAppWidgetId(widget.widgetId)
|
||||||
|
}
|
||||||
|
viewModel.removeWidget(widget)
|
||||||
|
},
|
||||||
|
onWidgetResize = {
|
||||||
|
viewModel.setWidgetHeight(widget, it)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.onPlaced {
|
||||||
|
swapThresholds[i][0] = it.positionInParent().y
|
||||||
|
swapThresholds[i][1] = it.positionInParent().y + it.size.height
|
||||||
|
}
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.offset {
|
||||||
|
IntOffset(0, offsetY.value.toInt())
|
||||||
|
},
|
||||||
|
draggableState = rememberDraggableState {
|
||||||
|
scope.launch {
|
||||||
|
val newOffset = offsetY.value + it
|
||||||
|
offsetY.snapTo(newOffset)
|
||||||
|
if (i > 0 && newOffset < (swapThresholds[i - 1][0] - swapThresholds[i - 1][1])) {
|
||||||
|
if (dragOffsetAfterSwap == null) {
|
||||||
|
dragOffsetAfterSwap =
|
||||||
|
swapThresholds[i - 1][1] - swapThresholds[i - 1][0] + newOffset
|
||||||
|
viewModel.moveUp(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i < widgets.lastIndex && newOffset > (swapThresholds[i + 1][1] - swapThresholds[i + 1][0])) {
|
||||||
|
if (dragOffsetAfterSwap == null) {
|
||||||
|
dragOffsetAfterSwap =
|
||||||
|
swapThresholds[i + 1][0] - swapThresholds[i + 1][1] + newOffset
|
||||||
|
viewModel.moveDown(i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDragStopped = {
|
||||||
|
scope.launch {
|
||||||
|
offsetY.animateTo(0f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val icon =
|
||||||
|
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
|
||||||
|
ExtendedFloatingActionButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.CenterHorizontally),
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
painter = rememberAnimatedVectorPainter(
|
||||||
|
animatedImageVector = icon,
|
||||||
|
atEnd = !editMode
|
||||||
|
), contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
if (editMode) R.string.widget_add_widget
|
||||||
|
else R.string.menu_edit_widgets
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}, onClick = {
|
||||||
|
if (!editMode) {
|
||||||
|
onEditModeChange(true)
|
||||||
|
} else {
|
||||||
|
if (viewModel.getAvailableBuiltInWidgets().isEmpty()) {
|
||||||
|
pickWidgetLauncher.launch(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
PickAppWidgetActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
showAddDialog = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (showAddDialog) {
|
||||||
|
val availableBuiltInWidgets =
|
||||||
|
remember { viewModel.getAvailableBuiltInWidgets() }
|
||||||
|
Dialog(onDismissRequest = { showAddDialog = false }) {
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 16.dp,
|
||||||
|
shadowElevation = 16.dp,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp),
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.widget_add_widget),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
modifier = Modifier.padding(
|
||||||
|
start = 24.dp,
|
||||||
|
end = 24.dp,
|
||||||
|
top = 24.dp,
|
||||||
|
bottom = 8.dp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(
|
||||||
|
top = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
items(availableBuiltInWidgets) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable {
|
||||||
|
viewModel.addWidget(it)
|
||||||
|
showAddDialog = false
|
||||||
|
}
|
||||||
|
.padding(
|
||||||
|
horizontal = 24.dp,
|
||||||
|
vertical = 16.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = it.loadLabel(LocalContext.current),
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.clickable {
|
||||||
|
pickWidgetLauncher.launch(
|
||||||
|
Intent(
|
||||||
|
context,
|
||||||
|
PickAppWidgetActivity::class.java
|
||||||
|
)
|
||||||
|
)
|
||||||
|
showAddDialog = false
|
||||||
|
}
|
||||||
|
.padding(
|
||||||
|
horizontal = 24.dp,
|
||||||
|
vertical = 16.dp
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Rounded.Add,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(end = 8.dp)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.widget_add_external),
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TextButton(
|
||||||
|
onClick = { showAddDialog = false },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(bottom = 16.dp, end = 24.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(android.R.string.cancel)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,14 +18,8 @@ import org.koin.core.component.inject
|
|||||||
class WidgetsVM : ViewModel(), KoinComponent {
|
class WidgetsVM : ViewModel(), KoinComponent {
|
||||||
private val widgetRepository: WidgetRepository by inject()
|
private val widgetRepository: WidgetRepository by inject()
|
||||||
|
|
||||||
val isEditMode = MutableLiveData(false)
|
|
||||||
|
|
||||||
val widgets = widgetRepository.getWidgets().asLiveData()
|
val widgets = widgetRepository.getWidgets().asLiveData()
|
||||||
|
|
||||||
fun setEditMode(editMode: Boolean) {
|
|
||||||
isEditMode.value = editMode
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addWidget(widget: Widget) {
|
fun addWidget(widget: Widget) {
|
||||||
widgetRepository.addWidget(widget, widgets.value?.size ?: 0)
|
widgetRepository.addWidget(widget, widgets.value?.size ?: 0)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,337 +0,0 @@
|
|||||||
package de.mm20.launcher2.ui.launcher.widgets
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.appwidget.AppWidgetHost
|
|
||||||
import android.appwidget.AppWidgetManager
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.activity.viewModels
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.Animatable
|
|
||||||
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
|
|
||||||
import androidx.compose.animation.graphics.res.animatedVectorResource
|
|
||||||
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
|
||||||
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.gestures.rememberDraggableState
|
|
||||||
import androidx.compose.foundation.layout.*
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.items
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.rounded.Add
|
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.runtime.*
|
|
||||||
import androidx.compose.runtime.livedata.observeAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.layout.onPlaced
|
|
||||||
import androidx.compose.ui.layout.positionInParent
|
|
||||||
import androidx.compose.ui.platform.ComposeView
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.window.Dialog
|
|
||||||
import androidx.lifecycle.Lifecycle
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
|
||||||
import de.mm20.launcher2.ui.ClockWidget
|
|
||||||
import de.mm20.launcher2.ui.theme.LauncherTheme
|
|
||||||
import de.mm20.launcher2.ui.R
|
|
||||||
import de.mm20.launcher2.ui.base.ProvideSettings
|
|
||||||
import de.mm20.launcher2.ui.ktx.toDp
|
|
||||||
import de.mm20.launcher2.ui.launcher.widgets.picker.PickAppWidgetActivity
|
|
||||||
import de.mm20.launcher2.widgets.ExternalWidget
|
|
||||||
import kotlinx.coroutines.awaitCancellation
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
|
||||||
class WidgetsView @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs) {
|
|
||||||
|
|
||||||
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
|
|
||||||
|
|
||||||
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
|
|
||||||
|
|
||||||
private val pickWidgetLauncher: ActivityResultLauncher<Intent>
|
|
||||||
|
|
||||||
private val clockWidgetHeight = MutableLiveData(0)
|
|
||||||
|
|
||||||
init {
|
|
||||||
context as AppCompatActivity
|
|
||||||
|
|
||||||
pickWidgetLauncher = context.registerForActivityResult(
|
|
||||||
ActivityResultContracts.StartActivityForResult()
|
|
||||||
) {
|
|
||||||
val data = it.data ?: return@registerForActivityResult
|
|
||||||
val widgetId = data.getIntExtra(
|
|
||||||
AppWidgetManager.EXTRA_APPWIDGET_ID,
|
|
||||||
AppWidgetManager.INVALID_APPWIDGET_ID
|
|
||||||
)
|
|
||||||
if (widgetId == AppWidgetManager.INVALID_APPWIDGET_ID) return@registerForActivityResult
|
|
||||||
if (it.resultCode == Activity.RESULT_OK) {
|
|
||||||
viewModel.addAppWidget(context, widgetId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
context.lifecycleScope.launch {
|
|
||||||
context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
|
||||||
widgetHost.startListening()
|
|
||||||
try {
|
|
||||||
awaitCancellation()
|
|
||||||
} finally {
|
|
||||||
widgetHost.stopListening()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
val composeView = ComposeView(context)
|
|
||||||
composeView.setContent {
|
|
||||||
LauncherTheme {
|
|
||||||
ProvideSettings {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.wrapContentHeight()
|
|
||||||
.padding(8.dp)
|
|
||||||
) {
|
|
||||||
val editMode by viewModel.isEditMode.observeAsState(false)
|
|
||||||
val clockHeight by clockWidgetHeight.observeAsState(0)
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
var showAddDialog by remember { mutableStateOf(false) }
|
|
||||||
|
|
||||||
AnimatedVisibility(!editMode) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(clockHeight.toDp()),
|
|
||||||
contentAlignment = Alignment.BottomCenter
|
|
||||||
) {
|
|
||||||
ClockWidget(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val widgets by viewModel.widgets.observeAsState(emptyList())
|
|
||||||
Column {
|
|
||||||
val swapThresholds = remember(widgets) {
|
|
||||||
Array(widgets.size) { floatArrayOf(0f, 0f) }
|
|
||||||
}
|
|
||||||
for ((i, widget) in widgets.withIndex()) {
|
|
||||||
key(if (widget is ExternalWidget) widget.widgetId else widget) {
|
|
||||||
var dragOffsetAfterSwap = remember<Float?> { null }
|
|
||||||
val offsetY = remember(widgets) { Animatable(dragOffsetAfterSwap ?: 0f) }
|
|
||||||
|
|
||||||
LaunchedEffect(widgets) {
|
|
||||||
dragOffsetAfterSwap = null
|
|
||||||
}
|
|
||||||
|
|
||||||
WidgetItem(
|
|
||||||
widget = widget,
|
|
||||||
appWidgetHost = widgetHost,
|
|
||||||
editMode = editMode,
|
|
||||||
onWidgetRemove = {
|
|
||||||
if (widget is ExternalWidget) {
|
|
||||||
widgetHost.deleteAppWidgetId(widget.widgetId)
|
|
||||||
}
|
|
||||||
viewModel.removeWidget(widget)
|
|
||||||
},
|
|
||||||
onWidgetResize = {
|
|
||||||
viewModel.setWidgetHeight(widget, it)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.onPlaced {
|
|
||||||
swapThresholds[i][0] = it.positionInParent().y
|
|
||||||
swapThresholds[i][1] = it.positionInParent().y + it.size.height
|
|
||||||
}
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
.offset {
|
|
||||||
IntOffset(0, offsetY.value.toInt())
|
|
||||||
},
|
|
||||||
draggableState = rememberDraggableState {
|
|
||||||
scope.launch {
|
|
||||||
val newOffset = offsetY.value + it
|
|
||||||
offsetY.snapTo(newOffset)
|
|
||||||
if (i > 0 && newOffset < (swapThresholds[i - 1][0] - swapThresholds[i - 1][1])) {
|
|
||||||
if (dragOffsetAfterSwap == null) {
|
|
||||||
dragOffsetAfterSwap = swapThresholds[i - 1][1] - swapThresholds[i - 1][0] + newOffset
|
|
||||||
viewModel.moveUp(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (i < widgets.lastIndex && newOffset > (swapThresholds[i + 1][1] - swapThresholds[i + 1][0])) {
|
|
||||||
if (dragOffsetAfterSwap == null) {
|
|
||||||
dragOffsetAfterSwap = swapThresholds[i + 1][0] - swapThresholds[i + 1][1] + newOffset
|
|
||||||
viewModel.moveDown(i)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onDragStopped = {
|
|
||||||
scope.launch {
|
|
||||||
offsetY.animateTo(0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val icon =
|
|
||||||
AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_edit_add)
|
|
||||||
ExtendedFloatingActionButton(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(16.dp)
|
|
||||||
.align(Alignment.CenterHorizontally),
|
|
||||||
icon = {
|
|
||||||
Icon(
|
|
||||||
painter = rememberAnimatedVectorPainter(
|
|
||||||
animatedImageVector = icon,
|
|
||||||
atEnd = !editMode
|
|
||||||
), contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
text = {
|
|
||||||
Text(
|
|
||||||
stringResource(
|
|
||||||
if (editMode) R.string.widget_add_widget
|
|
||||||
else R.string.menu_edit_widgets
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}, onClick = {
|
|
||||||
if (!editMode) {
|
|
||||||
viewModel.setEditMode(true)
|
|
||||||
} else {
|
|
||||||
if (viewModel.getAvailableBuiltInWidgets().isEmpty()) {
|
|
||||||
pickWidgetLauncher.launch(
|
|
||||||
Intent(
|
|
||||||
context,
|
|
||||||
PickAppWidgetActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
showAddDialog = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (showAddDialog) {
|
|
||||||
val availableBuiltInWidgets =
|
|
||||||
remember { viewModel.getAvailableBuiltInWidgets() }
|
|
||||||
Dialog(onDismissRequest = { showAddDialog = false }) {
|
|
||||||
Surface(
|
|
||||||
tonalElevation = 16.dp,
|
|
||||||
shadowElevation = 16.dp,
|
|
||||||
shape = RoundedCornerShape(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp),
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.widget_add_widget),
|
|
||||||
style = MaterialTheme.typography.titleLarge,
|
|
||||||
modifier = Modifier.padding(
|
|
||||||
start = 24.dp,
|
|
||||||
end = 24.dp,
|
|
||||||
top = 24.dp,
|
|
||||||
bottom = 8.dp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(
|
|
||||||
top = 16.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
items(availableBuiltInWidgets) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.clickable {
|
|
||||||
viewModel.addWidget(it)
|
|
||||||
showAddDialog = false
|
|
||||||
}
|
|
||||||
.padding(
|
|
||||||
horizontal = 24.dp,
|
|
||||||
vertical = 16.dp
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = it.loadLabel(context),
|
|
||||||
style = MaterialTheme.typography.bodyLarge
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(top = 16.dp)
|
|
||||||
.clickable {
|
|
||||||
pickWidgetLauncher.launch(
|
|
||||||
Intent(
|
|
||||||
context,
|
|
||||||
PickAppWidgetActivity::class.java
|
|
||||||
)
|
|
||||||
)
|
|
||||||
showAddDialog = false
|
|
||||||
}
|
|
||||||
.padding(
|
|
||||||
horizontal = 24.dp,
|
|
||||||
vertical = 16.dp
|
|
||||||
),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Rounded.Add,
|
|
||||||
contentDescription = null,
|
|
||||||
modifier = Modifier.padding(end = 8.dp)
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.widget_add_external),
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TextButton(
|
|
||||||
onClick = { showAddDialog = false },
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.End)
|
|
||||||
.padding(bottom = 16.dp, end = 24.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(android.R.string.cancel)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
addView(composeView)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setClockWidgetHeight(height: Int) {
|
|
||||||
clockWidgetHeight.value = height
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:id="@+id/rootView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipChildren="false">
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.legacy.view.BatteryChargingView
|
|
||||||
android:id="@+id/batteryAnimationView"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="96dp"
|
|
||||||
android:layout_gravity="bottom|center_horizontal" />
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.launcher.LauncherScaffoldView
|
|
||||||
android:id="@+id/container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent" />
|
|
||||||
|
|
||||||
<View
|
|
||||||
android:id="@+id/activityStartOverlay"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:background="@color/cardview_background"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
|
|
||||||
</FrameLayout>
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
tools:layout_height="match_parent"
|
|
||||||
tools:layout_width="match_parent"
|
|
||||||
tools:parentTag="android.widget.FrameLayout">
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:id="@+id/scrollView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:scrollbars="none"
|
|
||||||
tools:context=".activity.LauncherActivity">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/scrollContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.launcher.search.SearchView
|
|
||||||
android:id="@+id/searchContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginTop="8dp"
|
|
||||||
android:visibility="gone" />
|
|
||||||
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.launcher.widgets.WidgetsView
|
|
||||||
android:id="@+id/widgetContainer"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="false"
|
|
||||||
android:clipToPadding="false"
|
|
||||||
android:orientation="vertical" />
|
|
||||||
</LinearLayout>
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
<de.mm20.launcher2.ui.launcher.search.SearchBarView
|
|
||||||
android:id="@+id/searchBar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="112dp"
|
|
||||||
android:descendantFocusability="beforeDescendants"
|
|
||||||
android:focusableInTouchMode="true" />
|
|
||||||
|
|
||||||
<com.google.android.material.appbar.MaterialToolbar
|
|
||||||
android:id="@+id/editWidgetToolbar"
|
|
||||||
style="@style/Widget.Material3.Toolbar.Surface"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:elevation="4dp"
|
|
||||||
android:translationY="-56dp"
|
|
||||||
android:visibility="gone"
|
|
||||||
app:title="@string/menu_edit_widgets" />
|
|
||||||
</merge>
|
|
||||||
Loading…
x
Reference in New Issue
Block a user