Unclutter this mess we call LauncherActivity

also migrate search bar to Jetpack Compose
This commit is contained in:
MM20 2022-01-25 17:55:57 +01:00
parent 0447d3072d
commit f701e90c47
No known key found for this signature in database
GPG Key ID: 0B61A8F2DEAFA389
37 changed files with 1471 additions and 1183 deletions

View File

@ -28,6 +28,7 @@ android {
versionCode = versionCodeDate()
versionName = "1.3.0"
multiDexEnabled = true
signingConfig = signingConfigs.getByName("debug")
}
buildTypes {
release {

View File

@ -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

View File

@ -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() {

View File

@ -12,6 +12,7 @@ android {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
signingConfig = signingConfigs.getByName("debug")
}
buildTypes {

View File

@ -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>

View File

@ -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
)
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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
}

View File

@ -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) {

View File

@ -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()
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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

View File

@ -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) {

View File

@ -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, {

View File

@ -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) {

View File

@ -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) {

View File

@ -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)
)
}
}
}
}
}

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 {

View File

@ -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 {

View File

@ -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) }

View File

@ -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 {

View File

@ -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"

View 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>

View 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>

View 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>