FloatingSearchBar
This commit is contained in:
3
.idea/misc.xml
generated
3
.idea/misc.xml
generated
@@ -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" />
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("") }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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.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
|
||||||
|
|||||||
Reference in New Issue
Block a user