commit
a65ad9a2f8
@ -0,0 +1,52 @@ |
|||||||
|
package io.legado.app.base |
||||||
|
|
||||||
|
import android.app.Application |
||||||
|
import androidx.lifecycle.AndroidViewModel |
||||||
|
import kotlinx.coroutines.* |
||||||
|
import kotlin.coroutines.CoroutineContext |
||||||
|
|
||||||
|
open class BaseViewModel(application: Application) : AndroidViewModel(application), CoroutineScope { |
||||||
|
override val coroutineContext: CoroutineContext |
||||||
|
get() = Dispatchers.Main |
||||||
|
|
||||||
|
|
||||||
|
private val launchManager: MutableList<Job> = mutableListOf() |
||||||
|
|
||||||
|
protected fun launchOnUI( |
||||||
|
tryBlock: suspend CoroutineScope.() -> Unit,//成功 |
||||||
|
errorBlock: suspend CoroutineScope.(Throwable) -> Unit,//失败 |
||||||
|
finallyBlock: suspend CoroutineScope.() -> Unit//结束 |
||||||
|
) { |
||||||
|
launchOnUI { |
||||||
|
tryCatch(tryBlock, errorBlock, finallyBlock) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* add launch task to [launchManager] |
||||||
|
*/ |
||||||
|
private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) { |
||||||
|
val job = launch { block() }//主线程 |
||||||
|
launchManager.add(job) |
||||||
|
job.invokeOnCompletion { launchManager.remove(job) } |
||||||
|
} |
||||||
|
|
||||||
|
private suspend fun tryCatch( |
||||||
|
tryBlock: suspend CoroutineScope.() -> Unit, |
||||||
|
errorBlock: suspend CoroutineScope.(Throwable) -> Unit, |
||||||
|
finallyBlock: suspend CoroutineScope.() -> Unit |
||||||
|
) { |
||||||
|
try { |
||||||
|
coroutineScope { tryBlock() } |
||||||
|
} catch (e: Throwable) { |
||||||
|
coroutineScope { errorBlock(e) } |
||||||
|
} finally { |
||||||
|
coroutineScope { finallyBlock() } |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCleared() { |
||||||
|
super.onCleared() |
||||||
|
launchManager.clear() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,405 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.util.SparseArray |
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.View |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.recyclerview.widget.GridLayoutManager |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
import java.util.* |
||||||
|
|
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/11/24. |
||||||
|
* |
||||||
|
* 通用的adapter 可添加header,footer,以及不同类型item |
||||||
|
*/ |
||||||
|
abstract class CommonRecyclerAdapter<ITEM>(protected val context: Context) : RecyclerView.Adapter<ItemViewHolder>() { |
||||||
|
|
||||||
|
constructor(context: Context, vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) : this(context) { |
||||||
|
addItemViewDelegates(*delegates) |
||||||
|
} |
||||||
|
|
||||||
|
constructor(context: Context, vararg delegates: ItemViewDelegate<ITEM>) : this(context) { |
||||||
|
addItemViewDelegates(*delegates) |
||||||
|
} |
||||||
|
|
||||||
|
private val inflater: LayoutInflater = LayoutInflater.from(context) |
||||||
|
|
||||||
|
private var headerItems: SparseArray<View>? = null |
||||||
|
private var footerItems: SparseArray<View>? = null |
||||||
|
|
||||||
|
private val itemDelegates: HashMap<Int, ItemViewDelegate<ITEM>> = hashMapOf() |
||||||
|
private val items: MutableList<ITEM> = mutableListOf() |
||||||
|
|
||||||
|
private val lock = Object() |
||||||
|
|
||||||
|
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null |
||||||
|
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null |
||||||
|
|
||||||
|
private var itemAnimation: ItemAnimation? = null |
||||||
|
|
||||||
|
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) { |
||||||
|
itemClickListener = listener |
||||||
|
} |
||||||
|
|
||||||
|
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) { |
||||||
|
itemLongClickListener = listener |
||||||
|
} |
||||||
|
|
||||||
|
fun bindToRecyclerView(recyclerView: RecyclerView) { |
||||||
|
recyclerView.adapter = this |
||||||
|
} |
||||||
|
|
||||||
|
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(viewType: Int, delegate: DELEGATE) { |
||||||
|
itemDelegates.put(viewType, delegate) |
||||||
|
} |
||||||
|
|
||||||
|
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegate(delegate: DELEGATE) { |
||||||
|
itemDelegates.put(itemDelegates.size, delegate) |
||||||
|
} |
||||||
|
|
||||||
|
fun <DELEGATE : ItemViewDelegate<ITEM>> addItemViewDelegates(vararg delegates: DELEGATE) { |
||||||
|
delegates.forEach { |
||||||
|
addItemViewDelegate(it) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addItemViewDelegates(vararg delegates: Pair<Int, ItemViewDelegate<ITEM>>) { |
||||||
|
delegates.forEach { |
||||||
|
addItemViewDelegate(it.first, it.second) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addHeaderView(header: View) { |
||||||
|
synchronized(lock) { |
||||||
|
if (headerItems == null) { |
||||||
|
headerItems = SparseArray() |
||||||
|
} |
||||||
|
headerItems?.let { |
||||||
|
val index = it.size() |
||||||
|
it.put(TYPE_HEADER_VIEW + it.size(), header) |
||||||
|
notifyItemInserted(index) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addFooterView(footer: View) { |
||||||
|
synchronized(lock) { |
||||||
|
if (footerItems == null) { |
||||||
|
footerItems = SparseArray() |
||||||
|
} |
||||||
|
footerItems?.let { |
||||||
|
val index = getActualItemCount() + it.size() |
||||||
|
it.put(TYPE_FOOTER_VIEW + it.size(), footer) |
||||||
|
notifyItemInserted(index) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun removeHeaderView(header: View) { |
||||||
|
synchronized(lock) { |
||||||
|
headerItems?.let { |
||||||
|
val index = it.indexOfValue(header) |
||||||
|
if (index >= 0) { |
||||||
|
it.remove(index) |
||||||
|
notifyItemRemoved(index) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun removeFooterView(footer: View) { |
||||||
|
synchronized(lock) { |
||||||
|
footerItems?.let { |
||||||
|
val index = it.indexOfValue(footer) |
||||||
|
if (index >= 0) { |
||||||
|
it.remove(index) |
||||||
|
notifyItemRemoved(getActualItemCount() + index - 2) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun setItems(items: List<ITEM>?) { |
||||||
|
synchronized(lock) { |
||||||
|
if (this.items.isNotEmpty()) { |
||||||
|
this.items.clear() |
||||||
|
} |
||||||
|
if (items != null) { |
||||||
|
this.items.addAll(items) |
||||||
|
} |
||||||
|
notifyDataSetChanged() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun setItem(position: Int, item: ITEM) { |
||||||
|
synchronized(lock) { |
||||||
|
val oldSize = getActualItemCount() |
||||||
|
if (position in 0 until oldSize) { |
||||||
|
this.items[position] = item |
||||||
|
notifyItemChanged(position + getHeaderCount()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addItem(item: ITEM) { |
||||||
|
synchronized(lock) { |
||||||
|
val oldSize = getActualItemCount() |
||||||
|
if (this.items.add(item)) { |
||||||
|
if (oldSize == 0) { |
||||||
|
notifyDataSetChanged() |
||||||
|
} else { |
||||||
|
notifyItemInserted(oldSize + getHeaderCount()) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addItems(position: Int, newItems: List<ITEM>) { |
||||||
|
synchronized(lock) { |
||||||
|
val oldSize = getActualItemCount() |
||||||
|
if (position in 0 until oldSize) { |
||||||
|
if (if (oldSize == 0) this.items.addAll(newItems) else this.items.addAll(position, newItems)) { |
||||||
|
if (oldSize == 0) { |
||||||
|
notifyDataSetChanged() |
||||||
|
} else { |
||||||
|
notifyItemRangeChanged(position + getHeaderCount(), newItems.size) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun addItems(newItems: List<ITEM>) { |
||||||
|
synchronized(lock) { |
||||||
|
val oldSize = getActualItemCount() |
||||||
|
if (this.items.addAll(newItems)) { |
||||||
|
if (oldSize == 0) { |
||||||
|
notifyDataSetChanged() |
||||||
|
} else { |
||||||
|
notifyItemRangeChanged(oldSize + getHeaderCount(), newItems.size) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun removeItem(position: Int) { |
||||||
|
synchronized(lock) { |
||||||
|
if (this.items.removeAt(position) != null) |
||||||
|
notifyItemRemoved(position + getHeaderCount()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun removeItem(item: ITEM) { |
||||||
|
synchronized(lock) { |
||||||
|
if (this.items.remove(item)) |
||||||
|
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount()) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun removeItems(items: List<ITEM>) { |
||||||
|
synchronized(lock) { |
||||||
|
if (this.items.removeAll(items)) |
||||||
|
notifyDataSetChanged() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun swapItem(oldPosition: Int, newPosition: Int) { |
||||||
|
synchronized(lock) { |
||||||
|
val size = getActualItemCount() |
||||||
|
if (oldPosition in 0 until size && newPosition in 0 until size) { |
||||||
|
Collections.swap(this.items, oldPosition + getHeaderCount(), newPosition + getHeaderCount()) |
||||||
|
notifyDataSetChanged() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun updateItem(position: Int, payload: Any) { |
||||||
|
synchronized(lock) { |
||||||
|
val size = getActualItemCount() |
||||||
|
if (position in 0 until size) { |
||||||
|
notifyItemChanged(position + getHeaderCount(), payload) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) { |
||||||
|
synchronized(lock) { |
||||||
|
val size = getActualItemCount() |
||||||
|
if (fromPosition in 0 until size && toPosition in 0 until size) { |
||||||
|
notifyItemRangeChanged(fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun isEmpty(): Boolean { |
||||||
|
return items.isEmpty() |
||||||
|
} |
||||||
|
|
||||||
|
fun isNotEmpty(): Boolean { |
||||||
|
return items.isNotEmpty() |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 除去header和footer |
||||||
|
*/ |
||||||
|
fun getActualItemCount(): Int { |
||||||
|
return items.size |
||||||
|
} |
||||||
|
|
||||||
|
fun getHeaderCount(): Int { |
||||||
|
return headerItems?.size() ?: 0 |
||||||
|
} |
||||||
|
|
||||||
|
fun getFooterCount(): Int { |
||||||
|
return footerItems?.size() ?: 0 |
||||||
|
} |
||||||
|
|
||||||
|
fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null |
||||||
|
|
||||||
|
fun getItems(): List<ITEM> = items |
||||||
|
|
||||||
|
protected open fun getItemViewType(item: ITEM, position: Int): Int { |
||||||
|
return 0 |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* grid 模式下使用 |
||||||
|
*/ |
||||||
|
protected open fun getSpanSize(item: ITEM, viewType: Int, position: Int): Int { |
||||||
|
return 1 |
||||||
|
} |
||||||
|
|
||||||
|
final override fun getItemCount(): Int { |
||||||
|
return getActualItemCount() + getHeaderCount() + getFooterCount() |
||||||
|
} |
||||||
|
|
||||||
|
final override fun getItemViewType(position: Int): Int { |
||||||
|
return when { |
||||||
|
isHeader(position) -> TYPE_HEADER_VIEW + position |
||||||
|
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() |
||||||
|
else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0 |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder { |
||||||
|
return when { |
||||||
|
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> { |
||||||
|
ItemViewHolder(headerItems!!.get(viewType)) |
||||||
|
} |
||||||
|
|
||||||
|
viewType >= TYPE_FOOTER_VIEW -> { |
||||||
|
ItemViewHolder(footerItems!!.get(viewType)) |
||||||
|
} |
||||||
|
|
||||||
|
else -> { |
||||||
|
val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutID, parent, false)) |
||||||
|
|
||||||
|
if (itemClickListener != null) { |
||||||
|
holder.itemView.setOnClickListener { |
||||||
|
getItem(holder.layoutPosition)?.let { |
||||||
|
itemClickListener?.invoke(holder, it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if (itemLongClickListener != null) { |
||||||
|
holder.itemView.setOnLongClickListener { |
||||||
|
getItem(holder.layoutPosition)?.let { |
||||||
|
itemLongClickListener?.invoke(holder, it) ?: true |
||||||
|
} ?: true |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
holder |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { |
||||||
|
} |
||||||
|
|
||||||
|
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList<Any>) { |
||||||
|
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { |
||||||
|
getItem(holder.layoutPosition)?.let { |
||||||
|
itemDelegates.getValue(getItemViewType(holder.layoutPosition)) |
||||||
|
.convert(holder, it, payloads) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onViewAttachedToWindow(holder: ItemViewHolder) { |
||||||
|
super.onViewAttachedToWindow(holder) |
||||||
|
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { |
||||||
|
addAnimation(holder) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) { |
||||||
|
super.onAttachedToRecyclerView(recyclerView) |
||||||
|
val manager = recyclerView.layoutManager |
||||||
|
if (manager is GridLayoutManager) { |
||||||
|
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { |
||||||
|
override fun getSpanSize(position: Int): Int { |
||||||
|
return getItem(position)?.let { |
||||||
|
if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( |
||||||
|
it, getItemViewType(position), position |
||||||
|
) |
||||||
|
} ?: manager.spanCount |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun setItemAnimation(item: ItemAnimation) { |
||||||
|
itemAnimation = item |
||||||
|
} |
||||||
|
|
||||||
|
private fun isHeader(position: Int): Boolean { |
||||||
|
return position < getHeaderCount() |
||||||
|
} |
||||||
|
|
||||||
|
private fun isFooter(position: Int): Boolean { |
||||||
|
return position >= getActualItemCount() + getHeaderCount() |
||||||
|
} |
||||||
|
|
||||||
|
private fun getActualPosition(position: Int): Int { |
||||||
|
return position - getHeaderCount() |
||||||
|
} |
||||||
|
|
||||||
|
private fun addAnimation(holder: ItemViewHolder) { |
||||||
|
if (itemAnimation == null) { |
||||||
|
itemAnimation = ItemAnimation.create().enabled(true) |
||||||
|
} |
||||||
|
|
||||||
|
itemAnimation?.let { |
||||||
|
if (it.itemAnimEnabled) { |
||||||
|
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { |
||||||
|
startAnimation(holder, it) |
||||||
|
it.itemAnimStartPosition = holder.layoutPosition |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) { |
||||||
|
for (anim in item.itemAnimation.getAnimators(holder.itemView)) { |
||||||
|
anim.setDuration(item.itemAnimDuration).start() |
||||||
|
anim.interpolator = item.itemAnimInterpolator |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE |
||||||
|
private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999 |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,32 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/12/15. |
||||||
|
* |
||||||
|
* 上拉加载更多 |
||||||
|
*/ |
||||||
|
abstract class InfiniteScrollListener() : RecyclerView.OnScrollListener() { |
||||||
|
private val loadMoreRunnable = Runnable { onLoadMore() } |
||||||
|
|
||||||
|
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { |
||||||
|
// if (dy < 0 || dataLoading.isDataLoading()) return |
||||||
|
|
||||||
|
val layoutManager:LinearLayoutManager = recyclerView.layoutManager as LinearLayoutManager |
||||||
|
val visibleItemCount = recyclerView.childCount |
||||||
|
val totalItemCount = layoutManager.itemCount |
||||||
|
val firstVisibleItem = layoutManager.findFirstVisibleItemPosition() |
||||||
|
|
||||||
|
if (totalItemCount - visibleItemCount <= firstVisibleItem + VISIBLE_THRESHOLD) { |
||||||
|
recyclerView.post(loadMoreRunnable) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
abstract fun onLoadMore() |
||||||
|
|
||||||
|
companion object { |
||||||
|
private const val VISIBLE_THRESHOLD = 5 |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,91 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import android.view.animation.Interpolator |
||||||
|
import android.view.animation.LinearInterpolator |
||||||
|
import io.legado.app.base.adapter.animations.AlphaInAnimation |
||||||
|
import io.legado.app.base.adapter.animations.BaseAnimation |
||||||
|
import io.legado.app.base.adapter.animations.ScaleInAnimation |
||||||
|
import io.legado.app.base.adapter.animations.SlideInBottomAnimation |
||||||
|
import io.legado.app.base.adapter.animations.SlideInLeftAnimation |
||||||
|
import io.legado.app.base.adapter.animations.SlideInRightAnimation |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/12/15. |
||||||
|
*/ |
||||||
|
class ItemAnimation private constructor() { |
||||||
|
|
||||||
|
var itemAnimEnabled = false |
||||||
|
var itemAnimFirstOnly = true |
||||||
|
var itemAnimation: BaseAnimation = SlideInBottomAnimation() |
||||||
|
var itemAnimInterpolator: Interpolator = LinearInterpolator() |
||||||
|
var itemAnimDuration: Long = 300L |
||||||
|
var itemAnimStartPosition: Int = -1 |
||||||
|
|
||||||
|
fun interpolator(interpolator: Interpolator): ItemAnimation { |
||||||
|
itemAnimInterpolator = interpolator |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
fun duration(duration: Long): ItemAnimation { |
||||||
|
itemAnimDuration = duration |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
fun startPostion(startPos: Int): ItemAnimation { |
||||||
|
itemAnimStartPosition = startPos |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
fun animation(animationType: Int = FADE_IN, animation: BaseAnimation? = null): ItemAnimation { |
||||||
|
if (animation != null) { |
||||||
|
itemAnimation = animation |
||||||
|
} else { |
||||||
|
when (animationType) { |
||||||
|
FADE_IN -> itemAnimation = AlphaInAnimation() |
||||||
|
SCALE_IN -> itemAnimation = ScaleInAnimation() |
||||||
|
BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation() |
||||||
|
LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation() |
||||||
|
RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation() |
||||||
|
} |
||||||
|
} |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
fun enabled(enabled: Boolean): ItemAnimation { |
||||||
|
itemAnimEnabled = enabled |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
fun firstOnly(firstOnly: Boolean): ItemAnimation { |
||||||
|
itemAnimFirstOnly = firstOnly |
||||||
|
return this |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
/** |
||||||
|
* Use with [.openLoadAnimation] |
||||||
|
*/ |
||||||
|
const val FADE_IN: Int = 0x00000001 |
||||||
|
/** |
||||||
|
* Use with [.openLoadAnimation] |
||||||
|
*/ |
||||||
|
const val SCALE_IN: Int = 0x00000002 |
||||||
|
/** |
||||||
|
* Use with [.openLoadAnimation] |
||||||
|
*/ |
||||||
|
const val BOTTOM_SLIDE_IN: Int = 0x00000003 |
||||||
|
/** |
||||||
|
* Use with [.openLoadAnimation] |
||||||
|
*/ |
||||||
|
const val LEFT_SLIDE_IN: Int = 0x00000004 |
||||||
|
/** |
||||||
|
* Use with [.openLoadAnimation] |
||||||
|
*/ |
||||||
|
const val RIGHT_SLIDE_IN: Int = 0x00000005 |
||||||
|
|
||||||
|
fun create(): ItemAnimation { |
||||||
|
return ItemAnimation() |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/11/24. |
||||||
|
* |
||||||
|
* item代理, |
||||||
|
*/ |
||||||
|
abstract class ItemViewDelegate<in ITEM>(protected val context: Context) { |
||||||
|
|
||||||
|
abstract val layoutID: Int |
||||||
|
|
||||||
|
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import android.view.View |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/11/28. |
||||||
|
*/ |
||||||
|
class ItemViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) |
@ -0,0 +1,26 @@ |
|||||||
|
package io.legado.app.base.adapter |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by Invincible on 2017/12/15. |
||||||
|
*/ |
||||||
|
abstract class SimpleRecyclerAdapter<ITEM>(context: Context) : CommonRecyclerAdapter<ITEM>(context) { |
||||||
|
|
||||||
|
init { |
||||||
|
addItemViewDelegate(object : ItemViewDelegate<ITEM>(context) { |
||||||
|
override val layoutID: Int |
||||||
|
get() = this@SimpleRecyclerAdapter.layoutID |
||||||
|
|
||||||
|
override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>) { |
||||||
|
this@SimpleRecyclerAdapter.convert(holder, item, payloads) |
||||||
|
} |
||||||
|
|
||||||
|
}) |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
abstract val layoutID: Int |
||||||
|
|
||||||
|
abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList<Any>) |
||||||
|
} |
@ -0,0 +1,17 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.ObjectAnimator |
||||||
|
import android.view.View |
||||||
|
|
||||||
|
|
||||||
|
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) : BaseAnimation { |
||||||
|
|
||||||
|
override fun getAnimators(view: View): Array<Animator> = |
||||||
|
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
private const val DEFAULT_ALPHA_FROM = 0f |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.view.View |
||||||
|
|
||||||
|
/** |
||||||
|
* adapter item 动画 |
||||||
|
*/ |
||||||
|
interface BaseAnimation { |
||||||
|
|
||||||
|
fun getAnimators(view: View): Array<Animator> |
||||||
|
|
||||||
|
} |
@ -0,0 +1,20 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.ObjectAnimator |
||||||
|
import android.view.View |
||||||
|
|
||||||
|
|
||||||
|
class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation { |
||||||
|
|
||||||
|
override fun getAnimators(view: View): Array<Animator> { |
||||||
|
val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f) |
||||||
|
val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f) |
||||||
|
return arrayOf(scaleX, scaleY) |
||||||
|
} |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
private const val DEFAULT_SCALE_FROM = .5f |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,13 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.ObjectAnimator |
||||||
|
import android.view.View |
||||||
|
import io.legado.app.base.adapter.animations.BaseAnimation |
||||||
|
|
||||||
|
class SlideInBottomAnimation : BaseAnimation { |
||||||
|
|
||||||
|
|
||||||
|
override fun getAnimators(view: View): Array<Animator> = |
||||||
|
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f)) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.ObjectAnimator |
||||||
|
import android.view.View |
||||||
|
import io.legado.app.base.adapter.animations.BaseAnimation |
||||||
|
|
||||||
|
|
||||||
|
class SlideInLeftAnimation : BaseAnimation { |
||||||
|
|
||||||
|
|
||||||
|
override fun getAnimators(view: View): Array<Animator> = |
||||||
|
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f)) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.base.adapter.animations |
||||||
|
|
||||||
|
import android.animation.Animator |
||||||
|
import android.animation.ObjectAnimator |
||||||
|
import android.view.View |
||||||
|
import io.legado.app.base.adapter.animations.BaseAnimation |
||||||
|
|
||||||
|
|
||||||
|
class SlideInRightAnimation : BaseAnimation { |
||||||
|
|
||||||
|
|
||||||
|
override fun getAnimators(view: View): Array<Animator> = |
||||||
|
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f)) |
||||||
|
} |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.data.api |
||||||
|
|
||||||
|
import kotlinx.coroutines.Deferred |
||||||
|
import retrofit2.http.* |
||||||
|
|
||||||
|
interface CommonHttpApi { |
||||||
|
|
||||||
|
@GET |
||||||
|
fun get(@Url url: String, @QueryMap map: Map<String, String>): Deferred<String> |
||||||
|
|
||||||
|
@FormUrlEncoded |
||||||
|
@POST |
||||||
|
fun post(@Url url: String, @FieldMap map: Map<String, String>): Deferred<String> |
||||||
|
} |
@ -0,0 +1,49 @@ |
|||||||
|
package io.legado.app.data.dao |
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData |
||||||
|
import androidx.paging.DataSource |
||||||
|
import androidx.room.* |
||||||
|
import io.legado.app.data.entities.ReplaceRule |
||||||
|
|
||||||
|
|
||||||
|
@Dao |
||||||
|
interface ReplaceRuleDao { |
||||||
|
|
||||||
|
@Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") |
||||||
|
fun observeAll(): DataSource.Factory<Int, ReplaceRule> |
||||||
|
|
||||||
|
@Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC") |
||||||
|
fun observeAllIds(): LiveData<List<Int>> |
||||||
|
|
||||||
|
@get:Query("SELECT MAX(sortOrder) FROM replace_rules") |
||||||
|
val maxOrder: Int |
||||||
|
|
||||||
|
@get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") |
||||||
|
val all: List<ReplaceRule> |
||||||
|
|
||||||
|
@get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC") |
||||||
|
val allEnabled: List<ReplaceRule> |
||||||
|
|
||||||
|
@Query("SELECT * FROM replace_rules WHERE id = :id") |
||||||
|
fun findById(id: Int): ReplaceRule? |
||||||
|
|
||||||
|
@Query("SELECT * FROM replace_rules WHERE id in (:ids)") |
||||||
|
fun findByIds(vararg ids: Int): List<ReplaceRule> |
||||||
|
|
||||||
|
@Query("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'") |
||||||
|
fun findEnabledByScope(scope: String): List<ReplaceRule> |
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) |
||||||
|
fun insert(vararg replaceRules: ReplaceRule) |
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) |
||||||
|
fun insert(replaceRule: ReplaceRule): Long |
||||||
|
|
||||||
|
@Update |
||||||
|
fun update(vararg replaceRules: ReplaceRule) |
||||||
|
|
||||||
|
@Delete |
||||||
|
fun delete(vararg replaceRules: ReplaceRule) |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -1,2 +1,3 @@ |
|||||||
package io.legado.app.data.entities |
package io.legado.app.data.entities |
||||||
|
|
||||||
|
class SearchBook |
@ -0,0 +1,107 @@ |
|||||||
|
package io.legado.app.help.http |
||||||
|
|
||||||
|
import kotlinx.coroutines.CompletableDeferred |
||||||
|
import kotlinx.coroutines.Deferred |
||||||
|
import retrofit2.* |
||||||
|
import java.lang.reflect.ParameterizedType |
||||||
|
import java.lang.reflect.Type |
||||||
|
|
||||||
|
class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() { |
||||||
|
companion object { |
||||||
|
@JvmStatic @JvmName("create") |
||||||
|
operator fun invoke() = CoroutinesCallAdapterFactory() |
||||||
|
} |
||||||
|
|
||||||
|
override fun get( |
||||||
|
returnType: Type, |
||||||
|
annotations: Array<out Annotation>, |
||||||
|
retrofit: Retrofit |
||||||
|
): CallAdapter<*, *>? { |
||||||
|
if (Deferred::class.java != getRawType(returnType)) { |
||||||
|
return null |
||||||
|
} |
||||||
|
if (returnType !is ParameterizedType) { |
||||||
|
throw IllegalStateException( |
||||||
|
"Deferred return type must be parameterized as Deferred<Foo> or Deferred<out Foo>") |
||||||
|
} |
||||||
|
val responseType = getParameterUpperBound(0, returnType) |
||||||
|
|
||||||
|
val rawDeferredType = getRawType(responseType) |
||||||
|
return if (rawDeferredType == Response::class.java) { |
||||||
|
if (responseType !is ParameterizedType) { |
||||||
|
throw IllegalStateException( |
||||||
|
"Response must be parameterized as Response<Foo> or Response<out Foo>") |
||||||
|
} |
||||||
|
ResponseCallAdapter<Any>( |
||||||
|
getParameterUpperBound( |
||||||
|
0, |
||||||
|
responseType |
||||||
|
) |
||||||
|
) |
||||||
|
} else { |
||||||
|
BodyCallAdapter<Any>(responseType) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private class BodyCallAdapter<T>( |
||||||
|
private val responseType: Type |
||||||
|
) : CallAdapter<T, Deferred<T>> { |
||||||
|
|
||||||
|
override fun responseType() = responseType |
||||||
|
|
||||||
|
override fun adapt(call: Call<T>): Deferred<T> { |
||||||
|
val deferred = CompletableDeferred<T>() |
||||||
|
|
||||||
|
deferred.invokeOnCompletion { |
||||||
|
if (deferred.isCancelled) { |
||||||
|
call.cancel() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
call.enqueue(object : Callback<T> { |
||||||
|
override fun onFailure(call: Call<T>, t: Throwable) { |
||||||
|
deferred.completeExceptionally(t) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onResponse(call: Call<T>, response: Response<T>) { |
||||||
|
if (response.isSuccessful) { |
||||||
|
deferred.complete(response.body()!!) |
||||||
|
} else { |
||||||
|
deferred.completeExceptionally(HttpException(response)) |
||||||
|
} |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return deferred |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private class ResponseCallAdapter<T>( |
||||||
|
private val responseType: Type |
||||||
|
) : CallAdapter<T, Deferred<Response<T>>> { |
||||||
|
|
||||||
|
override fun responseType() = responseType |
||||||
|
|
||||||
|
override fun adapt(call: Call<T>): Deferred<Response<T>> { |
||||||
|
val deferred = CompletableDeferred<Response<T>>() |
||||||
|
|
||||||
|
deferred.invokeOnCompletion { |
||||||
|
if (deferred.isCancelled) { |
||||||
|
call.cancel() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
call.enqueue(object : Callback<T> { |
||||||
|
override fun onFailure(call: Call<T>, t: Throwable) { |
||||||
|
deferred.completeExceptionally(t) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onResponse(call: Call<T>, response: Response<T>) { |
||||||
|
deferred.complete(response) |
||||||
|
} |
||||||
|
}) |
||||||
|
|
||||||
|
return deferred |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,64 @@ |
|||||||
|
package io.legado.app.help.http |
||||||
|
|
||||||
|
import okhttp3.* |
||||||
|
import retrofit2.Retrofit |
||||||
|
import java.util.* |
||||||
|
import java.util.concurrent.TimeUnit |
||||||
|
|
||||||
|
object HttpHelper { |
||||||
|
|
||||||
|
val client: OkHttpClient = getOkHttpClient() |
||||||
|
|
||||||
|
|
||||||
|
fun <T> getApiService(baseUrl: String, clazz: Class<T>): T { |
||||||
|
return getRetrofit(baseUrl).create(clazz) |
||||||
|
} |
||||||
|
|
||||||
|
fun getRetrofit(baseUrl: String): Retrofit { |
||||||
|
return Retrofit.Builder().baseUrl(baseUrl) |
||||||
|
//增加返回值为字符串的支持(以实体类返回) |
||||||
|
// .addConverterFactory(EncodeConverter.create()) |
||||||
|
//增加返回值为Observable<T>的支持 |
||||||
|
.addCallAdapterFactory(CoroutinesCallAdapterFactory.invoke()) |
||||||
|
.client(client) |
||||||
|
.build() |
||||||
|
} |
||||||
|
|
||||||
|
private fun getOkHttpClient(): OkHttpClient { |
||||||
|
val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) |
||||||
|
.tlsVersions(TlsVersion.TLS_1_2) |
||||||
|
.build() |
||||||
|
|
||||||
|
val specs = ArrayList<ConnectionSpec>() |
||||||
|
specs.add(cs) |
||||||
|
specs.add(ConnectionSpec.COMPATIBLE_TLS) |
||||||
|
specs.add(ConnectionSpec.CLEARTEXT) |
||||||
|
|
||||||
|
val sslParams = SSLHelper.getSslSocketFactory() |
||||||
|
return OkHttpClient.Builder() |
||||||
|
.connectTimeout(15, TimeUnit.SECONDS) |
||||||
|
.writeTimeout(15, TimeUnit.SECONDS) |
||||||
|
.readTimeout(15, TimeUnit.SECONDS) |
||||||
|
.retryOnConnectionFailure(true) |
||||||
|
.sslSocketFactory(sslParams.sSLSocketFactory, sslParams.trustManager) |
||||||
|
.hostnameVerifier(SSLHelper.unsafeHostnameVerifier) |
||||||
|
.connectionSpecs(specs) |
||||||
|
.followRedirects(true) |
||||||
|
.followSslRedirects(true) |
||||||
|
.protocols(listOf(Protocol.HTTP_1_1)) |
||||||
|
.addInterceptor(getHeaderInterceptor()) |
||||||
|
.build() |
||||||
|
} |
||||||
|
|
||||||
|
private fun getHeaderInterceptor(): Interceptor { |
||||||
|
return Interceptor { chain -> |
||||||
|
val request = chain.request() |
||||||
|
.newBuilder() |
||||||
|
.addHeader("Keep-Alive", "300") |
||||||
|
.addHeader("Connection", "Keep-Alive") |
||||||
|
.addHeader("Cache-Control", "no-cache") |
||||||
|
.build() |
||||||
|
chain.proceed(request) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,175 @@ |
|||||||
|
package io.legado.app.help.http |
||||||
|
|
||||||
|
import javax.net.ssl.* |
||||||
|
import java.io.IOException |
||||||
|
import java.io.InputStream |
||||||
|
import java.security.KeyManagementException |
||||||
|
import java.security.KeyStore |
||||||
|
import java.security.NoSuchAlgorithmException |
||||||
|
import java.security.cert.CertificateException |
||||||
|
import java.security.cert.CertificateFactory |
||||||
|
import java.security.cert.X509Certificate |
||||||
|
|
||||||
|
object SSLHelper { |
||||||
|
|
||||||
|
val sslSocketFactory: SSLParams |
||||||
|
get() = getSslSocketFactoryBase(null, null, null) |
||||||
|
|
||||||
|
/** |
||||||
|
* 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查, |
||||||
|
* 这是一种有很大安全漏洞的办法 |
||||||
|
*/ |
||||||
|
val unsafeTrustManager: X509TrustManager = object : X509TrustManager { |
||||||
|
@Throws(CertificateException::class) |
||||||
|
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) { |
||||||
|
} |
||||||
|
|
||||||
|
@Throws(CertificateException::class) |
||||||
|
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) { |
||||||
|
} |
||||||
|
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate> { |
||||||
|
return arrayOf() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 此类是用于主机名验证的基接口。 在握手期间,如果 URL 的主机名和服务器的标识主机名不匹配, |
||||||
|
* 则验证机制可以回调此接口的实现程序来确定是否应该允许此连接。策略可以是基于证书的或依赖于其他验证方案。 |
||||||
|
* 当验证 URL 主机名使用的默认规则失败时使用这些回调。如果主机名是可接受的,则返回 true |
||||||
|
*/ |
||||||
|
val unsafeHostnameVerifier: HostnameVerifier = HostnameVerifier { _, _ -> true } |
||||||
|
|
||||||
|
class SSLParams { |
||||||
|
var sSLSocketFactory: SSLSocketFactory? = null |
||||||
|
var trustManager: X509TrustManager? = null |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* https单向认证 |
||||||
|
* 可以额外配置信任服务端的证书策略,否则默认是按CA证书去验证的,若不是CA可信任的证书,则无法通过验证 |
||||||
|
*/ |
||||||
|
fun getSslSocketFactory(trustManager: X509TrustManager): SSLParams { |
||||||
|
return getSslSocketFactoryBase(trustManager, null, null) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* https单向认证 |
||||||
|
* 用含有服务端公钥的证书校验服务端证书 |
||||||
|
*/ |
||||||
|
fun getSslSocketFactory(vararg certificates: InputStream): SSLParams { |
||||||
|
return getSslSocketFactoryBase(null, null, null, *certificates) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* https双向认证 |
||||||
|
* bksFile 和 password -> 客户端使用bks证书校验服务端证书 |
||||||
|
* certificates -> 用含有服务端公钥的证书校验服务端证书 |
||||||
|
*/ |
||||||
|
fun getSslSocketFactory(bksFile: InputStream, password: String, vararg certificates: InputStream): SSLParams { |
||||||
|
return getSslSocketFactoryBase(null, bksFile, password, *certificates) |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* https双向认证 |
||||||
|
* bksFile 和 password -> 客户端使用bks证书校验服务端证书 |
||||||
|
* X509TrustManager -> 如果需要自己校验,那么可以自己实现相关校验,如果不需要自己校验,那么传null即可 |
||||||
|
*/ |
||||||
|
fun getSslSocketFactory(bksFile: InputStream, password: String, trustManager: X509TrustManager): SSLParams { |
||||||
|
return getSslSocketFactoryBase(trustManager, bksFile, password) |
||||||
|
} |
||||||
|
|
||||||
|
private fun getSslSocketFactoryBase( |
||||||
|
trustManager: X509TrustManager?, |
||||||
|
bksFile: InputStream?, |
||||||
|
password: String?, |
||||||
|
vararg certificates: InputStream |
||||||
|
): SSLParams { |
||||||
|
val sslParams = SSLParams() |
||||||
|
try { |
||||||
|
val keyManagers = prepareKeyManager(bksFile, password) |
||||||
|
val trustManagers = prepareTrustManager(*certificates) |
||||||
|
val manager: X509TrustManager? |
||||||
|
manager = //优先使用用户自定义的TrustManager |
||||||
|
trustManager ?: if (trustManagers != null) { |
||||||
|
//然后使用默认的TrustManager |
||||||
|
chooseTrustManager(trustManagers) |
||||||
|
} else { |
||||||
|
//否则使用不安全的TrustManager |
||||||
|
unsafeTrustManager |
||||||
|
} |
||||||
|
// 创建TLS类型的SSLContext对象, that uses our TrustManager |
||||||
|
val sslContext = SSLContext.getInstance("TLS") |
||||||
|
// 用上面得到的trustManagers初始化SSLContext,这样sslContext就会信任keyStore中的证书 |
||||||
|
// 第一个参数是授权的密钥管理器,用来授权验证,比如授权自签名的证书验证。第二个是被授权的证书管理器,用来验证服务器端的证书 |
||||||
|
sslContext.init(keyManagers, manager?.let { arrayOf<TrustManager>(it) }, null) |
||||||
|
// 通过sslContext获取SSLSocketFactory对象 |
||||||
|
sslParams.sSLSocketFactory = sslContext.socketFactory |
||||||
|
sslParams.trustManager = manager |
||||||
|
return sslParams |
||||||
|
} catch (e: NoSuchAlgorithmException) { |
||||||
|
throw AssertionError(e) |
||||||
|
} catch (e: KeyManagementException) { |
||||||
|
throw AssertionError(e) |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
private fun prepareKeyManager(bksFile: InputStream?, password: String?): Array<KeyManager>? { |
||||||
|
try { |
||||||
|
if (bksFile == null || password == null) return null |
||||||
|
val clientKeyStore = KeyStore.getInstance("BKS") |
||||||
|
clientKeyStore.load(bksFile, password.toCharArray()) |
||||||
|
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()) |
||||||
|
kmf.init(clientKeyStore, password.toCharArray()) |
||||||
|
return kmf.keyManagers |
||||||
|
} catch (e: Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
private fun prepareTrustManager(vararg certificates: InputStream): Array<TrustManager>? { |
||||||
|
if (certificates.isEmpty()) return null |
||||||
|
try { |
||||||
|
val certificateFactory = CertificateFactory.getInstance("X.509") |
||||||
|
// 创建一个默认类型的KeyStore,存储我们信任的证书 |
||||||
|
val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) |
||||||
|
keyStore.load(null) |
||||||
|
var index = 0 |
||||||
|
for (certStream in certificates) { |
||||||
|
val certificateAlias = Integer.toString(index++) |
||||||
|
// 证书工厂根据证书文件的流生成证书 cert |
||||||
|
val cert = certificateFactory.generateCertificate(certStream) |
||||||
|
// 将 cert 作为可信证书放入到keyStore中 |
||||||
|
keyStore.setCertificateEntry(certificateAlias, cert) |
||||||
|
try { |
||||||
|
certStream?.close() |
||||||
|
} catch (e: IOException) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
//我们创建一个默认类型的TrustManagerFactory |
||||||
|
val tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()) |
||||||
|
//用我们之前的keyStore实例初始化TrustManagerFactory,这样tmf就会信任keyStore中的证书 |
||||||
|
tmf.init(keyStore) |
||||||
|
//通过tmf获取TrustManager数组,TrustManager也会信任keyStore中的证书 |
||||||
|
return tmf.trustManagers |
||||||
|
} catch (e: Exception) { |
||||||
|
e.printStackTrace() |
||||||
|
} |
||||||
|
|
||||||
|
return null |
||||||
|
} |
||||||
|
|
||||||
|
private fun chooseTrustManager(trustManagers: Array<TrustManager>): X509TrustManager? { |
||||||
|
for (trustManager in trustManagers) { |
||||||
|
if (trustManager is X509TrustManager) { |
||||||
|
return trustManager |
||||||
|
} |
||||||
|
} |
||||||
|
return null |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,53 @@ |
|||||||
|
package io.legado.app.ui.replacerule |
||||||
|
|
||||||
|
import android.os.Bundle |
||||||
|
import androidx.appcompat.app.AppCompatActivity |
||||||
|
import androidx.lifecycle.LiveData |
||||||
|
import androidx.lifecycle.Observer |
||||||
|
import androidx.paging.LivePagedListBuilder |
||||||
|
import androidx.paging.PagedList |
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.data.entities.ReplaceRule |
||||||
|
import kotlinx.android.synthetic.main.activity_replace_rule.* |
||||||
|
import org.jetbrains.anko.doAsync |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
|
||||||
|
|
||||||
|
class ReplaceRuleActivity : AppCompatActivity() { |
||||||
|
private lateinit var adapter: ReplaceRuleAdapter |
||||||
|
private var rulesLiveData: LiveData<PagedList<ReplaceRule>>? = null |
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
|
super.onCreate(savedInstanceState) |
||||||
|
setContentView(R.layout.activity_replace_rule) |
||||||
|
rv_replace_rule.layoutManager = LinearLayoutManager(this) |
||||||
|
initRecyclerView() |
||||||
|
initDataObservers() |
||||||
|
} |
||||||
|
|
||||||
|
private fun initRecyclerView() { |
||||||
|
rv_replace_rule.layoutManager = LinearLayoutManager(this) |
||||||
|
adapter = ReplaceRuleAdapter(this) |
||||||
|
adapter.onClickListener = object: ReplaceRuleAdapter.OnClickListener { |
||||||
|
override fun update(rule: ReplaceRule) { |
||||||
|
doAsync { App.db.replaceRuleDao().update(rule) } |
||||||
|
} |
||||||
|
override fun delete(rule: ReplaceRule) { |
||||||
|
doAsync { App.db.replaceRuleDao().delete(rule) } |
||||||
|
} |
||||||
|
override fun edit(rule: ReplaceRule) { |
||||||
|
toast("Edit function not implemented!") |
||||||
|
} |
||||||
|
} |
||||||
|
rv_replace_rule.adapter = adapter |
||||||
|
} |
||||||
|
|
||||||
|
private fun initDataObservers() { |
||||||
|
rulesLiveData?.removeObservers(this) |
||||||
|
rulesLiveData = LivePagedListBuilder(App.db.replaceRuleDao().observeAll(), 30).build() |
||||||
|
rulesLiveData?.observe(this, Observer<PagedList<ReplaceRule>> { adapter.submitList(it) }) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,70 @@ |
|||||||
|
package io.legado.app.ui.replacerule |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.view.LayoutInflater |
||||||
|
import android.view.View |
||||||
|
import android.view.ViewGroup |
||||||
|
import androidx.core.view.isGone |
||||||
|
import androidx.paging.PagedListAdapter |
||||||
|
import androidx.recyclerview.widget.DiffUtil |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.data.entities.ReplaceRule |
||||||
|
import kotlinx.android.synthetic.main.item_relace_rule.view.* |
||||||
|
import org.jetbrains.anko.sdk27.listeners.onClick |
||||||
|
|
||||||
|
|
||||||
|
class ReplaceRuleAdapter(context: Context) : |
||||||
|
PagedListAdapter<ReplaceRule, ReplaceRuleAdapter.MyViewHolder>(DIFF_CALLBACK) { |
||||||
|
|
||||||
|
var onClickListener: OnClickListener? = null |
||||||
|
|
||||||
|
companion object { |
||||||
|
|
||||||
|
@JvmField |
||||||
|
val DIFF_CALLBACK = object : DiffUtil.ItemCallback<ReplaceRule>() { |
||||||
|
override fun areItemsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean = |
||||||
|
oldItem.id == newItem.id |
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItem: ReplaceRule, newItem: ReplaceRule): Boolean = |
||||||
|
oldItem.id == newItem.id |
||||||
|
&& oldItem.pattern == newItem.pattern |
||||||
|
&& oldItem.replacement == newItem.replacement |
||||||
|
&& oldItem.isRegex == newItem.isRegex |
||||||
|
&& oldItem.isEnabled == newItem.isEnabled |
||||||
|
&& oldItem.scope == newItem.scope |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
init { |
||||||
|
notifyDataSetChanged() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { |
||||||
|
return MyViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.item_relace_rule, parent, false)) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: MyViewHolder, pos: Int) { |
||||||
|
getItem(pos)?.let { holder.bind(it, onClickListener, pos == itemCount - 1) } |
||||||
|
} |
||||||
|
|
||||||
|
class MyViewHolder(view: View) : RecyclerView.ViewHolder(view) { |
||||||
|
fun bind(rule: ReplaceRule, listener: OnClickListener?, hideDivider: Boolean) = with(itemView) { |
||||||
|
cb_enable.text = rule.name |
||||||
|
cb_enable.isChecked = rule.isEnabled |
||||||
|
divider.isGone = hideDivider |
||||||
|
iv_delete.onClick { listener?.delete(rule) } |
||||||
|
iv_edit.onClick { listener?.edit(rule) } |
||||||
|
cb_enable.onClick { |
||||||
|
rule.isEnabled = cb_enable.isChecked |
||||||
|
listener?.update(rule) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
interface OnClickListener { |
||||||
|
fun update(rule: ReplaceRule) |
||||||
|
fun delete(rule: ReplaceRule) |
||||||
|
fun edit(rule: ReplaceRule) |
||||||
|
} |
||||||
|
} |
@ -1,21 +1,19 @@ |
|||||||
package io.legado.app.ui.search |
package io.legado.app.ui.search |
||||||
|
|
||||||
import android.os.Bundle |
import android.os.Bundle |
||||||
import androidx.lifecycle.ViewModelProvider |
|
||||||
import io.legado.app.R |
import io.legado.app.R |
||||||
import io.legado.app.base.BaseActivity |
import io.legado.app.base.BaseActivity |
||||||
|
import io.legado.app.utils.getViewModel |
||||||
|
|
||||||
class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() { |
class SearchActivity : BaseActivity<SearchDataBinding, SearchViewModel>() { |
||||||
|
|
||||||
override val viewModel: SearchViewModel |
override val viewModel: SearchViewModel |
||||||
get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(SearchViewModel::class.java) |
get() = getViewModel(SearchViewModel::class.java) |
||||||
|
|
||||||
override val layoutID: Int |
override val layoutID: Int |
||||||
get() = R.layout.activity_search |
get() = R.layout.activity_search |
||||||
|
|
||||||
override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { |
override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { |
||||||
|
|
||||||
|
|
||||||
} |
} |
||||||
|
|
||||||
} |
} |
||||||
|
@ -0,0 +1,34 @@ |
|||||||
|
package io.legado.app.ui.search |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.base.adapter.ItemViewDelegate |
||||||
|
import io.legado.app.base.adapter.ItemViewHolder |
||||||
|
import io.legado.app.base.adapter.SimpleRecyclerAdapter |
||||||
|
import io.legado.app.data.entities.SearchBook |
||||||
|
import kotlinx.android.synthetic.main.item_search.view.* |
||||||
|
|
||||||
|
class SearchAdapter(context: Context) : SimpleRecyclerAdapter<SearchBook>(context) { |
||||||
|
|
||||||
|
init { |
||||||
|
addItemViewDelegate(TestItemDelegate(context)) |
||||||
|
} |
||||||
|
|
||||||
|
override val layoutID: Int |
||||||
|
get() = R.layout.item_search |
||||||
|
|
||||||
|
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) { |
||||||
|
holder.itemView.bookName.text = "我欲封天" |
||||||
|
} |
||||||
|
|
||||||
|
internal class TestItemDelegate(context: Context) : ItemViewDelegate<SearchBook>(context){ |
||||||
|
override val layoutID: Int |
||||||
|
get() = TODO("not implemented") //To change initializer of created properties use File | Settings | File Templates. |
||||||
|
|
||||||
|
override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList<Any>) { |
||||||
|
TODO("not implemented") //To change body of created functions use File | Settings | File Templates. |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,6 +1,39 @@ |
|||||||
package io.legado.app.ui.search |
package io.legado.app.ui.search |
||||||
|
|
||||||
import android.app.Application |
import android.app.Application |
||||||
import androidx.lifecycle.AndroidViewModel |
import android.util.Log |
||||||
|
import androidx.lifecycle.LiveData |
||||||
|
import androidx.lifecycle.MutableLiveData |
||||||
|
import io.legado.app.base.BaseViewModel |
||||||
|
import io.legado.app.data.api.CommonHttpApi |
||||||
|
import io.legado.app.data.entities.SearchBook |
||||||
|
import io.legado.app.help.http.HttpHelper |
||||||
|
import kotlinx.coroutines.Dispatchers.IO |
||||||
|
import kotlinx.coroutines.withContext |
||||||
|
|
||||||
class SearchViewModel(application: Application) : AndroidViewModel(application) |
class SearchViewModel(application: Application) : BaseViewModel(application) { |
||||||
|
|
||||||
|
val searchBooks: LiveData<List<SearchBook>> = MutableLiveData() |
||||||
|
|
||||||
|
public fun search(start: () -> Unit, finally: () -> Unit) { |
||||||
|
launchOnUI( |
||||||
|
{ |
||||||
|
start() |
||||||
|
val searchResponse = withContext(IO) { |
||||||
|
HttpHelper.getApiService( |
||||||
|
"http:www.baidu.com", |
||||||
|
CommonHttpApi::class.java |
||||||
|
).get("", mutableMapOf()) |
||||||
|
} |
||||||
|
|
||||||
|
val result = searchResponse.await() |
||||||
|
}, |
||||||
|
{ Log.i("TAG", "${it.message}") }, |
||||||
|
{ finally() }) |
||||||
|
|
||||||
|
// GlobalScope.launch { |
||||||
|
// |
||||||
|
// } |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
|
@ -0,0 +1,5 @@ |
|||||||
|
package io.legado.app.utils |
||||||
|
|
||||||
|
import android.os.Environment |
||||||
|
|
||||||
|
fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath |
@ -0,0 +1,9 @@ |
|||||||
|
package io.legado.app.utils |
||||||
|
|
||||||
|
import com.jayway.jsonpath.ReadContext |
||||||
|
|
||||||
|
fun ReadContext.readString(path: String) = this.read(path, String::class.java) |
||||||
|
|
||||||
|
fun ReadContext.readBool(path: String) = this.read(path, Boolean::class.java) |
||||||
|
|
||||||
|
fun ReadContext.readInt(path: String) = this.read(path, Int::class.java) |
@ -0,0 +1,12 @@ |
|||||||
|
package io.legado.app.utils |
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity |
||||||
|
import androidx.fragment.app.Fragment |
||||||
|
import androidx.lifecycle.ViewModel |
||||||
|
import androidx.lifecycle.ViewModelProviders |
||||||
|
|
||||||
|
fun <T : ViewModel> AppCompatActivity.getViewModel(clazz: Class<T>) = ViewModelProviders.of(this).get(clazz) |
||||||
|
|
||||||
|
fun <T : ViewModel> Fragment.getViewModel(clazz: Class<T>) = ViewModelProviders.of(this).get(clazz) |
||||||
|
|
||||||
|
fun <T : ViewModel> Fragment.getViewModelOfActivity(clazz: Class<T>) = ViewModelProviders.of(requireActivity()).get(clazz) |
@ -0,0 +1,4 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<selector xmlns:android="http://schemas.android.com/apk/res/android"> |
||||||
|
<item android:drawable="@android:color/transparent"/> |
||||||
|
</selector> |
@ -0,0 +1,17 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="24" |
||||||
|
android:viewportHeight="24"> |
||||||
|
|
||||||
|
<path |
||||||
|
android:fillColor="#595757" |
||||||
|
android:pathData="M14.434,20 L9.566,20 C7.535,20,5.863,18.665,5.761,16.961 L5.18,7.199 L4,7.199 L4,5.919 L8.402,5.919 L8.644,5.315 C8.957,4.526,9.827,4,10.813,4 L13.188,4 C14.172,4,15.043,4.526,15.355,5.313 L15.598,5.918 L20,5.918 L20,7.198 L18.82,7.198 L18.238,16.96 C18.137,18.665,16.466,20,14.434,20 Z M6.706,7.199 L7.283,16.898 C7.344,17.917,8.347,18.719,9.566,18.719 L14.433,18.719 C15.653,18.719,16.656,17.916,16.716,16.898 L17.293,7.199 L6.706,7.199 Z M10.01,5.919 L13.99,5.919 L13.91,5.718 C13.806,5.457,13.515,5.28,13.187,5.28 L10.812,5.28 C10.484,5.28,10.193,5.457,10.089,5.718 L10.01,5.919 Z" /> |
||||||
|
<path |
||||||
|
android:fillColor="#595757" |
||||||
|
android:pathData="M9.542,16.509 L9.127,8.83 L10.648,8.773 L11.064,16.449 L9.542,16.509 Z" /> |
||||||
|
<path |
||||||
|
android:fillColor="#595757" |
||||||
|
android:pathData="M14.457,16.509 L12.936,16.449 L13.352,8.773 L14.873,8.83 L14.457,16.509 Z" /> |
||||||
|
</vector> |
@ -0,0 +1,14 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
android:width="24dp" |
||||||
|
android:height="24dp" |
||||||
|
android:viewportWidth="24" |
||||||
|
android:viewportHeight="24"> |
||||||
|
|
||||||
|
<path |
||||||
|
android:fillColor="#595757" |
||||||
|
android:pathData="M5.6,20.009c-0.881,0-1.6-0.789-1.6-1.758V5.749C4,4.78,4.719,3.991,5.6,3.991h10.62 c0.882,0,1.599,0.789,1.599,1.758v1.258h-1.483l-0.076-1.258c0-0.122-0.062-0.172-0.062-0.172L5.6,5.582 c0.002,0.015-0.039,0.069-0.039,0.167v12.502c0,0.107,0.051,0.164,0.063,0.172l10.596-0.005c-0.002-0.014,0.039-0.067,0.039-0.167 l0.016-4.739h1.469l0.075,4.739c0,0.969-0.717,1.758-1.599,1.758H5.6z" /> |
||||||
|
<path |
||||||
|
android:fillColor="#595757" |
||||||
|
android:pathData="M 12.549 13.354 L 13.738 12.323 L 13.738 12.323 L 18.967 7.553 L 20 8.646 L 14.658 13.514 L 13.54 14.515 Z" /> |
||||||
|
</vector> |
@ -0,0 +1,30 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
tools:context="io.legado.app.ui.search.SearchActivity"> |
||||||
|
|
||||||
|
<io.legado.app.ui.widget.TitleBar |
||||||
|
android:id="@+id/titleBar" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
app:title="净化替换"/> |
||||||
|
|
||||||
|
<androidx.recyclerview.widget.RecyclerView |
||||||
|
android:id="@+id/rv_replace_rule" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="match_parent" |
||||||
|
android:paddingStart="8dp" |
||||||
|
android:paddingEnd="8dp" |
||||||
|
android:layout_marginBottom="8dp" |
||||||
|
app:layout_constraintStart_toStartOf="parent" |
||||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||||
|
app:layout_constraintTop_toBottomOf="@+id/titleBar" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintVertical_bias="0.0"/> |
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -0,0 +1,57 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:paddingStart="8dp" |
||||||
|
android:paddingEnd="8dp"> |
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatCheckBox |
||||||
|
android:id="@+id/cb_enable" |
||||||
|
android:text="Rule Name" |
||||||
|
android:layout_width="0dp" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:paddingTop="16dp" |
||||||
|
android:paddingBottom="16dp" |
||||||
|
android:textSize="16sp" |
||||||
|
android:textColor="@color/text_default" |
||||||
|
app:layout_constraintStart_toStartOf="parent" |
||||||
|
app:layout_constraintEnd_toStartOf="@+id/iv_delete" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" /> |
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView |
||||||
|
android:id="@+id/iv_edit" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:padding="8dp" |
||||||
|
android:background="@drawable/bg_ib_pre_round" |
||||||
|
android:contentDescription="@string/edit" |
||||||
|
android:src="@drawable/ic_edit" |
||||||
|
app:tint="@color/text_default" |
||||||
|
app:layout_constraintEnd_toStartOf="@id/iv_delete" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent"/> |
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatImageView |
||||||
|
android:id="@+id/iv_delete" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
android:padding="8dp" |
||||||
|
android:background="@drawable/bg_ib_pre_round" |
||||||
|
android:contentDescription="@string/delete" |
||||||
|
android:src="@drawable/ic_clear_all" |
||||||
|
app:tint="@color/text_default" |
||||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent" |
||||||
|
app:layout_constraintTop_toTopOf="parent"/> |
||||||
|
|
||||||
|
<View |
||||||
|
android:id="@+id/divider" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="0.5dp" |
||||||
|
android:background="@color/divider" |
||||||
|
app:layout_constraintBottom_toBottomOf="parent"/> |
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
@ -0,0 +1,19 @@ |
|||||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout |
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android" |
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto" |
||||||
|
android:layout_width="match_parent" |
||||||
|
android:layout_height="wrap_content"> |
||||||
|
|
||||||
|
|
||||||
|
<TextView |
||||||
|
android:id="@+id/bookName" |
||||||
|
app:layout_constraintStart_toStartOf="parent" |
||||||
|
app:layout_constraintEnd_toEndOf="parent" |
||||||
|
android:layout_width="wrap_content" |
||||||
|
android:layout_height="wrap_content" |
||||||
|
app:layout_constraintTop_toTopOf="parent" |
||||||
|
android:text="测试"/> |
||||||
|
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout> |
Loading…
Reference in new issue