Unclutter this mess we call LauncherActivity
also migrate search bar to Jetpack Compose
This commit is contained in:
parent
0447d3072d
commit
f701e90c47
@ -28,6 +28,7 @@ android {
|
||||
versionCode = versionCodeDate()
|
||||
versionName = "1.3.0"
|
||||
multiDexEnabled = true
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
||||
@ -48,7 +48,7 @@
|
||||
android:label="@string/title_activity_settings"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".ui.legacy.activity.LauncherActivity"
|
||||
android:parentActivityName=".ui.launcher.LauncherActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:taskAffinity="de.mm20.launcher2.settings"
|
||||
android:theme="@style/SettingsTheme">
|
||||
@ -58,7 +58,7 @@
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.legacy.activity.LauncherActivity" />
|
||||
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
|
||||
@ -7,7 +7,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.fragment.app.Fragment
|
||||
import de.mm20.launcher2.fragment.PreferencesMainFragment
|
||||
import de.mm20.launcher2.fragment.PreferencesServicesFragment
|
||||
import de.mm20.launcher2.ui.legacy.activity.LauncherActivity
|
||||
import de.mm20.launcher2.ui.launcher.LauncherActivity
|
||||
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
@ -12,6 +12,7 @@ android {
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
consumerProguardFiles("consumer-rules.pro")
|
||||
signingConfig = signingConfigs.getByName("debug")
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@ -6,7 +6,7 @@
|
||||
<application>
|
||||
|
||||
<activity
|
||||
android:name=".legacy.activity.LauncherActivity"
|
||||
android:name=".launcher.LauncherActivity"
|
||||
android:excludeFromRecents="true"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
@ -46,7 +46,7 @@
|
||||
android:label="@string/title_activity_settings"
|
||||
android:launchMode="singleTask"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".legacy.activity.LauncherActivity"
|
||||
android:parentActivityName=".launcher.LauncherActivity"
|
||||
android:screenOrientation="portrait"
|
||||
android:taskAffinity="de.mm20.launcher2.settings"
|
||||
android:theme="@style/SettingsTheme.NoActionBar">
|
||||
@ -56,7 +56,7 @@
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="de.mm20.launcher2.ui.legacy.activity.LauncherActivity" />
|
||||
android:value="de.mm20.launcher2.ui.launcher.LauncherActivity" />
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
|
||||
@ -0,0 +1,26 @@
|
||||
package de.mm20.launcher2.ui.component
|
||||
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun LauncherCard(
|
||||
modifier: Modifier = Modifier,
|
||||
elevation: Dp = 2.dp,
|
||||
backgroundOpacity: Float = 1f,
|
||||
content: @Composable () -> Unit = {}
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = RoundedCornerShape(8.dp),
|
||||
content = content,
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = backgroundOpacity.coerceIn(0f, 1f)),
|
||||
shadowElevation = elevation,
|
||||
tonalElevation = elevation
|
||||
)
|
||||
}
|
||||
@ -32,10 +32,9 @@ import androidx.compose.ui.unit.sp
|
||||
import com.google.accompanist.pager.ExperimentalPagerApi
|
||||
import com.google.accompanist.pager.PagerState
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.locals.LocalNavController
|
||||
import de.mm20.launcher2.ui.locals.LocalWindowSize
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
import org.koin.androidx.compose.viewModel
|
||||
|
||||
/**
|
||||
@ -55,7 +54,7 @@ fun SearchBar(
|
||||
) {
|
||||
var searchQuery by remember { mutableStateOf("") }
|
||||
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
|
||||
LaunchedEffect(searchQuery) {
|
||||
viewModel.search(searchQuery)
|
||||
|
||||
@ -0,0 +1,162 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.app.WallpaperManager
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.*
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.*
|
||||
import com.afollestad.materialdialogs.LayoutMode
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import de.mm20.launcher2.icons.DynamicIconController
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.ktx.dp
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
||||
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
||||
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
|
||||
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import java.util.*
|
||||
|
||||
|
||||
class LauncherActivity : BaseActivity() {
|
||||
|
||||
private val viewModel: LauncherActivityVM by viewModels()
|
||||
|
||||
private val preferences = LauncherPreferences.instance
|
||||
|
||||
private var windowBackgroundBlur: Boolean = false
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
if (!isAtLeastApiLevel(31)) return
|
||||
window.attributes = window.attributes.also {
|
||||
if (value) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun updateSystemBarAppearance() {
|
||||
val allowLightSystemBars = allowsLightSystemBars()
|
||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
insetsController.isAppearanceLightNavigationBars =
|
||||
allowLightSystemBars && preferences.lightNavBar
|
||||
insetsController.isAppearanceLightStatusBars =
|
||||
allowLightSystemBars && preferences.lightStatusBar
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityLauncherBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val iconRepository: IconRepository by inject()
|
||||
iconRepository.recreate()
|
||||
ThemeHelper.applyTheme(theme)
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
binding = ActivityLauncherBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
|
||||
var editFavoritesDialog: MaterialDialog? = null
|
||||
viewModel.isEditFavoritesShown.observe(this) {
|
||||
if (it) {
|
||||
val view = EditFavoritesView(this@LauncherActivity)
|
||||
editFavoritesDialog =
|
||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
|
||||
customView(view = view)
|
||||
title(res = R.string.menu_item_edit_favs)
|
||||
positiveButton(res = R.string.close) {
|
||||
viewModel.hideEditFavorites()
|
||||
it.dismiss()
|
||||
}
|
||||
onDismiss {
|
||||
view.save()
|
||||
viewModel.hideEditFavorites()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
editFavoritesDialog?.dismiss()
|
||||
editFavoritesDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenItemsDialog: MaterialDialog? = null
|
||||
viewModel.isHiddenItemsShown.observe(this) {
|
||||
if (it) {
|
||||
val view = HiddenItemsView(this)
|
||||
hiddenItemsDialog = MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
||||
.show {
|
||||
title(R.string.menu_hidden_items)
|
||||
customView(view = view)
|
||||
negativeButton(R.string.close) { dismiss() }
|
||||
onDismiss {
|
||||
viewModel.hideHiddenItems()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
hiddenItemsDialog?.dismiss()
|
||||
hiddenItemsDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
if (LauncherPreferences.instance.dimWallpaper) {
|
||||
binding.dimWallpaper.setBackgroundColor(getColor(R.color.wallpaper_dim))
|
||||
}
|
||||
|
||||
val dynamicIconController: DynamicIconController by inject()
|
||||
|
||||
lifecycle.addObserver(dynamicIconController)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ActivityStarter.resume()
|
||||
ActivityStarter.create(binding.rootView)
|
||||
binding.activityStartOverlay.visibility = View.INVISIBLE
|
||||
|
||||
updateSystemBarAppearance()
|
||||
|
||||
binding.container.doOnNextLayout {
|
||||
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
||||
}
|
||||
}
|
||||
|
||||
private fun allowsLightSystemBars(): Boolean {
|
||||
val dimWallpaper = LauncherPreferences.instance.dimWallpaper
|
||||
val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
|
||||
return !(isDarkTheme && dimWallpaper)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ActivityStarter.pause()
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
onBackPressed()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
ActivityStarter.destroy()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class LauncherActivityVM : ViewModel() {
|
||||
val isHiddenItemsShown = MutableLiveData(false)
|
||||
val isEditFavoritesShown = MutableLiveData(false)
|
||||
val dimBackground = MutableLiveData(false)
|
||||
|
||||
fun showEditFavorites() {
|
||||
isEditFavoritesShown.value = true
|
||||
}
|
||||
|
||||
fun hideEditFavorites() {
|
||||
isEditFavoritesShown.value = false
|
||||
}
|
||||
|
||||
|
||||
fun showHiddenItems() {
|
||||
isHiddenItemsShown.value = true
|
||||
}
|
||||
|
||||
fun hideHiddenItems() {
|
||||
isHiddenItemsShown.value = false
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,61 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import de.mm20.launcher2.ktx.isBrightColor
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
|
||||
class LauncherScaffoldVM : ViewModel() {
|
||||
val isSearchOpen = MutableLiveData(false)
|
||||
val blurBackground = MutableLiveData(false)
|
||||
|
||||
val statusBarColor = MutableLiveData(0)
|
||||
val darkStatusBarIcons = MutableLiveData(false)
|
||||
|
||||
val searchBarLevel = MutableLiveData(SearchBarLevel.Resting)
|
||||
|
||||
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() {
|
||||
if (isSearchOpen.value == true) return
|
||||
isSearchOpen.value = true
|
||||
if (scrollY == 0) {
|
||||
searchBarLevel.value = SearchBarLevel.Active
|
||||
blurBackground.value = true
|
||||
}
|
||||
}
|
||||
|
||||
fun closeSearch() {
|
||||
if (isSearchOpen.value == false) return
|
||||
isSearchOpen.value = false
|
||||
if (scrollY == 0) {
|
||||
searchBarLevel.value = SearchBarLevel.Resting
|
||||
blurBackground.value = false
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleSearch() {
|
||||
if (isSearchOpen.value == true) closeSearch()
|
||||
else openSearch()
|
||||
}
|
||||
|
||||
fun setStatusBarColor(color: Int) {
|
||||
statusBarColor.value = color
|
||||
darkStatusBarIcons.value = color.isBrightColor()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,281 @@
|
||||
package de.mm20.launcher2.ui.launcher
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.LayoutTransition
|
||||
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.WindowCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.core.view.setPadding
|
||||
import de.mm20.launcher2.ktx.dp
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.ktx.isBrightColor
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
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.SearchVM
|
||||
import de.mm20.launcher2.ui.launcher.widgets.WidgetsVM
|
||||
|
||||
@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 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()
|
||||
}
|
||||
|
||||
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
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
|
||||
)
|
||||
}
|
||||
|
||||
widgetsViewModel.isEditMode.observe(context) {
|
||||
if (it) {
|
||||
binding.scrollView.setOnTouchListener(null)
|
||||
OneShotLayoutTransition.run(binding.scrollContainer)
|
||||
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.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
||||
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
binding.searchBar.onFocus = {
|
||||
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) {
|
||||
WindowInsetsControllerCompat(context.window, this).isAppearanceLightStatusBars = it
|
||||
}
|
||||
}
|
||||
|
||||
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.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() {
|
||||
binding.searchContainer.visibility = View.VISIBLE
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,323 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColor
|
||||
import androidx.compose.animation.core.animateDp
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
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.Image
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.Search
|
||||
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.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import coil.compose.rememberImagePainter
|
||||
import de.mm20.launcher2.ktx.tryStartActivity
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.component.LauncherCard
|
||||
import de.mm20.launcher2.ui.launcher.LauncherActivityVM
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
import java.io.File
|
||||
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
level: SearchBarLevel,
|
||||
onFocus: () -> Unit = {}
|
||||
) {
|
||||
val searchViewModel: SearchVM = viewModel()
|
||||
val activityViewModel: LauncherActivityVM = viewModel()
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val query by searchViewModel.searchQuery.observeAsState("")
|
||||
|
||||
val websearches by searchViewModel.websearchResults.observeAsState(emptyList())
|
||||
|
||||
SearchBar(
|
||||
level,
|
||||
websearches,
|
||||
value = query,
|
||||
onValueChange = {
|
||||
searchViewModel.search(it)
|
||||
},
|
||||
overflowMenu = { show, onDismissRequest ->
|
||||
DropdownMenu(expanded = show, onDismissRequest = onDismissRequest) {
|
||||
DropdownMenuItem(onClick = {
|
||||
activityViewModel.showEditFavorites()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.menu_item_edit_favs))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
activityViewModel.showHiddenItems()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.menu_hidden_items))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||
null
|
||||
)
|
||||
)
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.wallpaper))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
context.startActivity(Intent(context, SettingsActivity::class.java))
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(stringResource(R.string.settings))
|
||||
}
|
||||
DropdownMenuItem(onClick = {
|
||||
context.startActivity(Intent().also {
|
||||
it.component = ComponentName(
|
||||
context.packageName,
|
||||
"de.mm20.launcher2.activity.SettingsActivity"
|
||||
)
|
||||
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
})
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text("Legacy Settings")
|
||||
}
|
||||
}
|
||||
},
|
||||
onFocus = onFocus,
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
||||
@Composable
|
||||
fun SearchBar(
|
||||
level: SearchBarLevel,
|
||||
websearches: List<Websearch>,
|
||||
overflowMenu: @Composable (show: Boolean, onDismissRequest: () -> Unit) -> Unit = { _, _ -> },
|
||||
value: String,
|
||||
onValueChange: (String) -> Unit,
|
||||
onFocus: () -> Unit = {}
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
var showOverflowMenu by remember { mutableStateOf(false) }
|
||||
|
||||
val transition = updateTransition(level, label = "Searchbar")
|
||||
|
||||
|
||||
val elevation by transition.animateDp(
|
||||
label = "elevation",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
targetState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}
|
||||
) {
|
||||
when (it) {
|
||||
SearchBarLevel.Resting -> 0.dp
|
||||
SearchBarLevel.Active -> 2.dp
|
||||
SearchBarLevel.Raised -> 8.dp
|
||||
}
|
||||
}
|
||||
|
||||
val backgroundOpacity by transition.animateFloat(label = "backgroundOpacity",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}) {
|
||||
if (it == SearchBarLevel.Resting) 0f else 1f
|
||||
}
|
||||
|
||||
val contentColor by transition.animateColor(label = "textColor",
|
||||
transitionSpec = {
|
||||
when {
|
||||
initialState == SearchBarLevel.Resting -> tween(durationMillis = 200)
|
||||
targetState == SearchBarLevel.Resting -> tween(
|
||||
durationMillis = 200,
|
||||
delayMillis = 200
|
||||
)
|
||||
else -> tween(durationMillis = 500)
|
||||
}
|
||||
}) {
|
||||
if (it == SearchBarLevel.Resting) Color.White else LocalContentColor.current
|
||||
}
|
||||
|
||||
val rightIcon = AnimatedImageVector.animatedVectorResource(R.drawable.anim_ic_menu_clear)
|
||||
|
||||
LauncherCard(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentHeight()
|
||||
.padding(8.dp),
|
||||
backgroundOpacity = backgroundOpacity,
|
||||
elevation = elevation
|
||||
) {
|
||||
Column {
|
||||
Row(
|
||||
modifier = Modifier.height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(12.dp),
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
Box(
|
||||
modifier = Modifier.weight(1f)
|
||||
) {
|
||||
if (value.isEmpty()) {
|
||||
Text(
|
||||
text = stringResource(R.string.edit_text_search_hint),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
color = contentColor
|
||||
)
|
||||
}
|
||||
val focusManager = LocalFocusManager.current
|
||||
LaunchedEffect(level) {
|
||||
if (level == SearchBarLevel.Resting) focusManager.clearFocus()
|
||||
}
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.onFocusChanged {
|
||||
if (it.hasFocus) onFocus()
|
||||
}
|
||||
.fillMaxWidth(),
|
||||
textStyle = MaterialTheme.typography.bodyLarge.copy(
|
||||
color = contentColor
|
||||
),
|
||||
singleLine = true,
|
||||
value = value,
|
||||
onValueChange = onValueChange,
|
||||
)
|
||||
}
|
||||
Box {
|
||||
IconButton(onClick = {
|
||||
if (value.isNotBlank()) onValueChange("")
|
||||
else showOverflowMenu = true
|
||||
}) {
|
||||
Icon(
|
||||
painter = rememberAnimatedVectorPainter(
|
||||
rightIcon,
|
||||
atEnd = value.isNotBlank()
|
||||
),
|
||||
contentDescription = null,
|
||||
tint = contentColor
|
||||
)
|
||||
}
|
||||
overflowMenu(showOverflowMenu) { showOverflowMenu = false }
|
||||
}
|
||||
}
|
||||
AnimatedVisibility(websearches.isNotEmpty()) {
|
||||
LazyRow(
|
||||
modifier = Modifier
|
||||
.height(48.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
items(websearches) {
|
||||
Surface(
|
||||
shape = RoundedCornerShape(4.dp),
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.height(32.dp)
|
||||
.clickable {
|
||||
it
|
||||
.getLaunchIntent()
|
||||
?.let {
|
||||
context.tryStartActivity(it)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
val icon = it.icon
|
||||
if (icon == null) {
|
||||
Icon(
|
||||
imageVector = Icons.Rounded.Search,
|
||||
contentDescription = null,
|
||||
tint = if (it.color == 0) MaterialTheme.colorScheme.primary else Color(
|
||||
it.color
|
||||
)
|
||||
)
|
||||
} else {
|
||||
Image(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = rememberImagePainter(File(icon)),
|
||||
contentDescription = null
|
||||
)
|
||||
}
|
||||
Text(
|
||||
it.label,
|
||||
modifier = Modifier.padding(start = 4.dp),
|
||||
style = MaterialTheme.typography.labelMedium
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum class SearchBarLevel {
|
||||
/**
|
||||
* The default, "hidden" state, when the launcher is in its initial state (scroll position is 0
|
||||
* and search is closed)
|
||||
*/
|
||||
Resting,
|
||||
|
||||
/**
|
||||
* When the search is open but there is no content behind the search bar (scroll position is 0)
|
||||
*/
|
||||
Active,
|
||||
|
||||
/**
|
||||
* When there is content below the search bar which requires the search bar to be raised above
|
||||
* this content (scroll position is not 0)
|
||||
*/
|
||||
Raised
|
||||
}
|
||||
@ -21,7 +21,7 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class SearchViewModel : ViewModel(), KoinComponent {
|
||||
class SearchVM : ViewModel(), KoinComponent {
|
||||
|
||||
private val favoritesRepository: FavoritesRepository by inject()
|
||||
|
||||
@ -36,6 +36,7 @@ class SearchViewModel : ViewModel(), KoinComponent {
|
||||
private val websearchRepository: WebsearchRepository by inject()
|
||||
|
||||
val isSearching = MutableLiveData(false)
|
||||
val searchQuery = MutableLiveData("")
|
||||
|
||||
val favorites by lazy {
|
||||
favoritesRepository.getFavorites().asLiveData()
|
||||
@ -53,8 +54,13 @@ class SearchViewModel : ViewModel(), KoinComponent {
|
||||
|
||||
val hideFavorites = MutableLiveData(false)
|
||||
|
||||
init {
|
||||
search("")
|
||||
}
|
||||
|
||||
var searchJob: Job? = null
|
||||
fun search(query: String) {
|
||||
searchQuery.value = query
|
||||
try {
|
||||
searchJob?.cancel()
|
||||
} catch (e: CancellationException) {
|
||||
@ -0,0 +1,20 @@
|
||||
package de.mm20.launcher2.ui.launcher.search
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.LinearLayout
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
import de.mm20.launcher2.ui.databinding.ViewSearchBinding
|
||||
|
||||
class SearchView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
val binding = ViewSearchBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
init {
|
||||
orientation = VERTICAL
|
||||
layoutTransition = ChangingLayoutTransition()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets
|
||||
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.liveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetRepository
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
|
||||
class WidgetsVM: ViewModel(), KoinComponent {
|
||||
private val widgetRepository: WidgetRepository by inject()
|
||||
|
||||
val isEditMode = MutableLiveData(false)
|
||||
|
||||
val widgets = liveData<List<Widget>?> {
|
||||
emit(widgetRepository.getWidgets())
|
||||
}
|
||||
|
||||
fun setEditMode(editMode: Boolean) {
|
||||
isEditMode.value = editMode
|
||||
}
|
||||
|
||||
fun saveWidgets(widgets: List<Widget>) {
|
||||
viewModelScope.launch {
|
||||
widgetRepository.saveWidgets(widgets)
|
||||
}
|
||||
}
|
||||
|
||||
fun getInternalWidgets() : List<Widget> {
|
||||
return widgetRepository.getInternalWidgets()
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,291 @@
|
||||
package de.mm20.launcher2.ui.launcher.widgets
|
||||
|
||||
import android.animation.LayoutTransition
|
||||
import android.annotation.SuppressLint
|
||||
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.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.view.iterator
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.lifecycle.repeatOnLifecycle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.balsikandar.crashreporter.CrashReporter
|
||||
import de.mm20.launcher2.ktx.dp
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.databinding.ViewWidgetsBinding
|
||||
import de.mm20.launcher2.ui.legacy.component.WidgetView
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetType
|
||||
import kotlinx.coroutines.awaitCancellation
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
class WidgetsView @JvmOverloads constructor(
|
||||
context: Context, attrs: AttributeSet? = null
|
||||
) : LinearLayout(context, attrs) {
|
||||
|
||||
private val binding = ViewWidgetsBinding.inflate(LayoutInflater.from(context), this)
|
||||
|
||||
private val widgetHost: AppWidgetHost = AppWidgetHost(context.applicationContext, 44203)
|
||||
|
||||
private val viewModel: WidgetsVM by (context as AppCompatActivity).viewModels()
|
||||
|
||||
private lateinit var widgets: MutableList<Widget>
|
||||
|
||||
private val pickWidgetLauncher: ActivityResultLauncher<Intent>
|
||||
private val configureWidgetLauncher: ActivityResultLauncher<Intent>
|
||||
|
||||
init {
|
||||
context as AppCompatActivity
|
||||
|
||||
layoutTransition = ChangingLayoutTransition()
|
||||
binding.widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
configureWidgetLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
val data = it.data ?: return@registerForActivityResult
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
bindAppWidget(data)
|
||||
}
|
||||
}
|
||||
|
||||
pickWidgetLauncher = context.registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()
|
||||
) {
|
||||
val data = it.data ?: return@registerForActivityResult
|
||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (widgetId == -1) return@registerForActivityResult
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
val appWidget = AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetInfo(widgetId) ?: return@registerForActivityResult
|
||||
if (appWidget.configure != null) {
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
|
||||
intent.component = appWidget.configure
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||
configureWidgetLauncher.launch(intent)
|
||||
} else {
|
||||
bindAppWidget(data)
|
||||
}
|
||||
} else {
|
||||
widgetHost.deleteAppWidgetId(widgetId)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.widgets.observe(context) {
|
||||
if (it != null && !::widgets.isInitialized) {
|
||||
widgets = it.toMutableList()
|
||||
initWidgets()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.isEditMode.observe(context) {
|
||||
if (it) {
|
||||
binding.clockWidget.visibility = View.GONE
|
||||
|
||||
for (v in binding.widgetList.iterator()) {
|
||||
if (v is WidgetView) {
|
||||
v.editMode = true
|
||||
v.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this)
|
||||
binding.fabEditWidget.apply {
|
||||
setIconResource(R.drawable.ic_add)
|
||||
setText(R.string.widget_add_widget)
|
||||
setOnClickListener {
|
||||
addWidget()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (::widgets.isInitialized) viewModel.saveWidgets(widgets)
|
||||
binding.widgetList.layoutTransition = ChangingLayoutTransition()
|
||||
binding.clockWidget.visibility = View.VISIBLE
|
||||
for (v in binding.widgetList.iterator()) {
|
||||
if (v is WidgetView) {
|
||||
v.editMode = false
|
||||
v.layoutTransition = ChangingLayoutTransition()
|
||||
}
|
||||
}
|
||||
binding.fabEditWidget.apply {
|
||||
setIconResource(R.drawable.ic_edit)
|
||||
setText(R.string.menu_edit_widgets)
|
||||
setOnClickListener {
|
||||
viewModel.setEditMode(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.lifecycleScope.launch {
|
||||
context.lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
|
||||
widgetHost.startListening()
|
||||
try {
|
||||
awaitCancellation()
|
||||
} finally {
|
||||
// TODO: find out why there is a NPE thrown sometimes
|
||||
try {
|
||||
widgetHost.stopListening()
|
||||
} catch (e: NullPointerException) {
|
||||
CrashReporter.logException(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.fabEditWidget.setOnClickListener {
|
||||
viewModel.setEditMode(true)
|
||||
}
|
||||
}
|
||||
|
||||
fun setClockWidgetHeight(height: Int) {
|
||||
val params = binding.clockWidget.layoutParams
|
||||
params.height = height
|
||||
binding.clockWidget.layoutParams = params
|
||||
}
|
||||
|
||||
|
||||
private fun initWidgets() {
|
||||
binding.widgetList.removeAllViews()
|
||||
val params = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
for (w in widgets) {
|
||||
val view = WidgetView(context)
|
||||
view.layoutTransition = ChangingLayoutTransition()
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(w, widgetHost)) {
|
||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
binding.widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
|
||||
Collections.swap(widgets, firstPosition, secondPosition)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressLint("CheckResult")
|
||||
private fun addWidget() {
|
||||
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
||||
val internalWidgets =
|
||||
viewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
||||
if (internalWidgets.isNotEmpty()) {
|
||||
MaterialDialog(context).show {
|
||||
title(R.string.widget_add_widget)
|
||||
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
|
||||
val widget = internalWidgets[index]
|
||||
val view = WidgetView(context)
|
||||
val params = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(widget, widgetHost)) {
|
||||
view.editMode = true
|
||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this@WidgetsView)
|
||||
binding.widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this@WidgetsView)
|
||||
}
|
||||
widgets.add(widget)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
|
||||
neutralButton(R.string.widget_add_external) {
|
||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
pickWidgetLauncher.launch(pickIntent)
|
||||
it.dismiss()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
pickWidgetLauncher.launch(pickIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeWidget(widget: Widget?) {
|
||||
widget ?: return
|
||||
widgets.remove(widget)
|
||||
val id = widget.data.toIntOrNull() ?: return
|
||||
widgetHost.deleteAppWidgetId(id)
|
||||
}
|
||||
|
||||
private fun bindAppWidget(data: Intent) {
|
||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (widgetId == -1) return
|
||||
val appWidget = AppWidgetManager.getInstance(context)
|
||||
.getAppWidgetInfo(widgetId) ?: return
|
||||
val widget = Widget(
|
||||
type = WidgetType.THIRD_PARTY,
|
||||
data = widgetId.toString(),
|
||||
height = appWidget.minHeight
|
||||
)
|
||||
val params = LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
val view = WidgetView(context)
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(widget, widgetHost)) {
|
||||
view.editMode = true
|
||||
|
||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this)
|
||||
binding.widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(this)
|
||||
}
|
||||
widgets.add(widget)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,743 +0,0 @@
|
||||
package de.mm20.launcher2.ui.legacy.activity
|
||||
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.LayoutTransition
|
||||
import android.animation.ObjectAnimator
|
||||
import android.app.Activity
|
||||
import android.app.WallpaperManager
|
||||
import android.appwidget.AppWidgetHost
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.graphics.Point
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.util.TypedValue
|
||||
import android.view.*
|
||||
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
|
||||
import android.view.ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.Toast
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.animation.doOnEnd
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.*
|
||||
import androidx.core.widget.NestedScrollView
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.afollestad.materialdialogs.LayoutMode
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.afollestad.materialdialogs.bottomsheets.BottomSheet
|
||||
import com.afollestad.materialdialogs.callbacks.onDismiss
|
||||
import com.afollestad.materialdialogs.customview.customView
|
||||
import com.afollestad.materialdialogs.list.listItems
|
||||
import com.jmedeisis.draglinearlayout.DragLinearLayout
|
||||
import de.mm20.launcher2.icons.DynamicIconController
|
||||
import de.mm20.launcher2.icons.IconRepository
|
||||
import de.mm20.launcher2.ktx.dp
|
||||
import de.mm20.launcher2.ktx.isAtLeastApiLevel
|
||||
import de.mm20.launcher2.ktx.isBrightColor
|
||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
import de.mm20.launcher2.transition.OneShotLayoutTransition
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.base.BaseActivity
|
||||
import de.mm20.launcher2.ui.databinding.ActivityLauncherBinding
|
||||
import de.mm20.launcher2.ui.launcher.modals.EditFavoritesView
|
||||
import de.mm20.launcher2.ui.launcher.modals.HiddenItemsView
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.legacy.component.WidgetView
|
||||
import de.mm20.launcher2.ui.legacy.helper.ThemeHelper
|
||||
import de.mm20.launcher2.ui.settings.SettingsActivity
|
||||
import de.mm20.launcher2.widgets.Widget
|
||||
import de.mm20.launcher2.widgets.WidgetType
|
||||
import de.mm20.launcher2.widgets.WidgetViewModel
|
||||
import kotlinx.coroutines.*
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||
import java.util.*
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
class LauncherActivity : BaseActivity() {
|
||||
|
||||
/**
|
||||
* True if the search result list is visible
|
||||
*/
|
||||
private var searchVisibility = false
|
||||
set(value) {
|
||||
field = value
|
||||
windowBackgroundBlur = value
|
||||
}
|
||||
|
||||
private lateinit var widgetHost: AppWidgetHost
|
||||
private val widgets = mutableListOf<Widget>()
|
||||
|
||||
private lateinit var overlayView: ViewGroupOverlay
|
||||
|
||||
private val widgetViewModel: WidgetViewModel by viewModel()
|
||||
|
||||
private val searchViewModel: SearchViewModel by viewModels()
|
||||
|
||||
private val preferences = LauncherPreferences.instance
|
||||
|
||||
private var windowBackgroundBlur: Boolean = false
|
||||
set(value) {
|
||||
if (field == value) return
|
||||
field = value
|
||||
if (!isAtLeastApiLevel(31)) return
|
||||
window.attributes = window.attributes.also {
|
||||
if (value) {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var widgetEditMode = false
|
||||
set(value) {
|
||||
field = value
|
||||
if (value) {
|
||||
binding.clockWidget.visibility = View.GONE
|
||||
binding.searchBar.setRightIcon(R.drawable.ic_done)
|
||||
binding.scrollView.setOnTouchListener(null)
|
||||
for (v in binding.widgetList.iterator()) {
|
||||
if (v is WidgetView) {
|
||||
v.editMode = true
|
||||
v.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
OneShotLayoutTransition.run(binding.scrollContainer)
|
||||
binding.fabEditWidget.apply {
|
||||
setIconResource(R.drawable.ic_add)
|
||||
setText(R.string.widget_add_widget)
|
||||
setOnClickListener {
|
||||
addWidget()
|
||||
}
|
||||
}
|
||||
val statusBarColor = TypedValue().also {
|
||||
theme.resolveAttribute(
|
||||
R.attr.colorSurface,
|
||||
it,
|
||||
true
|
||||
)
|
||||
}.data
|
||||
window.statusBarColor = statusBarColor
|
||||
if (statusBarColor.isBrightColor()) {
|
||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
insetsController.isAppearanceLightStatusBars = true
|
||||
}
|
||||
binding.searchBar.visibility = View.INVISIBLE
|
||||
binding.editWidgetToolbar
|
||||
.animate()
|
||||
.translationY(0f)
|
||||
.alpha(1f)
|
||||
.withStartAction {
|
||||
binding.editWidgetToolbar.visibility = View.VISIBLE
|
||||
}
|
||||
.start()
|
||||
} else {
|
||||
widgetViewModel.saveWidgets(widgets)
|
||||
binding.widgetList.layoutTransition = ChangingLayoutTransition()
|
||||
binding.widgetContainer.layoutTransition = ChangingLayoutTransition()
|
||||
binding.scrollContainer.layoutTransition = ChangingLayoutTransition()
|
||||
binding.searchBar.setRightIcon(R.drawable.ic_more_vert)
|
||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||
binding.clockWidget.visibility = View.VISIBLE
|
||||
for (v in binding.widgetList.iterator()) {
|
||||
if (v is WidgetView) {
|
||||
v.editMode = false
|
||||
v.layoutTransition = ChangingLayoutTransition()
|
||||
}
|
||||
}
|
||||
binding.fabEditWidget.apply {
|
||||
setIconResource(R.drawable.ic_edit)
|
||||
setText(R.string.menu_edit_widgets)
|
||||
setOnClickListener {
|
||||
widgetEditMode = true
|
||||
}
|
||||
}
|
||||
window.statusBarColor = Color.TRANSPARENT
|
||||
|
||||
updateSystemBarAppearance()
|
||||
|
||||
binding.searchBar.visibility = View.VISIBLE
|
||||
binding.editWidgetToolbar
|
||||
.animate()
|
||||
.translationY(-binding.editWidgetToolbar.height.toFloat())
|
||||
.alpha(0f)
|
||||
.withEndAction {
|
||||
binding.editWidgetToolbar.visibility = View.GONE
|
||||
}
|
||||
.start()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateSystemBarAppearance() {
|
||||
val allowLightSystemBars = allowsLightSystemBars()
|
||||
val insetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||
insetsController.isAppearanceLightNavigationBars =
|
||||
allowLightSystemBars && preferences.lightNavBar
|
||||
insetsController.isAppearanceLightStatusBars =
|
||||
allowLightSystemBars && preferences.lightStatusBar
|
||||
}
|
||||
|
||||
private lateinit var binding: ActivityLauncherBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val iconRepository: IconRepository by inject()
|
||||
iconRepository.recreate()
|
||||
ThemeHelper.applyTheme(theme)
|
||||
|
||||
if (LauncherPreferences.instance.firstRunVersion < 1) {
|
||||
LauncherPreferences.instance.firstRunVersion = 1
|
||||
}
|
||||
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
|
||||
binding = ActivityLauncherBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(binding.root)
|
||||
|
||||
overlayView = binding.rootView.overlay
|
||||
|
||||
if (LauncherPreferences.instance.dimWallpaper) {
|
||||
binding.dimWallpaper.setBackgroundColor(getColor(R.color.wallpaper_dim))
|
||||
}
|
||||
|
||||
binding.scrollContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.searchContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.widgetContainer.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
val params = binding.clockWidget.layoutParams
|
||||
params.height = Point().also { windowManager.defaultDisplay.getSize(it) }.y
|
||||
binding.clockWidget.layoutParams = params
|
||||
binding.container.doOnLayout {
|
||||
adjustWidgetSpace()
|
||||
}
|
||||
initWidgets()
|
||||
binding.scrollView.setOnTouchListener(scrollViewOnTouchListener)
|
||||
binding.scrollView.setOnScrollChangeListener { _: NestedScrollView?, _: Int, scrollY: Int, _: Int, oldScrollY: Int ->
|
||||
when {
|
||||
/* Hide searchbar*/
|
||||
scrollY > oldScrollY && ((scrollY > binding.searchBar.height) || widgetEditMode) -> {
|
||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
||||
if (newTransY < -binding.searchBar.height.toFloat() * 1.5f) {
|
||||
newTransY = -binding.searchBar.height.toFloat() * 1.5f
|
||||
}
|
||||
binding.searchBar.translationY = newTransY
|
||||
}
|
||||
/* Show searchbar*/
|
||||
scrollY < oldScrollY -> {
|
||||
var newTransY = binding.searchBar.translationY - scrollY + oldScrollY
|
||||
if (newTransY > 0f) {
|
||||
newTransY = 0f
|
||||
}
|
||||
binding.searchBar.translationY = newTransY
|
||||
}
|
||||
}
|
||||
if (scrollY > 0 && (searchVisibility || widgetEditMode)
|
||||
) {
|
||||
binding.searchBar.raise()
|
||||
} else binding.searchBar.drop()
|
||||
if (scrollY == 0) {
|
||||
if (!searchVisibility) {
|
||||
binding.searchBar.hide()
|
||||
windowBackgroundBlur = false
|
||||
}
|
||||
} else {
|
||||
binding.searchBar.show()
|
||||
if (!searchVisibility) {
|
||||
windowBackgroundBlur = true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
binding.searchBar.onRightIconClick = onRightIconClick@{
|
||||
if (widgetEditMode) widgetEditMode = false
|
||||
else {
|
||||
val menu = PopupMenu(this, it)
|
||||
menu.inflate(R.menu.menu_launcher)
|
||||
menu.setOnMenuItemClickListener { item ->
|
||||
when (item.itemId) {
|
||||
R.id.menu_item_settings -> {
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
R.id.menu_item_wallpaper -> {
|
||||
startActivity(
|
||||
Intent.createChooser(
|
||||
Intent(Intent.ACTION_SET_WALLPAPER),
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
R.id.menu_item_hidden -> {
|
||||
val view = HiddenItemsView(this)
|
||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT))
|
||||
.show {
|
||||
title(R.string.menu_hidden_items)
|
||||
customView(view = view)
|
||||
negativeButton(R.string.close) { dismiss() }
|
||||
}
|
||||
//hiddenAppsActivated = true
|
||||
}
|
||||
R.id.menu_item_edit_favs -> {
|
||||
val view = EditFavoritesView(this@LauncherActivity)
|
||||
MaterialDialog(this, BottomSheet(LayoutMode.MATCH_PARENT)).show {
|
||||
customView(view = view)
|
||||
title(res = R.string.menu_item_edit_favs)
|
||||
positiveButton(res = R.string.close) {
|
||||
it.dismiss()
|
||||
}
|
||||
onDismiss {
|
||||
view.save()
|
||||
}
|
||||
}
|
||||
}
|
||||
R.id.menu_item_settings_old -> {
|
||||
finish()
|
||||
startActivity(Intent().also {
|
||||
it.component = ComponentName(
|
||||
packageName,
|
||||
"de.mm20.launcher2.activity.SettingsActivity"
|
||||
)
|
||||
it.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
})
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
menu.show()
|
||||
}
|
||||
}
|
||||
binding.searchBar.setOnTouchListener { _, _ ->
|
||||
if (!searchVisibility) showSearch()
|
||||
false
|
||||
}
|
||||
|
||||
binding.searchBar.onSearchQueryChanged = {
|
||||
search(it)
|
||||
}
|
||||
binding.widgetList.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
|
||||
binding.fabEditWidget.setOnClickListener {
|
||||
widgetEditMode = true
|
||||
}
|
||||
|
||||
binding.editWidgetToolbar.apply {
|
||||
navigationIcon =
|
||||
ContextCompat.getDrawable(this@LauncherActivity, R.drawable.ic_done)?.apply {
|
||||
setTint(ContextCompat.getColor(this@LauncherActivity, R.color.icon_color))
|
||||
}
|
||||
setNavigationOnClickListener {
|
||||
widgetEditMode = false
|
||||
}
|
||||
}
|
||||
|
||||
val dynamicIconController: DynamicIconController by inject()
|
||||
|
||||
lifecycle.addObserver(dynamicIconController)
|
||||
|
||||
lifecycleScope.launch {
|
||||
widgets.addAll(widgetViewModel.getWidgets())
|
||||
initWidgets()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun initWidgets() {
|
||||
widgetHost = AppWidgetHost(applicationContext, 0xacab)
|
||||
binding.widgetList.removeAllViews()
|
||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
for (w in widgets) {
|
||||
val view = WidgetView(this)
|
||||
view.layoutTransition = ChangingLayoutTransition()
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(w, widgetHost)) {
|
||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
binding.widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binding.widgetList.setOnViewSwapListener { _, firstPosition, _, secondPosition ->
|
||||
Collections.swap(widgets, firstPosition, secondPosition)
|
||||
}
|
||||
}
|
||||
|
||||
private fun addWidget() {
|
||||
val usedWidgets = widgets.filter { it.type == WidgetType.INTERNAL }.map { it.data }
|
||||
val internalWidgets =
|
||||
widgetViewModel.getInternalWidgets().filter { !usedWidgets.contains(it.data) }
|
||||
if (internalWidgets.isNotEmpty()) {
|
||||
MaterialDialog(this).show {
|
||||
val widgetList =
|
||||
this@LauncherActivity.findViewById<DragLinearLayout>(R.id.widgetList)
|
||||
val widgetContainer =
|
||||
this@LauncherActivity.findViewById<LinearLayout>(R.id.widgetContainer)
|
||||
title(R.string.widget_add_widget)
|
||||
listItems(items = internalWidgets.map { it.label }) { dialog, index, _ ->
|
||||
val widget = internalWidgets[index]
|
||||
val view = WidgetView(this@LauncherActivity)
|
||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(widget, widgetHost)) {
|
||||
view.editMode = true
|
||||
widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(widgetList)
|
||||
OneShotLayoutTransition.run(widgetContainer)
|
||||
widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(widgetList)
|
||||
OneShotLayoutTransition.run(widgetContainer)
|
||||
}
|
||||
widgets.add(widget)
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
@Suppress("DEPRECATION") // I don't care that neutral buttons are discouraged.
|
||||
neutralButton(R.string.widget_add_external) {
|
||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
|
||||
it.dismiss()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val appWidgetId = widgetHost.allocateAppWidgetId()
|
||||
val pickIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_PICK)
|
||||
pickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
startActivityForResult(pickIntent, REQUEST_PICK_APPWIDGET)
|
||||
}
|
||||
}
|
||||
|
||||
private fun removeWidget(widget: Widget?) {
|
||||
widget ?: return
|
||||
widgets.remove(widget)
|
||||
val id = widget.data.toIntOrNull() ?: return
|
||||
widgetHost.deleteAppWidgetId(id)
|
||||
}
|
||||
|
||||
private fun adjustWidgetSpace() {
|
||||
val height = binding.scrollView.height - binding.searchBar.height - 8 * dp
|
||||
binding.clockWidget.layoutParams = binding.clockWidget.layoutParams.also {
|
||||
it.height = height.toInt()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun search(text: String) {
|
||||
searchViewModel.search(text)
|
||||
if (binding.webSearchViewSpacer.tag != "measured" || binding.webSearchViewSpacer.height == 0) {
|
||||
val webSearchView = binding.searchBar.getWebSearchView()
|
||||
webSearchView.doOnNextLayout {
|
||||
binding.webSearchViewSpacer.layoutParams = binding.webSearchViewSpacer.layoutParams
|
||||
.apply { height = webSearchView.height }
|
||||
binding.webSearchViewSpacer.tag = "measured"
|
||||
}
|
||||
}
|
||||
binding.webSearchViewSpacer.visibility = if (text.isBlank()) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
private fun toggleSearch() {
|
||||
if (searchVisibility) {
|
||||
hideSearch()
|
||||
} else {
|
||||
showSearch()
|
||||
}
|
||||
}
|
||||
|
||||
private fun hideSearch() {
|
||||
|
||||
searchVisibility = false
|
||||
val set = AnimatorSet()
|
||||
set.duration = 300
|
||||
set.doOnEnd {
|
||||
binding.searchContainer.visibility = View.GONE
|
||||
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.start()
|
||||
binding.scrollView.scrollTo(0, 0)
|
||||
binding.searchBar.hide()
|
||||
if (!binding.searchBar.getSearchQuery().isEmpty()) binding.searchBar.setSearchQuery("")
|
||||
(getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager)
|
||||
.hideSoftInputFromWindow(binding.searchBar.windowToken, 0)
|
||||
}
|
||||
|
||||
private fun showSearch() {
|
||||
|
||||
searchVisibility = true
|
||||
binding.searchBar.show()
|
||||
binding.searchContainer.visibility = View.VISIBLE
|
||||
binding.widgetContainer.visibility = View.GONE
|
||||
val set = AnimatorSet()
|
||||
set.duration = 300
|
||||
set.doOnEnd {
|
||||
search("")
|
||||
}
|
||||
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()
|
||||
}
|
||||
|
||||
override fun onBackPressed() {
|
||||
if (widgetEditMode) widgetEditMode = false
|
||||
if (searchVisibility) hideSearch()
|
||||
else ObjectAnimator.ofInt(binding.scrollView, "scrollY", 0).setDuration(200).start()
|
||||
|
||||
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
ActivityStarter.resume()
|
||||
ActivityStarter.create(binding.rootView)
|
||||
binding.activityStartOverlay.visibility = View.INVISIBLE
|
||||
|
||||
search(binding.searchBar.getSearchQuery())
|
||||
|
||||
updateSystemBarAppearance()
|
||||
|
||||
binding.container.doOnNextLayout {
|
||||
WallpaperManager.getInstance(this).setWallpaperOffsets(it.windowToken, 0.5f, 0.5f)
|
||||
}
|
||||
|
||||
|
||||
//getSystemService(Context.INPUT_METHOD_SERVICE)
|
||||
// .castTo<InputMethodManager>()
|
||||
// .hideSoftInputFromWindow(currentFocus?.windowToken, 0)
|
||||
|
||||
//overridePendingTransition(R.anim.app_to_launcher_in, R.anim.app_to_launcher_out)
|
||||
}
|
||||
|
||||
private fun allowsLightSystemBars(): Boolean {
|
||||
val dimWallpaper = LauncherPreferences.instance.dimWallpaper
|
||||
val isDarkTheme = resources.getBoolean(R.bool.is_dark_theme)
|
||||
return !(isDarkTheme && dimWallpaper)
|
||||
}
|
||||
|
||||
private fun hasNotificationListenerPermission(): Boolean {
|
||||
val listeners = NotificationManagerCompat.getEnabledListenerPackages(this)
|
||||
for (listener in listeners) {
|
||||
if (listener == packageName) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private val themeListener = { key: String ->
|
||||
recreate()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
preferences.doOnPreferenceChange("is_light_wallpaper", action = themeListener)
|
||||
widgetHost.startListening()
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
ActivityStarter.pause()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
try {
|
||||
widgetHost.stopListening()
|
||||
} catch (e: NullPointerException) {
|
||||
Log.e("MM20", Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent?) {
|
||||
super.onNewIntent(intent)
|
||||
onBackPressed()
|
||||
adjustWidgetSpace()
|
||||
}
|
||||
|
||||
override fun onRequestPermissionsResult(
|
||||
requestCode: Int,
|
||||
permissions: Array<out String>,
|
||||
grantResults: IntArray
|
||||
) {
|
||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||
search(binding.searchBar.getSearchQuery())
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (data == null) return
|
||||
if (requestCode == REQUEST_PICK_APPWIDGET) {
|
||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (widgetId == -1) return
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
val appWidget = AppWidgetManager.getInstance(applicationContext)
|
||||
.getAppWidgetInfo(widgetId) ?: return
|
||||
if (appWidget.configure != null) {
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_CONFIGURE)
|
||||
intent.component = appWidget.configure
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
|
||||
} else {
|
||||
onActivityResult(REQUEST_BIND_APPWIDGET, Activity.RESULT_OK, data)
|
||||
}
|
||||
} else {
|
||||
widgetHost.deleteAppWidgetId(widgetId)
|
||||
}
|
||||
}
|
||||
if (requestCode == REQUEST_CREATE_APPWIDGET) {
|
||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (widgetId == -1) return
|
||||
val intent = Intent(AppWidgetManager.ACTION_APPWIDGET_BIND)
|
||||
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId)
|
||||
startActivityForResult(intent, REQUEST_BIND_APPWIDGET)
|
||||
}
|
||||
if (requestCode == REQUEST_BIND_APPWIDGET && resultCode == Activity.RESULT_OK) {
|
||||
val widgetId = data.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, -1)
|
||||
if (widgetId == -1) return
|
||||
val appWidget = AppWidgetManager.getInstance(applicationContext)
|
||||
.getAppWidgetInfo(widgetId) ?: return
|
||||
val widget = Widget(
|
||||
type = WidgetType.THIRD_PARTY,
|
||||
data = widgetId.toString(),
|
||||
height = appWidget.minHeight
|
||||
)
|
||||
val params = LinearLayout.LayoutParams(MATCH_PARENT, WRAP_CONTENT)
|
||||
params.topMargin = (8 * dp).roundToInt()
|
||||
val view = WidgetView(this)
|
||||
view.layoutParams = params
|
||||
if (view.setWidget(widget, widgetHost)) {
|
||||
view.editMode = true
|
||||
|
||||
binding.widgetList.addDragView(view, view.getDragHandle())
|
||||
view.onRemove = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
binding.widgetList.removeDragView(view)
|
||||
removeWidget(view.widget)
|
||||
}
|
||||
view.onResizeModeChange = {
|
||||
OneShotLayoutTransition.run(binding.widgetList)
|
||||
OneShotLayoutTransition.run(binding.widgetContainer)
|
||||
}
|
||||
widgets.add(widget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val scrollViewOnTouchListener: (View, MotionEvent) -> Boolean = onTouch@{ _, event ->
|
||||
when (event.action) {
|
||||
MotionEvent.ACTION_DOWN -> 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 + binding.container.translationY
|
||||
if (newTransY > 0 && newTransY < binding.searchBar.height) {
|
||||
binding.container.translationY = newTransY
|
||||
binding.searchBar.show()
|
||||
} else if (newTransY <= 0) {
|
||||
binding.container.translationY = 0f
|
||||
} else {
|
||||
binding.container.translationY = binding.searchBar.height.toFloat()
|
||||
|
||||
}
|
||||
windowBackgroundBlur =
|
||||
searchVisibility || newTransY > 0.6 * binding.searchBar.height
|
||||
|
||||
if (binding.container.translationY == 0f) return@onTouch false
|
||||
}
|
||||
}
|
||||
binding.scrollView.scrollY == binding.scrollContainer.height - binding.scrollView.height && searchVisibility -> {
|
||||
if (event.historySize > 0) {
|
||||
val dY = event.y - event.getHistoricalY(0)
|
||||
val newTransY = 0.4f * dY + binding.container.translationY
|
||||
|
||||
if (newTransY <= 0 && newTransY > -binding.searchBar.height) {
|
||||
binding.container.translationY = newTransY
|
||||
binding.searchBar.show()
|
||||
} else if (newTransY > 0) {
|
||||
binding.container.translationY = 0f
|
||||
} else {
|
||||
binding.container.translationY = -binding.searchBar.height.toFloat()
|
||||
}
|
||||
|
||||
if (binding.container.translationY == 0f) return@onTouch false
|
||||
}
|
||||
}
|
||||
else -> return@onTouch false
|
||||
}
|
||||
true
|
||||
}
|
||||
MotionEvent.ACTION_UP -> {
|
||||
if (binding.container.translationY >= binding.searchBar.height * 0.6) toggleSearch()
|
||||
if (binding.container.translationY <= -binding.searchBar.height) hideSearch()
|
||||
binding.container.animate().translationY(0f).setDuration(200).start()
|
||||
if (!searchVisibility && binding.scrollView.scrollY == 0) binding.searchBar.hide()
|
||||
false
|
||||
}
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
ActivityStarter.destroy()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val REQUEST_PICK_APPWIDGET = 4412
|
||||
const val REQUEST_CREATE_APPWIDGET = 4460
|
||||
const val REQUEST_BIND_APPWIDGET = 4124
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.*
|
||||
import de.mm20.launcher2.search.data.Application
|
||||
import de.mm20.launcher2.ui.databinding.ViewApplicationBinding
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
class ApplicationView : FrameLayout {
|
||||
|
||||
@ -27,7 +27,7 @@ class ApplicationView : FrameLayout {
|
||||
layoutTransition = LayoutTransition()
|
||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
binding.applicationCard.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
applications = viewModel.appResults
|
||||
applications.observe(context as AppCompatActivity, Observer<List<Application>> {
|
||||
visibility = if (it.isEmpty()) View.GONE else View.VISIBLE
|
||||
|
||||
@ -18,7 +18,7 @@ import androidx.lifecycle.Observer
|
||||
import de.mm20.launcher2.search.data.Calculator
|
||||
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||
import de.mm20.launcher2.ui.databinding.ViewCalculatorBinding
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.search.CalculatorItem
|
||||
|
||||
class CalculatorView : FrameLayout {
|
||||
@ -36,7 +36,7 @@ class CalculatorView : FrameLayout {
|
||||
private val binding = ViewCalculatorBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
init {
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
calculator = viewModel.calculatorResult
|
||||
calculator.observe(context as AppCompatActivity, Observer {
|
||||
if (it == null) visibility = View.GONE
|
||||
|
||||
@ -15,7 +15,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.search.data.CalendarEvent
|
||||
import de.mm20.launcher2.search.data.MissingPermission
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@ -40,7 +40,7 @@ class CalendarView : FrameLayout, KoinComponent {
|
||||
val card = findViewById<ViewGroup>(R.id.card)
|
||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
val list = findViewById<SearchListView>(R.id.list)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
calendarEvents = viewModel.calendarResults
|
||||
calendarEvents.observe(context as AppCompatActivity, {
|
||||
if (it == null) {
|
||||
|
||||
@ -15,7 +15,7 @@ import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.search.data.Contact
|
||||
import de.mm20.launcher2.search.data.MissingPermission
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@ -38,7 +38,7 @@ class ContactView : FrameLayout, KoinComponent {
|
||||
layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
val card = findViewById<ViewGroup>(R.id.card)
|
||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
contacts = viewModel.contactResults
|
||||
val list = findViewById<SearchListView>(R.id.list)
|
||||
contacts.observe(context as AppCompatActivity, {
|
||||
|
||||
@ -9,7 +9,7 @@ import android.widget.FrameLayout
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import de.mm20.launcher2.ui.databinding.ViewFavoritesBinding
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
class FavoritesView : FrameLayout {
|
||||
|
||||
@ -21,7 +21,7 @@ class FavoritesView : FrameLayout {
|
||||
|
||||
|
||||
init {
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
val favorites = viewModel.favorites
|
||||
val hide = viewModel.hideFavorites
|
||||
favorites.observe(context as AppCompatActivity) {
|
||||
|
||||
@ -14,7 +14,7 @@ import de.mm20.launcher2.permissions.PermissionsManager
|
||||
import de.mm20.launcher2.search.data.File
|
||||
import de.mm20.launcher2.search.data.MissingPermission
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.legacy.search.SearchListView
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.get
|
||||
@ -38,7 +38,7 @@ class FileView : FrameLayout, KoinComponent {
|
||||
val card = findViewById<ViewGroup>(R.id.card)
|
||||
card.layoutTransition.enableTransitionType(LayoutTransition.CHANGING)
|
||||
val list = findViewById<SearchListView>(R.id.list)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
files = viewModel.fileResults
|
||||
files.observe(context as AppCompatActivity, {
|
||||
if (it == null) {
|
||||
|
||||
@ -1,256 +1,50 @@
|
||||
package de.mm20.launcher2.ui.legacy.component
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorSet
|
||||
import android.animation.ObjectAnimator
|
||||
import android.content.Context
|
||||
import android.graphics.Color
|
||||
import android.text.Editable
|
||||
import android.text.TextWatcher
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.animation.AccelerateInterpolator
|
||||
import android.view.animation.DecelerateInterpolator
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.postDelayed
|
||||
import com.airbnb.lottie.LottieCompositionFactory
|
||||
import com.airbnb.lottie.LottieDrawable
|
||||
import de.mm20.launcher2.ktx.dp
|
||||
import de.mm20.launcher2.preferences.LauncherPreferences
|
||||
import de.mm20.launcher2.preferences.SearchStyles
|
||||
import de.mm20.launcher2.transition.ChangingLayoutTransition
|
||||
import android.util.Log
|
||||
import android.view.MotionEvent
|
||||
import android.widget.FrameLayout
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.databinding.ViewSearchBarBinding
|
||||
import de.mm20.launcher2.ui.legacy.view.LauncherCardView
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBar
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchBarLevel
|
||||
|
||||
class SearchBar @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.materialCardViewStyle
|
||||
) : LauncherCardView(context, attrs, defStyleAttr) {
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null,
|
||||
defStyleAttr: Int = R.attr.materialCardViewStyle
|
||||
) : FrameLayout(context, attrs, defStyleAttr) {
|
||||
|
||||
private var raised = false
|
||||
private var visible = true
|
||||
private var currentAnimator: Animator? = null
|
||||
var level: SearchBarLevel = SearchBarLevel.Resting
|
||||
set(value) {
|
||||
levelState.value = value
|
||||
field = value
|
||||
}
|
||||
|
||||
private val rightDrawable = LottieDrawable().apply {
|
||||
composition = LottieCompositionFactory.fromRawResSync(context, R.raw.ic_menu_to_clear).value
|
||||
repeatMode = LottieDrawable.REVERSE
|
||||
}
|
||||
private val levelState = MutableLiveData(level)
|
||||
|
||||
private val binding = ViewSearchBarBinding.inflate(LayoutInflater.from(context), this)
|
||||
var onFocus: (() -> Unit)? = null
|
||||
|
||||
init {
|
||||
binding.overflowMenu.setImageDrawable(rightDrawable)
|
||||
binding.searchEdit.addTextChangedListener(object : TextWatcher {
|
||||
override fun afterTextChanged(s: Editable?) {
|
||||
val text = binding.searchEdit.text.toString()
|
||||
onSearchQueryChanged?.invoke(binding.searchEdit.text.toString())
|
||||
if (text.isEmpty()) {
|
||||
if (rightDrawable.frame > rightDrawable.minFrame.toInt()) {
|
||||
rightDrawable.speed = -1f
|
||||
rightDrawable.resumeAnimation()
|
||||
}
|
||||
} else {
|
||||
if (rightDrawable.frame < rightDrawable.maxFrame.toInt()) {
|
||||
rightDrawable.speed = 1f
|
||||
rightDrawable.resumeAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
|
||||
}
|
||||
|
||||
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
binding.overflowMenu.setOnClickListener {
|
||||
if (getSearchQuery().isEmpty()) onRightIconClick?.invoke(it)
|
||||
else (setSearchQuery(""))
|
||||
}
|
||||
|
||||
postDelayed(1) {
|
||||
hide()
|
||||
}
|
||||
|
||||
layoutTransition = ChangingLayoutTransition()
|
||||
}
|
||||
|
||||
fun setRightIcon(iconRes: Int) {
|
||||
binding.overflowMenu.setImageResource(iconRes)
|
||||
}
|
||||
|
||||
var onSearchQueryChanged: ((String) -> Unit)? = null
|
||||
var onRightIconClick: ((View) -> Unit)? = null
|
||||
|
||||
override fun setOnTouchListener(l: OnTouchListener?) {
|
||||
binding.searchEdit.setOnTouchListener(l)
|
||||
}
|
||||
|
||||
fun setSearchQuery(text: String) {
|
||||
binding.searchEdit.setText(text)
|
||||
}
|
||||
|
||||
fun getSearchQuery(): String {
|
||||
return binding.searchEdit.text.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Elevates the search bar to be higher than the other cards
|
||||
*/
|
||||
fun raise() {
|
||||
if (raised) return
|
||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
||||
currentAnimator = AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", elevation, 7 * dp).apply {
|
||||
interpolator = AccelerateInterpolator(3f)
|
||||
},
|
||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0xFF).apply {
|
||||
interpolator = DecelerateInterpolator(3f)
|
||||
}
|
||||
)
|
||||
}
|
||||
currentAnimator?.start()
|
||||
raised = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the search bar back down to the other cards niveau
|
||||
*/
|
||||
|
||||
fun drop() {
|
||||
if (!raised) return
|
||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
||||
currentAnimator = AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
|
||||
interpolator = DecelerateInterpolator(3f)
|
||||
},
|
||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
|
||||
interpolator = AccelerateInterpolator(3f)
|
||||
}
|
||||
)
|
||||
}
|
||||
currentAnimator?.start()
|
||||
raised = false
|
||||
}
|
||||
|
||||
fun show() {
|
||||
if (visible) return
|
||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
||||
currentAnimator = getShowAnimator()
|
||||
currentAnimator?.start()
|
||||
visible = true
|
||||
}
|
||||
|
||||
fun hide() {
|
||||
if (!visible) return
|
||||
currentAnimator?.takeIf { it.isStarted }?.end()
|
||||
currentAnimator = getHideAnimator()
|
||||
currentAnimator?.start()
|
||||
visible = false
|
||||
}
|
||||
|
||||
fun getWebSearchView(): View {
|
||||
return binding.webSearchView
|
||||
}
|
||||
|
||||
private fun getHideAnimator(): AnimatorSet {
|
||||
val searchStyle = LauncherPreferences.instance.searchStyle
|
||||
return when (searchStyle) {
|
||||
SearchStyles.NO_BG -> {
|
||||
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
|
||||
val cardElevation = resources.getDimension(R.dimen.card_elevation)
|
||||
val shadowY = resources.getDimension(R.dimen.elevation_shadow_1dp_y)
|
||||
val shadowR = resources.getDimension(R.dimen.elevation_shadow_1dp_radius)
|
||||
val shadowC = Color.argb(66, 0, 0, 0)
|
||||
binding.searchEdit.setShadowLayer(shadowR, 0f, shadowY, shadowC)
|
||||
AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", 0).apply {
|
||||
interpolator = AccelerateInterpolator(3f)
|
||||
},
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", -elevation).apply {
|
||||
interpolator = DecelerateInterpolator(3f)
|
||||
duration = 150
|
||||
},
|
||||
ObjectAnimator.ofArgb(binding.searchEdit, "hintTextColor", binding.searchEdit.hintTextColors.defaultColor, Color.WHITE),
|
||||
ObjectAnimator.ofArgb(binding.searchIcon, "colorFilter", iconColor, Color.WHITE),
|
||||
ObjectAnimator.ofArgb(binding.overflowMenu, "colorFilter", iconColor, Color.WHITE),
|
||||
ObjectAnimator.ofFloat(binding.searchIcon, "alpha", 1f),
|
||||
ObjectAnimator.ofFloat(binding.overflowMenu, "alpha", 1f),
|
||||
ObjectAnimator.ofFloat(binding.searchIcon, "elevation", cardElevation),
|
||||
ObjectAnimator.ofFloat(binding.overflowMenu, "elevation", cardElevation)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Solid style
|
||||
SearchStyles.SOLID -> {
|
||||
AnimatorSet()
|
||||
}
|
||||
// Hidden style
|
||||
else -> {
|
||||
AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 0f)
|
||||
val view = ComposeView(context)
|
||||
view.setContent {
|
||||
val level by levelState.observeAsState(SearchBarLevel.Resting)
|
||||
LegacyLauncherTheme {
|
||||
Box(contentAlignment = Alignment.TopCenter) {
|
||||
SearchBar(
|
||||
level,
|
||||
onFocus = { onFocus?.invoke() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
addView(view)
|
||||
}
|
||||
|
||||
private fun getShowAnimator(): AnimatorSet? {
|
||||
return when (LauncherPreferences.instance.searchStyle) {
|
||||
// Transparent style
|
||||
SearchStyles.NO_BG -> {
|
||||
val hint = ContextCompat.getColor(context, R.color.text_color_primary_disabled)
|
||||
val iconAttrs = context.obtainStyledAttributes(R.style.LauncherTheme_IconStyle, intArrayOf(android.R.attr.alpha))
|
||||
val iconAlpha = iconAttrs.getFloat(0, 0f)
|
||||
iconAttrs.recycle()
|
||||
val iconColor = ContextCompat.getColor(context, R.color.icon_color)
|
||||
binding.searchEdit.setShadowLayer(0f, 0f, 0f, 0)
|
||||
AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "translationZ", 0f).apply {
|
||||
interpolator = AccelerateInterpolator(3f)
|
||||
},
|
||||
ObjectAnimator.ofInt(this@SearchBar, "backgroundOpacity", LauncherPreferences.instance.cardOpacity).apply {
|
||||
interpolator = DecelerateInterpolator(3f)
|
||||
},
|
||||
ObjectAnimator.ofArgb(binding.searchEdit, "hintTextColor", Color.WHITE, hint),
|
||||
ObjectAnimator.ofArgb(binding.searchIcon, "colorFilter", Color.WHITE, iconColor),
|
||||
ObjectAnimator.ofArgb(binding.overflowMenu, "colorFilter", Color.WHITE, iconColor),
|
||||
ObjectAnimator.ofFloat(binding.searchIcon, "alpha", iconAlpha),
|
||||
ObjectAnimator.ofFloat(binding.overflowMenu, "alpha", iconAlpha),
|
||||
ObjectAnimator.ofFloat(binding.searchIcon, "elevation", 0f),
|
||||
ObjectAnimator.ofFloat(binding.overflowMenu, "elevation", 0f)
|
||||
)
|
||||
}
|
||||
}
|
||||
// Solid style
|
||||
SearchStyles.SOLID -> {
|
||||
null
|
||||
}
|
||||
// Hidden style
|
||||
else -> {
|
||||
AnimatorSet().apply {
|
||||
duration = 200
|
||||
playTogether(
|
||||
ObjectAnimator.ofFloat(this@SearchBar, "alpha", 1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -18,7 +18,7 @@ import androidx.lifecycle.Observer
|
||||
import de.mm20.launcher2.search.data.UnitConverter
|
||||
import de.mm20.launcher2.ui.LegacyLauncherTheme
|
||||
import de.mm20.launcher2.ui.databinding.ViewUnitconverterBinding
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.search.UnitConverterItem
|
||||
|
||||
class UnitConverterView : FrameLayout {
|
||||
@ -36,7 +36,7 @@ class UnitConverterView : FrameLayout {
|
||||
private val binding = ViewUnitconverterBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
init {
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
unitConverter = viewModel.unitConverterResult
|
||||
unitConverter.observe(context as AppCompatActivity, Observer {
|
||||
if (it == null) visibility = View.GONE
|
||||
|
||||
@ -2,7 +2,6 @@ package de.mm20.launcher2.ui.legacy.component
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
@ -22,7 +21,7 @@ import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||
import de.mm20.launcher2.search.data.Websearch
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.databinding.ViewWebsearchBinding
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
|
||||
@ -40,7 +39,7 @@ class WebSearchView : FrameLayout {
|
||||
private val binding = ViewWebsearchBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
|
||||
init {
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
websearches = viewModel.websearchResults
|
||||
websearches.observe(context as AppCompatActivity, Observer {
|
||||
updateWebsearches(it)
|
||||
|
||||
@ -12,7 +12,7 @@ import androidx.lifecycle.Observer
|
||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||
import de.mm20.launcher2.search.data.Website
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||
|
||||
class WebsiteView : FrameLayout {
|
||||
@ -35,7 +35,7 @@ class WebsiteView : FrameLayout {
|
||||
val card = findViewById<ViewGroup>(R.id.card)
|
||||
websiteView.layoutParams = params
|
||||
card.addView(websiteView)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
website = viewModel.websiteResult
|
||||
website.observe(context as AppCompatActivity, Observer {
|
||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||
|
||||
@ -11,7 +11,7 @@ import androidx.lifecycle.LiveData
|
||||
import de.mm20.launcher2.legacy.helper.ActivityStarter
|
||||
import de.mm20.launcher2.search.data.Wikipedia
|
||||
import de.mm20.launcher2.ui.R
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import de.mm20.launcher2.ui.legacy.searchable.SearchableView
|
||||
|
||||
class WikipediaView : FrameLayout {
|
||||
@ -34,7 +34,7 @@ class WikipediaView : FrameLayout {
|
||||
val card = findViewById<ViewGroup>(R.id.card)
|
||||
websiteView.layoutParams = params
|
||||
card.addView(websiteView)
|
||||
val viewModel: SearchViewModel by (context as AppCompatActivity).viewModels()
|
||||
val viewModel: SearchVM by (context as AppCompatActivity).viewModels()
|
||||
wikipedia = viewModel.wikipediaResult
|
||||
wikipedia.observe(context as AppCompatActivity, {
|
||||
visibility = if (it == null) View.GONE else View.VISIBLE
|
||||
|
||||
@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
@Composable
|
||||
fun applicationResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
val apps by viewModel.appResults.observeAsState(emptyList())
|
||||
return {
|
||||
SearchableGrid(items = apps, listState = it)
|
||||
|
||||
@ -8,11 +8,11 @@ import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.component.SectionDivider
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
@Composable
|
||||
fun calculatorItem(): LazyListScope.() -> Unit {
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
val calculator by viewModel.calculatorResult.observeAsState(null)
|
||||
return {
|
||||
calculator?.let {
|
||||
|
||||
@ -6,11 +6,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
@Composable
|
||||
fun favoriteResults(): LazyListScope.(listState: LazyListState) -> Unit {
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
|
||||
val favorites by viewModel.favorites.observeAsState(emptyList())
|
||||
return {
|
||||
|
||||
@ -5,11 +5,11 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
|
||||
@Composable
|
||||
fun fileResults(): LazyListScope.() -> Unit {
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
val files by viewModel.fileResults.observeAsState(emptyList())
|
||||
return {
|
||||
files?.let { SearchableList(items = it) }
|
||||
|
||||
@ -7,12 +7,12 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.unit.dp
|
||||
import de.mm20.launcher2.ui.component.SectionDivider
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchViewModel
|
||||
import de.mm20.launcher2.ui.launcher.search.SearchVM
|
||||
import org.koin.androidx.compose.viewModel
|
||||
|
||||
@Composable
|
||||
fun wikipediaResult(): LazyListScope.() -> Unit {
|
||||
val viewModel: SearchViewModel by viewModel()
|
||||
val viewModel: SearchVM by viewModel()
|
||||
val wikipedia by viewModel.wikipediaResult.observeAsState()
|
||||
return {
|
||||
wikipedia?.let {
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout 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"
|
||||
android:id="@+id/rootView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@ -18,168 +16,11 @@
|
||||
android:layout_height="96dp"
|
||||
android:layout_gravity="bottom|center_horizontal" />
|
||||
|
||||
<FrameLayout
|
||||
<de.mm20.launcher2.ui.launcher.LauncherScaffoldView
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
|
||||
<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:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingTop="56dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/searchContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:visibility="gone">
|
||||
|
||||
<View
|
||||
android:id="@+id/webSearchViewSpacer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.FavoritesView
|
||||
android:id="@+id/favoritesView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.ApplicationView
|
||||
android:id="@+id/applicationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.CalculatorView
|
||||
android:id="@+id/calculatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.ContactView
|
||||
android:id="@+id/contactView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.CalendarView
|
||||
android:id="@+id/calendarView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.UnitConverterView
|
||||
android:id="@+id/unitConverterView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.FileView
|
||||
android:id="@+id/fileView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.WebsiteView
|
||||
android:id="@+id/websiteView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.WikipediaView
|
||||
android:id="@+id/wikipediaView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/widgetContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="8dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
|
||||
android:id="@+id/clockWidget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.jmedeisis.draglinearlayout.DragLinearLayout
|
||||
android:animateLayoutChanges="true"
|
||||
android:id="@+id/widgetList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Widgets will be added here -->
|
||||
|
||||
</com.jmedeisis.draglinearlayout.DragLinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fabEditWidget"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/menu_edit_widgets"
|
||||
app:icon="@drawable/ic_edit" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
</androidx.core.widget.NestedScrollView>
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.SearchBar
|
||||
android:id="@+id/searchBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:descendantFocusability="beforeDescendants"
|
||||
android:focusableInTouchMode="true"
|
||||
app:cardElevation="@dimen/card_elevation" />
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/editWidgetToolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:elevation="4dp"
|
||||
android:translationY="-56dp"
|
||||
android:visibility="gone"
|
||||
style="@style/Widget.Material3.Toolbar.Surface"
|
||||
app:title="@string/menu_edit_widgets" />
|
||||
</FrameLayout>
|
||||
android:fitsSystemWindows="true" />
|
||||
|
||||
<View
|
||||
android:id="@+id/activityStartOverlay"
|
||||
|
||||
61
ui/src/main/res/layout/view_launcher_scaffold.xml
Normal file
61
ui/src/main/res/layout/view_launcher_scaffold.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?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:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical"
|
||||
android:padding="8dp">
|
||||
|
||||
<de.mm20.launcher2.ui.launcher.search.SearchView
|
||||
android:id="@+id/searchContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
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.legacy.component.SearchBar
|
||||
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>
|
||||
68
ui/src/main/res/layout/view_search.xml
Normal file
68
ui/src/main/res/layout/view_search.xml
Normal file
@ -0,0 +1,68 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
tools:parentTag="android.widget.LinearLayout"
|
||||
tools:layout_width="match_parent"
|
||||
tools:layout_height="wrap_content">
|
||||
|
||||
|
||||
<View
|
||||
android:id="@+id/webSearchViewSpacer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.FavoritesView
|
||||
android:id="@+id/favoritesView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.ApplicationView
|
||||
android:id="@+id/applicationView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.CalculatorView
|
||||
android:id="@+id/calculatorView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.ContactView
|
||||
android:id="@+id/contactView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.CalendarView
|
||||
android:id="@+id/calendarView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.UnitConverterView
|
||||
android:id="@+id/unitConverterView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.FileView
|
||||
android:id="@+id/fileView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.WebsiteView
|
||||
android:id="@+id/websiteView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.component.WikipediaView
|
||||
android:id="@+id/wikipediaView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="gone" />
|
||||
</merge>
|
||||
35
ui/src/main/res/layout/view_widgets.xml
Normal file
35
ui/src/main/res/layout/view_widgets.xml
Normal file
@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<merge xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
tools:layout_height="wrap_content"
|
||||
tools:layout_width="match_parent">
|
||||
|
||||
<de.mm20.launcher2.ui.legacy.widget.ClockWidget
|
||||
android:id="@+id/clockWidget"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
<com.jmedeisis.draglinearlayout.DragLinearLayout
|
||||
android:id="@+id/widgetList"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:animateLayoutChanges="true"
|
||||
android:clipChildren="false"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="vertical">
|
||||
|
||||
<!-- Widgets will be added here -->
|
||||
|
||||
</com.jmedeisis.draglinearlayout.DragLinearLayout>
|
||||
|
||||
<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
|
||||
android:id="@+id/fabEditWidget"
|
||||
app:icon="@drawable/ic_edit"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/menu_edit_widgets" />
|
||||
</merge>
|
||||
Loading…
x
Reference in New Issue
Block a user