FloatingSearchBar

This commit is contained in:
tom5079
2021-12-15 16:30:05 +09:00
parent 458530e80c
commit b690d01243
9 changed files with 292 additions and 99 deletions

3
.idea/misc.xml generated
View File

@@ -26,8 +26,11 @@
<entry key="../../../../layout/compose-model-1631801120026.xml" value="1.922077922077922" /> <entry key="../../../../layout/compose-model-1631801120026.xml" value="1.922077922077922" />
<entry key="../../../../layout/compose-model-1631838314391.xml" value="2.0" /> <entry key="../../../../layout/compose-model-1631838314391.xml" value="2.0" />
<entry key="../../../../layout/compose-model-1639478149655.xml" value="0.33" /> <entry key="../../../../layout/compose-model-1639478149655.xml" value="0.33" />
<entry key="../../../../layout/compose-model-1639535152524.xml" value="0.3055555555555556" />
<entry key="../../../../layout/compose-model-1639538998660.xml" value="0.30277777777777776" />
<entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" /> <entry key="../../../../layout/custom_preview.xml" value="0.518974358974359" />
<entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" /> <entry key="app/src/main/res/drawable/avd_star.xml" value="0.2722222222222222" />
<entry key="app/src/main/res/drawable/history.xml" value="0.3055555555555556" />
<entry key="app/src/main/res/layout/gallery_dialog.xml" value="0.30052083333333335" /> <entry key="app/src/main/res/layout/gallery_dialog.xml" value="0.30052083333333335" />
<entry key="app/src/main/res/layout/main_activity.xml" value="0.2953125" /> <entry key="app/src/main/res/layout/main_activity.xml" value="0.2953125" />
<entry key="app/src/main/res/layout/main_activity_content.xml" value="0.2953125" /> <entry key="app/src/main/res/layout/main_activity_content.xml" value="0.2953125" />

View File

@@ -22,8 +22,7 @@ android {
} }
buildTypes { buildTypes {
getByName("debug") { getByName("debug") {
isDebuggable = false isDebuggable = true
isMinifyEnabled = true
applicationIdSuffix = ".debug" applicationIdSuffix = ".debug"
versionNameSuffix = "-DEBUG" versionNameSuffix = "-DEBUG"
@@ -77,10 +76,12 @@ dependencies {
implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.navigation:navigation-compose:2.4.0-beta02") implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
implementation("com.google.accompanist:accompanist-flowlayout:0.20.2") implementation("com.google.accompanist:accompanist-flowlayout:0.20.3")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.2") implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.3")
implementation("com.google.accompanist:accompanist-insets:0.20.2") implementation("com.google.accompanist:accompanist-insets:0.20.3")
implementation("com.google.accompanist:accompanist-insets-ui:0.20.2") implementation("com.google.accompanist:accompanist-insets-ui:0.20.3")
implementation("com.google.accompanist:accompanist-drawablepainter:0.20.3")
implementation("com.google.accompanist:accompanist-systemuicontroller:0.20.3")
implementation("io.coil-kt:coil-compose:1.3.2") implementation("io.coil-kt:coil-compose:1.3.2")

View File

@@ -152,7 +152,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var cachedSortMode: Int = -1 var cachedSortMode: Int = -1
private val cache = mutableListOf<Int>() 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()) { if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null cachedQuery = null
cache.clear() cache.clear()
@@ -179,8 +179,8 @@ class Hitomi(app: Application) : Source(), DIAware {
channel.close() channel.close()
} }
return Pair(channel, cache.size) Pair(channel, cache.size)
} } }
override suspend fun suggestion(query: String) : List<TagSuggestion> { override suspend fun suggestion(query: String) : List<TagSuggestion> {
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map { return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {

View File

@@ -23,22 +23,33 @@ import android.os.Bundle
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.compose.foundation.gestures.awaitFirstDown import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.compose.foundation.gestures.forEachGesture import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.gestures.scrollable 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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.CircularProgressIndicator import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.Icon import androidx.compose.foundation.verticalScroll
import androidx.compose.material.Scaffold import androidx.compose.material.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.PointerEventPass
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.pointerInteropFilter 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastAny 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.*
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
@@ -68,6 +83,11 @@ import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
import kotlin.math.* import kotlin.math.*
private enum class NavigationIconState {
MENU,
ARROW
}
class MainActivity : ComponentActivity(), DIAware { class MainActivity : ComponentActivity(), DIAware {
override val di by closestDI() override val di by closestDI()
@@ -75,37 +95,36 @@ class MainActivity : ComponentActivity(), DIAware {
private val logger = newLogger(LoggerFactory.default) private val logger = newLogger(LoggerFactory.default)
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContent { 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 { 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( Scaffold(
floatingActionButton = { floatingActionButton = {
MultipleFloatingActionButton( MultipleFloatingActionButton(
@@ -127,6 +146,7 @@ class MainActivity : ComponentActivity(), DIAware {
stringResource(R.string.main_open_gallery_by_id) stringResource(R.string.main_open_gallery_by_id)
), ),
), ),
visible = isFabVisible,
targetState = isFabExpanded, targetState = isFabExpanded,
onStateChanged = { onStateChanged = {
isFabExpanded = it isFabExpanded = it
@@ -136,8 +156,24 @@ class MainActivity : ComponentActivity(), DIAware {
) { ) {
Box(Modifier.fillMaxSize()) { Box(Modifier.fillMaxSize()) {
LazyColumn( LazyColumn(
Modifier.fillMaxSize(), Modifier
state = lazyListState, .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) contentPadding = PaddingValues(0.dp, 56.dp, 0.dp, 0.dp)
) { ) {
items(model.searchResults, key = { it.itemID }) { itemInfo -> 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)) CircularProgressIndicator(Modifier.align(Alignment.Center))
FloatingSearchBar( FloatingSearchBar(
modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }), modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
query = query, query = model.query,
onQueryChange = { query = it }, onQueryChange = { model.query = it },
navigationIcon = {
Icon(
painter = rememberDrawablePainter(navigationIcon),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
},
actions = { actions = {
Icon( Icon(
Icons.Default.Sort, Icons.Default.Sort,
contentDescription = null, contentDescription = null,
modifier = Modifier.size(24.dp) modifier = Modifier.size(24.dp),
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
) )
Icon( Icon(
Icons.Default.Settings, Icons.Default.Settings,
contentDescription = null, 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() }
) )
} }
} }

View File

@@ -19,23 +19,31 @@
package xyz.quaver.pupil.ui.composable package xyz.quaver.pupil.ui.composable
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField 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.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Close
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusChanged
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor 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.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.util.KeyboardManager
@Preview @Preview
@Composable @Composable
@@ -43,16 +51,26 @@ fun FloatingSearchBar(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
query: String = "", query: String = "",
onQueryChange: (String) -> Unit = { }, onQueryChange: (String) -> Unit = { },
navigationIcon: @Composable () -> Unit = { navigationIcon: @Composable () -> Unit = { },
Icon( actions: @Composable RowScope.() -> Unit = { },
Icons.Default.Menu, onTextFieldFocused: () -> Unit = { },
modifier = Modifier.size(24.dp), onTextFieldUnfocused: () -> Unit = { }
contentDescription = null,
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
)
},
actions: @Composable RowScope.() -> 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( Card(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@@ -68,19 +86,46 @@ fun FloatingSearchBar(
navigationIcon() navigationIcon()
BasicTextField( 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, value = query,
onValueChange = onQueryChange, onValueChange = onQueryChange,
singleLine = true, singleLine = true,
cursorBrush = SolidColor(MaterialTheme.colors.primary), cursorBrush = SolidColor(MaterialTheme.colors.primary),
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
focusManager.clearFocus()
}
),
decorationBox = { innerTextField -> decorationBox = { innerTextField ->
if (query.isEmpty()) Row(
Text( verticalAlignment = Alignment.CenterVertically
stringResource(R.string.search_hint), ) {
color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) 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("") }
)
}
} }
) )

View File

@@ -16,6 +16,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.Modifier.Companion.any
import androidx.compose.ui.draw.alpha import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale 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.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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) { enum class FloatingActionButtonState(private val isExpanded: Boolean) {
COLLAPSED(false), EXPANDED(true); COLLAPSED(false), EXPANDED(true);
@@ -112,6 +114,7 @@ private class FloatingActionButtonItemProvider : PreviewParameterProvider<SubFab
fun MultipleFloatingActionButton( fun MultipleFloatingActionButton(
@PreviewParameter(provider = FloatingActionButtonItemProvider::class) items: List<SubFabItem>, @PreviewParameter(provider = FloatingActionButtonItemProvider::class) items: List<SubFabItem>,
fabIcon: ImageVector = Icons.Default.Add, fabIcon: ImageVector = Icons.Default.Add,
visible: Boolean = true,
targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED, targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
onStateChanged: ((FloatingActionButtonState) -> Unit)? = null onStateChanged: ((FloatingActionButtonState) -> Unit)? = null
) { ) {
@@ -131,10 +134,14 @@ fun MultipleFloatingActionButton(
} }
} }
if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
Column( Column(
horizontalAlignment = Alignment.End, horizontalAlignment = Alignment.End,
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
var allCollapsed = true
items.forEachIndexed { index, item -> items.forEachIndexed { index, item ->
val delay = when (targetState) { val delay = when (targetState) {
FloatingActionButtonState.COLLAPSED -> index FloatingActionButtonState.COLLAPSED -> index
@@ -207,12 +214,26 @@ fun MultipleFloatingActionButton(
) { ) {
item.onClick?.invoke(it) item.onClick?.invoke(it)
} }
if (buttonScale != 0f) allCollapsed = false
} }
FloatingActionButton(onClick = { val visibilityTransition = updateTransition(targetState = visible || !allCollapsed, label = "visible")
onStateChanged?.invoke(!targetState)
}) { val scale by visibilityTransition.animateFloat(
Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null) 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)
}
} }
} }

View File

@@ -20,13 +20,18 @@ package xyz.quaver.pupil.ui.viewmodel
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.* import androidx.lifecycle.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.direct import org.kodein.di.direct
import org.kodein.di.instance 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.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.sources.* import xyz.quaver.pupil.sources.*
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
@@ -39,15 +44,17 @@ import kotlin.random.Random
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware { class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
override val di by closestDI() override val di by closestDI()
private val logger = newLogger(LoggerFactory.default)
val searchResults = mutableStateListOf<ItemInfo>() val searchResults = mutableStateListOf<ItemInfo>()
private val _loading = MutableLiveData(false) var loading by mutableStateOf(false)
val loading = _loading as LiveData<Boolean> private set
private var queryJob: Job? = null private var queryJob: Job? = null
private var suggestionJob: Job? = null private var suggestionJob: Job? = null
val query = MutableLiveData<String>() var query by mutableStateOf("")
private val queryStack = mutableListOf<String>() private val queryStack = mutableListOf<String>()
private val defaultSourceFactory: (String) -> Source = { private val defaultSourceFactory: (String) -> Source = {
@@ -90,11 +97,11 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
sortModeIndex.value = 0 sortModeIndex.value = 0
} }
setQueryAndSearch() query = ""
resetAndQuery()
} }
fun setQueryAndSearch(query: String = "") { fun resetAndQuery() {
this.query.value = query
queryStack.add(query) queryStack.add(query)
setPage(1) setPage(1)
@@ -126,29 +133,26 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
suggestionJob?.cancel() suggestionJob?.cancel()
queryJob?.cancel() queryJob?.cancel()
_loading.value = true loading = true
searchResults.clear()
queryJob = viewModelScope.launch { queryJob = viewModelScope.launch {
launch(Dispatchers.Default) { val (channel, count) = source.search(
val channel = withContext(Dispatchers.IO) { query,
val (channel, count) = source.search( (currentPage - 1) * perPage until currentPage * perPage,
query.value ?: "", sortModeIndex
(currentPage - 1) * perPage until currentPage * perPage, )
sortModeIndex
)
totalItems.postValue(count) logger.info { count.toString() }
channel totalItems.postValue(count)
}
for (result in channel) { for (result in channel) {
yield() yield()
searchResults.add(result) searchResults.add(result)
}
_loading.postValue(false)
} }
loading = false
} }
} }
@@ -165,7 +169,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
viewModelScope.launch { viewModelScope.launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
_source.value?.search( _source.value?.search(
query.value + Preferences["default_query", ""], query + Preferences["default_query", ""],
random .. random, random .. random,
sortModeIndex.value!! sortModeIndex.value!!
)?.first?.receive() )?.first?.receive()
@@ -182,7 +186,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
@SuppressLint("NullSafeMutableLiveData") @SuppressLint("NullSafeMutableLiveData")
_suggestions.value = withContext(Dispatchers.IO) { _suggestions.value = withContext(Dispatchers.IO) {
kotlin.runCatching { kotlin.runCatching {
_source.value!!.suggestion(query.value!!) _source.value!!.suggestion(query)
}.getOrElse { emptyList() } }.getOrElse { emptyList() }
} }
} }
@@ -195,7 +199,8 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
if (queryStack.removeLastOrNull() == null || queryStack.isEmpty()) if (queryStack.removeLastOrNull() == null || queryStack.isEmpty())
return false return false
setQueryAndSearch(queryStack.removeLast()) query = queryStack.removeLast()
resetAndQuery()
return true return true
} }

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

View File

@@ -25,14 +25,18 @@ import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder import android.graphics.BitmapRegionDecoder
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.remember 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.Rect
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.platform.LocalFocusManager
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.google.accompanist.insets.LocalWindowInsets
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware import org.kodein.di.DirectDIAware