Merge branch 'devel' into feature/nextgen
commit
e4a2897731
@ -0,0 +1,162 @@
|
|||||||
|
package org.koitharu.kotatsu.base.ui.list
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuItem
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.appcompat.view.ActionMode
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
|
import androidx.lifecycle.LifecycleEventObserver
|
||||||
|
import androidx.lifecycle.LifecycleOwner
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import androidx.savedstate.SavedStateRegistry
|
||||||
|
import androidx.savedstate.SavedStateRegistryOwner
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import org.koitharu.kotatsu.base.ui.list.decor.AbstractSelectionItemDecoration
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
|
||||||
|
private const val KEY_SELECTION = "selection"
|
||||||
|
private const val PROVIDER_NAME = "selection_decoration"
|
||||||
|
|
||||||
|
class ListSelectionController(
|
||||||
|
private val activity: Activity,
|
||||||
|
private val decoration: AbstractSelectionItemDecoration,
|
||||||
|
private val registryOwner: SavedStateRegistryOwner,
|
||||||
|
private val callback: Callback,
|
||||||
|
) : ActionMode.Callback, SavedStateRegistry.SavedStateProvider {
|
||||||
|
|
||||||
|
private var actionMode: ActionMode? = null
|
||||||
|
private val stateEventObserver = StateEventObserver()
|
||||||
|
|
||||||
|
val count: Int
|
||||||
|
get() = decoration.checkedItemsCount
|
||||||
|
|
||||||
|
fun snapshot(): Set<Long> {
|
||||||
|
return peekCheckedIds().toSet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun peekCheckedIds(): Set<Long> {
|
||||||
|
return decoration.checkedItemsIds
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear() {
|
||||||
|
decoration.clearSelection()
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addAll(ids: Collection<Long>) {
|
||||||
|
if (ids.isEmpty()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoration.checkAll(ids)
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun attachToRecyclerView(recyclerView: RecyclerView) {
|
||||||
|
recyclerView.addItemDecoration(decoration)
|
||||||
|
registryOwner.lifecycle.addObserver(stateEventObserver)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun saveState(): Bundle {
|
||||||
|
val bundle = Bundle(1)
|
||||||
|
bundle.putLongArray(KEY_SELECTION, peekCheckedIds().toLongArray())
|
||||||
|
return bundle
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemClick(id: Long): Boolean {
|
||||||
|
if (decoration.checkedItemsCount != 0) {
|
||||||
|
decoration.toggleItemChecked(id)
|
||||||
|
if (decoration.checkedItemsCount == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
notifySelectionChanged()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onItemLongClick(id: Long): Boolean {
|
||||||
|
startActionMode()
|
||||||
|
return actionMode?.also {
|
||||||
|
decoration.setItemIsChecked(id, true)
|
||||||
|
notifySelectionChanged()
|
||||||
|
} != null
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return callback.onCreateActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||||
|
return callback.onPrepareActionMode(mode, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||||
|
return callback.onActionItemClicked(mode, item)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) {
|
||||||
|
callback.onDestroyActionMode(mode)
|
||||||
|
clear()
|
||||||
|
actionMode = null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startActionMode() {
|
||||||
|
if (actionMode == null) {
|
||||||
|
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifySelectionChanged() {
|
||||||
|
val count = decoration.checkedItemsCount
|
||||||
|
callback.onSelectionChanged(count)
|
||||||
|
if (count == 0) {
|
||||||
|
actionMode?.finish()
|
||||||
|
} else {
|
||||||
|
actionMode?.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreState(ids: Collection<Long>) {
|
||||||
|
if (ids.isEmpty() || decoration.checkedItemsCount != 0) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
decoration.checkAll(ids)
|
||||||
|
startActionMode()
|
||||||
|
notifySelectionChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Callback : ActionMode.Callback {
|
||||||
|
|
||||||
|
fun onSelectionChanged(count: Int)
|
||||||
|
|
||||||
|
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
|
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean
|
||||||
|
|
||||||
|
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean
|
||||||
|
|
||||||
|
override fun onDestroyActionMode(mode: ActionMode) = Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
private inner class StateEventObserver : LifecycleEventObserver {
|
||||||
|
|
||||||
|
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
|
||||||
|
if (event == Lifecycle.Event.ON_CREATE) {
|
||||||
|
val registry = registryOwner.savedStateRegistry
|
||||||
|
registry.registerSavedStateProvider(PROVIDER_NAME, this@ListSelectionController)
|
||||||
|
val state = registry.consumeRestoredStateForKey(PROVIDER_NAME)
|
||||||
|
if (state != null) {
|
||||||
|
Dispatchers.Main.dispatch(EmptyCoroutineContext) { // == Handler.post
|
||||||
|
if (source.lifecycle.currentState.isAtLeast(Lifecycle.State.CREATED)) {
|
||||||
|
restoreState(state.getLongArray(KEY_SELECTION)?.toList().orEmpty())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
package org.koitharu.kotatsu.utils.image
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import androidx.core.graphics.get
|
||||||
|
import coil.size.Size
|
||||||
|
import coil.transform.Transformation
|
||||||
|
|
||||||
|
class TrimTransformation : Transformation {
|
||||||
|
|
||||||
|
override val cacheKey: String = javaClass.name
|
||||||
|
|
||||||
|
override suspend fun transform(input: Bitmap, size: Size): Bitmap {
|
||||||
|
var left = 0
|
||||||
|
var top = 0
|
||||||
|
var right = 0
|
||||||
|
var bottom = 0
|
||||||
|
|
||||||
|
// Left
|
||||||
|
for (x in 0 until input.width) {
|
||||||
|
var isColBlank = true
|
||||||
|
val prevColor = input[x, 0]
|
||||||
|
for (y in 1 until input.height) {
|
||||||
|
if (input[x, y] != prevColor) {
|
||||||
|
isColBlank = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isColBlank) {
|
||||||
|
left++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (left == input.width) {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
// Right
|
||||||
|
for (x in (left until input.width).reversed()) {
|
||||||
|
var isColBlank = true
|
||||||
|
val prevColor = input[x, 0]
|
||||||
|
for (y in 1 until input.height) {
|
||||||
|
if (input[x, y] != prevColor) {
|
||||||
|
isColBlank = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isColBlank) {
|
||||||
|
right++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Top
|
||||||
|
for (y in 0 until input.height) {
|
||||||
|
var isRowBlank = true
|
||||||
|
val prevColor = input[0, y]
|
||||||
|
for (x in 1 until input.width) {
|
||||||
|
if (input[x, y] != prevColor) {
|
||||||
|
isRowBlank = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRowBlank) {
|
||||||
|
top++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Bottom
|
||||||
|
for (y in (top until input.height).reversed()) {
|
||||||
|
var isRowBlank = true
|
||||||
|
val prevColor = input[0, y]
|
||||||
|
for (x in 1 until input.width) {
|
||||||
|
if (input[x, y] != prevColor) {
|
||||||
|
isRowBlank = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isRowBlank) {
|
||||||
|
bottom++
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return if (left != 0 || right != 0 || top != 0 || bottom != 0) {
|
||||||
|
Bitmap.createBitmap(input, left, top, input.width - left - right, input.height - top - bottom)
|
||||||
|
} else {
|
||||||
|
input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?) = other is TrimTransformation
|
||||||
|
|
||||||
|
override fun hashCode() = javaClass.hashCode()
|
||||||
|
}
|
||||||
@ -1,32 +1,49 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<LinearLayout
|
<RelativeLayout
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:background="?selectableItemBackground"
|
android:clipChildren="false"
|
||||||
android:orientation="vertical"
|
android:orientation="vertical"
|
||||||
android:paddingVertical="@dimen/grid_spacing_outer">
|
android:paddingBottom="@dimen/grid_spacing_outer">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/textView_title"
|
android:id="@+id/textView_title"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignWithParentIfMissing="true"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
android:layout_marginHorizontal="@dimen/grid_spacing"
|
android:layout_marginHorizontal="@dimen/grid_spacing"
|
||||||
|
android:layout_marginTop="@dimen/grid_spacing_outer"
|
||||||
|
android:layout_toStartOf="@id/button_more"
|
||||||
android:gravity="center_vertical|start"
|
android:gravity="center_vertical|start"
|
||||||
android:padding="@dimen/grid_spacing"
|
android:padding="@dimen/grid_spacing"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||||
tools:text="@tools:sample/lorem[2]" />
|
tools:text="@tools:sample/lorem[2]" />
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/button_more"
|
||||||
|
style="@style/Widget.Material3.Button.TextButton"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_alignBaseline="@id/textView_title"
|
||||||
|
android:layout_alignParentEnd="true"
|
||||||
|
android:layout_marginEnd="@dimen/grid_spacing"
|
||||||
|
android:text="@string/show_all" />
|
||||||
|
|
||||||
<androidx.recyclerview.widget.RecyclerView
|
<androidx.recyclerview.widget.RecyclerView
|
||||||
android:id="@+id/recyclerView"
|
android:id="@+id/recyclerView"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_below="@id/textView_title"
|
||||||
|
android:layout_alignParentStart="true"
|
||||||
android:clipToPadding="false"
|
android:clipToPadding="false"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:paddingHorizontal="@dimen/grid_spacing"
|
android:paddingHorizontal="@dimen/grid_spacing"
|
||||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||||
|
|
||||||
</LinearLayout>
|
</RelativeLayout>
|
||||||
Loading…
Reference in New Issue