FloatingSearchBar
This commit is contained in:
@@ -152,7 +152,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
var cachedSortMode: Int = -1
|
||||
private val cache = mutableListOf<Int>()
|
||||
|
||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> {
|
||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> = coroutineScope { withContext(Dispatchers.IO) {
|
||||
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
||||
cachedQuery = null
|
||||
cache.clear()
|
||||
@@ -179,8 +179,8 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
channel.close()
|
||||
}
|
||||
|
||||
return Pair(channel, cache.size)
|
||||
}
|
||||
Pair(channel, cache.size)
|
||||
} }
|
||||
|
||||
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
||||
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
||||
|
||||
@@ -23,22 +23,33 @@ import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.viewModels
|
||||
import androidx.compose.foundation.gestures.awaitFirstDown
|
||||
import androidx.compose.foundation.gestures.forEachGesture
|
||||
import androidx.compose.foundation.gestures.scrollable
|
||||
import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.updateTransition
|
||||
import androidx.compose.foundation.MutatePriority
|
||||
import androidx.compose.foundation.ScrollState
|
||||
import androidx.compose.foundation.gestures.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.CircularProgressIndicator
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.livedata.observeAsState
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.input.pointer.PointerEventPass
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.pointerInteropFilter
|
||||
@@ -47,6 +58,10 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.WindowCompat
|
||||
import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import com.google.accompanist.systemuicontroller.rememberSystemUiController
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
@@ -68,6 +83,11 @@ import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||
import xyz.quaver.pupil.util.*
|
||||
import kotlin.math.*
|
||||
|
||||
private enum class NavigationIconState {
|
||||
MENU,
|
||||
ARROW
|
||||
}
|
||||
|
||||
class MainActivity : ComponentActivity(), DIAware {
|
||||
override val di by closestDI()
|
||||
|
||||
@@ -75,37 +95,36 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
setContent {
|
||||
val source: Source? by model.source.observeAsState(null)
|
||||
val loading: Boolean by model.loading.observeAsState(false)
|
||||
|
||||
var query by remember { mutableStateOf("") }
|
||||
|
||||
var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
||||
|
||||
val lazyListState = rememberLazyListState()
|
||||
|
||||
val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
|
||||
var searchBarOffset by remember { mutableStateOf(0) }
|
||||
|
||||
LaunchedEffect(lazyListState) {
|
||||
var lastOffset = 0
|
||||
|
||||
snapshotFlow { lazyListState.firstVisibleItemScrollOffset }
|
||||
.distinctUntilChanged()
|
||||
.collect { newOffset ->
|
||||
val dy = newOffset - lastOffset
|
||||
lastOffset = newOffset
|
||||
|
||||
if (abs(dy) < searchBarHeight)
|
||||
searchBarOffset = (searchBarOffset-dy).coerceIn(-searchBarHeight, 0)
|
||||
}
|
||||
}
|
||||
|
||||
PupilTheme {
|
||||
val source: Source? by model.source.observeAsState(null)
|
||||
|
||||
var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
||||
var isFabVisible by remember { mutableStateOf(true) }
|
||||
|
||||
val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
|
||||
var searchBarOffset by remember { mutableStateOf(0) }
|
||||
|
||||
val navigationIcon = remember { DrawerArrowDrawable(this) }
|
||||
var navigationIconState by remember { mutableStateOf(NavigationIconState.MENU) }
|
||||
val navigationIconTransition = updateTransition(navigationIconState, label = "navigationIconTransition")
|
||||
val navigationIconProgress by navigationIconTransition.animateFloat(
|
||||
label = "navigationIconProgress"
|
||||
) { state ->
|
||||
when (state) {
|
||||
NavigationIconState.MENU -> 0f
|
||||
NavigationIconState.ARROW -> 1f
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(navigationIconProgress) {
|
||||
navigationIcon.progress = navigationIconProgress
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
floatingActionButton = {
|
||||
MultipleFloatingActionButton(
|
||||
@@ -127,6 +146,7 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
stringResource(R.string.main_open_gallery_by_id)
|
||||
),
|
||||
),
|
||||
visible = isFabVisible,
|
||||
targetState = isFabExpanded,
|
||||
onStateChanged = {
|
||||
isFabExpanded = it
|
||||
@@ -136,8 +156,24 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
LazyColumn(
|
||||
Modifier.fillMaxSize(),
|
||||
state = lazyListState,
|
||||
Modifier
|
||||
.fillMaxSize()
|
||||
.nestedScroll(object : NestedScrollConnection {
|
||||
override fun onPreScroll(
|
||||
available: Offset,
|
||||
source: NestedScrollSource
|
||||
): Offset {
|
||||
searchBarOffset =
|
||||
(searchBarOffset + available.y.roundToInt()).coerceIn(
|
||||
-searchBarHeight,
|
||||
0
|
||||
)
|
||||
|
||||
isFabVisible = available.y > 0f
|
||||
|
||||
return Offset.Zero
|
||||
}
|
||||
}),
|
||||
contentPadding = PaddingValues(0.dp, 56.dp, 0.dp, 0.dp)
|
||||
) {
|
||||
items(model.searchResults, key = { it.itemID }) { itemInfo ->
|
||||
@@ -159,25 +195,36 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
}
|
||||
}
|
||||
|
||||
if (loading)
|
||||
if (model.loading)
|
||||
CircularProgressIndicator(Modifier.align(Alignment.Center))
|
||||
|
||||
FloatingSearchBar(
|
||||
modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
query = model.query,
|
||||
onQueryChange = { model.query = it },
|
||||
navigationIcon = {
|
||||
Icon(
|
||||
painter = rememberDrawablePainter(navigationIcon),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
Icon(
|
||||
Icons.Default.Sort,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
Icon(
|
||||
Icons.Default.Settings,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(24.dp)
|
||||
modifier = Modifier.size(24.dp),
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
},
|
||||
onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
|
||||
onTextFieldUnfocused = { navigationIconState = NavigationIconState.MENU; model.resetAndQuery() }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,23 +19,31 @@
|
||||
package xyz.quaver.pupil.ui.composable
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.KeyboardManager
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
@@ -43,16 +51,26 @@ fun FloatingSearchBar(
|
||||
modifier: Modifier = Modifier,
|
||||
query: String = "",
|
||||
onQueryChange: (String) -> Unit = { },
|
||||
navigationIcon: @Composable () -> Unit = {
|
||||
Icon(
|
||||
Icons.Default.Menu,
|
||||
modifier = Modifier.size(24.dp),
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
},
|
||||
actions: @Composable RowScope.() -> Unit = { }
|
||||
navigationIcon: @Composable () -> Unit = { },
|
||||
actions: @Composable RowScope.() -> Unit = { },
|
||||
onTextFieldFocused: () -> Unit = { },
|
||||
onTextFieldUnfocused: () -> Unit = { }
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
|
||||
DisposableEffect(context) {
|
||||
val keyboardManager = KeyboardManager(context)
|
||||
keyboardManager.attachKeyboardDismissListener {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
onDispose {
|
||||
keyboardManager.release()
|
||||
}
|
||||
}
|
||||
|
||||
Card(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@@ -68,19 +86,46 @@ fun FloatingSearchBar(
|
||||
navigationIcon()
|
||||
|
||||
BasicTextField(
|
||||
modifier = Modifier.weight(1f).padding(16.dp, 0.dp),
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(16.dp, 0.dp)
|
||||
.onFocusChanged {
|
||||
if (it.isFocused) onTextFieldFocused()
|
||||
else onTextFieldUnfocused()
|
||||
|
||||
isFocused = it.isFocused
|
||||
},
|
||||
value = query,
|
||||
onValueChange = onQueryChange,
|
||||
singleLine = true,
|
||||
cursorBrush = SolidColor(MaterialTheme.colors.primary),
|
||||
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
focusManager.clearFocus()
|
||||
}
|
||||
),
|
||||
decorationBox = { innerTextField ->
|
||||
if (query.isEmpty())
|
||||
Text(
|
||||
stringResource(R.string.search_hint),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Box(Modifier.weight(1f)) {
|
||||
if (query.isEmpty())
|
||||
Text(
|
||||
stringResource(R.string.search_hint),
|
||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
innerTextField()
|
||||
}
|
||||
|
||||
innerTextField()
|
||||
if (query.isNotEmpty() && isFocused)
|
||||
Icon(
|
||||
Icons.Default.Close,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f),
|
||||
modifier = Modifier.clickable { onQueryChange("") }
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.Modifier.Companion.any
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.rotate
|
||||
import androidx.compose.ui.draw.scale
|
||||
@@ -28,6 +29,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAll
|
||||
|
||||
enum class FloatingActionButtonState(private val isExpanded: Boolean) {
|
||||
COLLAPSED(false), EXPANDED(true);
|
||||
@@ -112,6 +114,7 @@ private class FloatingActionButtonItemProvider : PreviewParameterProvider<SubFab
|
||||
fun MultipleFloatingActionButton(
|
||||
@PreviewParameter(provider = FloatingActionButtonItemProvider::class) items: List<SubFabItem>,
|
||||
fabIcon: ImageVector = Icons.Default.Add,
|
||||
visible: Boolean = true,
|
||||
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
|
||||
onStateChanged: ((FloatingActionButtonState) -> Unit)? = null
|
||||
) {
|
||||
@@ -131,10 +134,14 @@ fun MultipleFloatingActionButton(
|
||||
}
|
||||
}
|
||||
|
||||
if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
|
||||
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
var allCollapsed = true
|
||||
|
||||
items.forEachIndexed { index, item ->
|
||||
val delay = when (targetState) {
|
||||
FloatingActionButtonState.COLLAPSED -> index
|
||||
@@ -207,12 +214,26 @@ fun MultipleFloatingActionButton(
|
||||
) {
|
||||
item.onClick?.invoke(it)
|
||||
}
|
||||
|
||||
if (buttonScale != 0f) allCollapsed = false
|
||||
}
|
||||
|
||||
FloatingActionButton(onClick = {
|
||||
onStateChanged?.invoke(!targetState)
|
||||
}) {
|
||||
Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null)
|
||||
val visibilityTransition = updateTransition(targetState = visible || !allCollapsed, label = "visible")
|
||||
|
||||
val scale by visibilityTransition.animateFloat(
|
||||
label = "main FAB scale"
|
||||
) { state ->
|
||||
if (state) 1f else 0f
|
||||
}
|
||||
|
||||
if (scale > 0f)
|
||||
FloatingActionButton(
|
||||
modifier = Modifier.scale(scale),
|
||||
onClick = {
|
||||
onStateChanged?.invoke(!targetState)
|
||||
}
|
||||
) {
|
||||
Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,13 +20,18 @@ package xyz.quaver.pupil.ui.viewmodel
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Application
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.lifecycle.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.sources.*
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
@@ -39,15 +44,17 @@ import kotlin.random.Random
|
||||
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
override val di by closestDI()
|
||||
|
||||
private val logger = newLogger(LoggerFactory.default)
|
||||
|
||||
val searchResults = mutableStateListOf<ItemInfo>()
|
||||
|
||||
private val _loading = MutableLiveData(false)
|
||||
val loading = _loading as LiveData<Boolean>
|
||||
var loading by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
private var queryJob: Job? = null
|
||||
private var suggestionJob: Job? = null
|
||||
|
||||
val query = MutableLiveData<String>()
|
||||
var query by mutableStateOf("")
|
||||
private val queryStack = mutableListOf<String>()
|
||||
|
||||
private val defaultSourceFactory: (String) -> Source = {
|
||||
@@ -90,11 +97,11 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
sortModeIndex.value = 0
|
||||
}
|
||||
|
||||
setQueryAndSearch()
|
||||
query = ""
|
||||
resetAndQuery()
|
||||
}
|
||||
|
||||
fun setQueryAndSearch(query: String = "") {
|
||||
this.query.value = query
|
||||
fun resetAndQuery() {
|
||||
queryStack.add(query)
|
||||
setPage(1)
|
||||
|
||||
@@ -126,29 +133,26 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
suggestionJob?.cancel()
|
||||
queryJob?.cancel()
|
||||
|
||||
_loading.value = true
|
||||
loading = true
|
||||
searchResults.clear()
|
||||
|
||||
queryJob = viewModelScope.launch {
|
||||
launch(Dispatchers.Default) {
|
||||
val channel = withContext(Dispatchers.IO) {
|
||||
val (channel, count) = source.search(
|
||||
query.value ?: "",
|
||||
(currentPage - 1) * perPage until currentPage * perPage,
|
||||
sortModeIndex
|
||||
)
|
||||
val (channel, count) = source.search(
|
||||
query,
|
||||
(currentPage - 1) * perPage until currentPage * perPage,
|
||||
sortModeIndex
|
||||
)
|
||||
|
||||
totalItems.postValue(count)
|
||||
logger.info { count.toString() }
|
||||
|
||||
channel
|
||||
}
|
||||
totalItems.postValue(count)
|
||||
|
||||
for (result in channel) {
|
||||
yield()
|
||||
searchResults.add(result)
|
||||
}
|
||||
|
||||
_loading.postValue(false)
|
||||
for (result in channel) {
|
||||
yield()
|
||||
searchResults.add(result)
|
||||
}
|
||||
|
||||
loading = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,7 +169,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
viewModelScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
_source.value?.search(
|
||||
query.value + Preferences["default_query", ""],
|
||||
query + Preferences["default_query", ""],
|
||||
random .. random,
|
||||
sortModeIndex.value!!
|
||||
)?.first?.receive()
|
||||
@@ -182,7 +186,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
@SuppressLint("NullSafeMutableLiveData")
|
||||
_suggestions.value = withContext(Dispatchers.IO) {
|
||||
kotlin.runCatching {
|
||||
_source.value!!.suggestion(query.value!!)
|
||||
_source.value!!.suggestion(query)
|
||||
}.getOrElse { emptyList() }
|
||||
}
|
||||
}
|
||||
@@ -195,7 +199,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
if (queryStack.removeLastOrNull() == null || queryStack.isEmpty())
|
||||
return false
|
||||
|
||||
setQueryAndSearch(queryStack.removeLast())
|
||||
query = queryStack.removeLast()
|
||||
resetAndQuery()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
67
app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt
Normal file
67
app/src/main/java/xyz/quaver/pupil/util/KeyboardManager.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2021 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
|
||||
//https://stackoverflow.com/questions/68389802/how-to-clear-textfield-focus-when-closing-the-keyboard-and-prevent-two-back-pres
|
||||
class KeyboardManager(context: Context) {
|
||||
private val activity = context as Activity
|
||||
private var keyboardDismissListener: KeyboardDismissListener? = null
|
||||
|
||||
private abstract class KeyboardDismissListener(
|
||||
private val rootView: View,
|
||||
private val onKeyboardDismiss: () -> Unit
|
||||
) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
private var isKeyboardClosed: Boolean = false
|
||||
override fun onGlobalLayout() {
|
||||
val r = Rect()
|
||||
rootView.getWindowVisibleDisplayFrame(r)
|
||||
val screenHeight = rootView.rootView.height
|
||||
val keypadHeight = screenHeight - r.bottom
|
||||
if (keypadHeight > screenHeight * 0.15) {
|
||||
// 0.15 ratio is right enough to determine keypad height.
|
||||
isKeyboardClosed = false
|
||||
} else if (!isKeyboardClosed) {
|
||||
isKeyboardClosed = true
|
||||
onKeyboardDismiss.invoke()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun attachKeyboardDismissListener(onKeyboardDismiss: () -> Unit) {
|
||||
val rootView = activity.findViewById<View>(android.R.id.content)
|
||||
keyboardDismissListener = object : KeyboardDismissListener(rootView, onKeyboardDismiss) {}
|
||||
keyboardDismissListener?.let {
|
||||
rootView.viewTreeObserver.addOnGlobalLayoutListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
val rootView = activity.findViewById<View>(android.R.id.content)
|
||||
keyboardDismissListener?.let {
|
||||
rootView.viewTreeObserver.removeOnGlobalLayoutListener(it)
|
||||
}
|
||||
keyboardDismissListener = null
|
||||
}
|
||||
}
|
||||
@@ -25,14 +25,18 @@ import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.toAndroidRect
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import kotlinx.serialization.json.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.DirectDIAware
|
||||
|
||||
Reference in New Issue
Block a user