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"?>
|
||||
<LinearLayout
|
||||
<RelativeLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?selectableItemBackground"
|
||||
android:clipChildren="false"
|
||||
android:orientation="vertical"
|
||||
android:paddingVertical="@dimen/grid_spacing_outer">
|
||||
android:paddingBottom="@dimen/grid_spacing_outer">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/textView_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_width="0dp"
|
||||
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_marginTop="@dimen/grid_spacing_outer"
|
||||
android:layout_toStartOf="@id/button_more"
|
||||
android:gravity="center_vertical|start"
|
||||
android:padding="@dimen/grid_spacing"
|
||||
android:singleLine="true"
|
||||
android:textAppearance="@style/TextAppearance.Kotatsu.SectionHeader"
|
||||
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
|
||||
android:id="@+id/recyclerView"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/textView_title"
|
||||
android:layout_alignParentStart="true"
|
||||
android:clipToPadding="false"
|
||||
android:orientation="horizontal"
|
||||
android:paddingHorizontal="@dimen/grid_spacing"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager" />
|
||||
|
||||
</LinearLayout>
|
||||
</RelativeLayout>
|
||||
Loading…
Reference in New Issue