From 7b0d3d0be2726dd2635aed7a61b82457d14ed9e5 Mon Sep 17 00:00:00 2001 From: Awesome <1760316362@qq.com> Date: Thu, 23 May 2019 21:24:21 +0800 Subject: [PATCH 01/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0BaseAdapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../app/base/adapter/CommonRecyclerAdapter.kt | 398 ++++++++++++++++++ .../legado/app/base/adapter/ItemAnimation.kt | 91 ++++ .../app/base/adapter/ItemViewDelegate.kt | 16 + .../legado/app/base/adapter/ItemViewHolder.kt | 9 + .../app/base/adapter/SimpleRecyclerAdapter.kt | 26 ++ .../adapter/animations/AlphaInAnimation.kt | 17 + .../base/adapter/animations/BaseAnimation.kt | 13 + .../adapter/animations/ScaleInAnimation.kt | 21 + .../animations/SlideInBottomAnimation.kt | 13 + .../animations/SlideInLeftAnimation.kt | 14 + .../animations/SlideInRightAnimation.kt | 14 + .../java/io/legado/app/help/LayoutManager.kt | 54 ++- .../io/legado/app/ui/search/SearchActivity.kt | 2 +- 13 files changed, 658 insertions(+), 30 deletions(-) create mode 100644 app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt create mode 100644 app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt new file mode 100644 index 000000000..c9b9f0cf2 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt @@ -0,0 +1,398 @@ +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(protected val context: Context) : RecyclerView.Adapter() { + + constructor(context: Context, vararg delegates: Pair>) : this(context) { + addItemViewDelegates(*delegates) + } + + constructor(context: Context, vararg delegates: ItemViewDelegate) : this(context) { + addItemViewDelegates(*delegates) + } + + private val inflater: LayoutInflater = LayoutInflater.from(context) + + private var headerItems: SparseArray? = null + private var footerItems: SparseArray? = null + + private val itemDelegates: HashMap> = hashMapOf() + private val items: MutableList = 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 > addItemViewDelegate(viewType: Int, delegate: DELEGATE) { + itemDelegates.put(viewType, delegate) + } + + fun > addItemViewDelegate(delegate: DELEGATE) { + itemDelegates.put(itemDelegates.size, delegate) + } + + fun > addItemViewDelegates(vararg delegates: DELEGATE) { + delegates.forEach { + addItemViewDelegate(it) + } + } + + fun addItemViewDelegates(vararg delegates: Pair>) { + 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?) { + 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) { + 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) { + 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) { + 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 = items[position % items.size] + + fun getItems(): List = 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 -> getItemViewType(getItem(getRealPosition(position)), getRealPosition(position)) + } + } + + 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 { + itemClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + } + } + + if (itemLongClickListener != null) { + holder.itemView.setOnLongClickListener { + itemLongClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + } + } + + holder + } + } + } + + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { + onBindViewHolder(holder, position, mutableListOf()) + } + + override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { + if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { + itemDelegates.getValue(getItemViewType(holder.layoutPosition)) + .convert(holder, getItem(holder.layoutPosition), 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 if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( + getItem(position), getItemViewType(position), position + ) + } + } + } + } + + fun setAnimationConfig(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 getRealPosition(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 + } + +} + + + + diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt new file mode 100644 index 000000000..9e6f20c61 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt @@ -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.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() + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt new file mode 100644 index 000000000..789ca250e --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt @@ -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(protected val context: Context) { + + abstract val layoutID: Int + + abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt new file mode 100644 index 000000000..c415fa4b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewHolder.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt new file mode 100644 index 000000000..176098f50 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt @@ -0,0 +1,26 @@ +package io.legado.app.base.adapter + +import android.content.Context + +/** + * Created by Invincible on 2017/12/15. + */ +abstract class SimpleRecyclerAdapter(context: Context) : CommonRecyclerAdapter(context) { + + init { + addItemViewDelegate(object : ItemViewDelegate(context) { + override val layoutID: Int + get() = this@SimpleRecyclerAdapter.layoutID + + override fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) { + this@SimpleRecyclerAdapter.convert(holder, item, payloads) + } + + }) + + } + + abstract val layoutID: Int + + abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt new file mode 100644 index 000000000..b307300a3 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/AlphaInAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f)) + + companion object { + + private const val DEFAULT_ALPHA_FROM = 0f + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt new file mode 100644 index 000000000..735ceca57 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/BaseAnimation.kt @@ -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 + +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt new file mode 100644 index 000000000..3f73e7a3c --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt @@ -0,0 +1,21 @@ +package io.legado.app.base.animations + +import android.animation.Animator +import android.animation.ObjectAnimator +import android.view.View +import io.legado.app.base.adapter.animations.BaseAnimation + + +class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation { + + override fun getAnimators(view: View): Array { + 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 + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt new file mode 100644 index 000000000..0fd2bfde6 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInBottomAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt new file mode 100644 index 000000000..daa6f17a1 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInLeftAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt new file mode 100644 index 000000000..71c8feac7 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/animations/SlideInRightAnimation.kt @@ -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 = + arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f)) +} diff --git a/app/src/main/java/io/legado/app/help/LayoutManager.kt b/app/src/main/java/io/legado/app/help/LayoutManager.kt index 9fc551222..e633993c7 100644 --- a/app/src/main/java/io/legado/app/help/LayoutManager.kt +++ b/app/src/main/java/io/legado/app/help/LayoutManager.kt @@ -6,7 +6,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.StaggeredGridLayoutManager -class LayoutManager private constructor() { +object LayoutManager { interface LayoutManagerFactory { fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager @@ -16,50 +16,46 @@ class LayoutManager private constructor() { @Retention(AnnotationRetention.SOURCE) annotation class Orientation - companion object { - - - fun linear(): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return LinearLayoutManager(recyclerView.context) - } + fun linear(): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context) } } + } - fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) - } + fun linear(@Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return LinearLayoutManager(recyclerView.context, orientation, reverseLayout) } } + } - fun grid(spanCount: Int): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return GridLayoutManager(recyclerView.context, spanCount) - } + fun grid(spanCount: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager(recyclerView.context, spanCount) } } + } - fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) - } + fun grid(spanCount: Int, @Orientation orientation: Int, reverseLayout: Boolean): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return GridLayoutManager(recyclerView.context, spanCount, orientation, reverseLayout) } } + } - fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { - return object : LayoutManagerFactory { - override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { - return StaggeredGridLayoutManager(spanCount, orientation) - } + fun staggeredGrid(spanCount: Int, @Orientation orientation: Int): LayoutManagerFactory { + return object : LayoutManagerFactory { + override fun create(recyclerView: RecyclerView): RecyclerView.LayoutManager { + return StaggeredGridLayoutManager(spanCount, orientation) } } } diff --git a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt index 2d4f64ba2..33be6236f 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt @@ -4,6 +4,7 @@ import android.os.Bundle import androidx.lifecycle.ViewModelProvider import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.help.LayoutManager class SearchActivity : BaseActivity() { @@ -15,7 +16,6 @@ class SearchActivity : BaseActivity() { override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { - } } From 57acb76df651ff832bc03598f25ef9bc125c7821 Mon Sep 17 00:00:00 2001 From: Awesome <1760316362@qq.com> Date: Thu, 23 May 2019 22:21:47 +0800 Subject: [PATCH 02/10] TitleBar --- .../base/adapter/InfiniteScrollListener.kt | 32 +++++++++++++++++ .../legado/app/base/adapter/ItemAnimation.kt | 2 +- .../adapter/animations/ScaleInAnimation.kt | 3 +- .../io/legado/app/data/entities/SearchBook.kt | 1 + .../io/legado/app/ui/main/MainActivity.kt | 4 +-- .../io/legado/app/ui/search/SearchActivity.kt | 1 - .../io/legado/app/ui/search/SearchAdapter.kt | 34 +++++++++++++++++++ .../java/io/legado/app/ui/widget/TitleBar.kt | 18 +++++++--- app/src/main/res/layout/app_bar_main.xml | 15 ++------ app/src/main/res/layout/item_search.xml | 19 +++++++++++ app/src/main/res/layout/view_titlebar.xml | 1 + app/src/main/res/values/attrs.xml | 2 ++ 12 files changed, 110 insertions(+), 22 deletions(-) create mode 100644 app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt create mode 100644 app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt create mode 100644 app/src/main/res/layout/item_search.xml diff --git a/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt new file mode 100644 index 000000000..fc7c48bab --- /dev/null +++ b/app/src/main/java/io/legado/app/base/adapter/InfiniteScrollListener.kt @@ -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 + } +} diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt index 9e6f20c61..6db4a7415 100644 --- a/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt +++ b/app/src/main/java/io/legado/app/base/adapter/ItemAnimation.kt @@ -4,7 +4,7 @@ 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.animations.ScaleInAnimation +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 diff --git a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt index 3f73e7a3c..3464f4079 100644 --- a/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt +++ b/app/src/main/java/io/legado/app/base/adapter/animations/ScaleInAnimation.kt @@ -1,9 +1,8 @@ -package io.legado.app.base.animations +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 ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) : BaseAnimation { diff --git a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt index feecd7295..8994f14bd 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt @@ -1,2 +1,3 @@ package io.legado.app.data.entities +class SearchBook \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index d04315dd6..cf4a2329d 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -14,6 +14,7 @@ import io.legado.app.base.BaseActivity import io.legado.app.ui.search.SearchActivity import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* +import kotlinx.android.synthetic.main.view_titlebar.* class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { override val viewModel: MainViewModel @@ -22,11 +23,10 @@ class MainActivity : BaseActivity(), NavigationV get() = R.layout.activity_main override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) { - setSupportActionBar(toolbar) fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) } val toggle = ActionBarDrawerToggle( - this, drawer_layout, toolbar, + this, drawer_layout, titleBar.toolbar, R.string.navigation_drawer_open, R.string.navigation_drawer_close ) diff --git a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt index 33be6236f..14994c535 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt @@ -15,7 +15,6 @@ class SearchActivity : BaseActivity() { get() = R.layout.activity_search override fun onViewModelCreated(viewModel: SearchViewModel, savedInstanceState: Bundle?) { - } } diff --git a/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt new file mode 100644 index 000000000..a23ee7605 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/search/SearchAdapter.kt @@ -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(context) { + + init { + addItemViewDelegate(TestItemDelegate(context)) + } + + override val layoutID: Int + get() = R.layout.item_search + + override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + holder.itemView.bookName.text = "我欲封天" + } + + internal class TestItemDelegate(context: Context) : ItemViewDelegate(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) { + TODO("not implemented") //To change body of created functions use File | Settings | File Templates. + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index e7df5b914..b354266eb 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -5,7 +5,10 @@ import android.content.res.ColorStateList import android.graphics.PorterDuff import android.graphics.drawable.Drawable import android.util.AttributeSet +import android.view.Menu +import android.view.View import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar import androidx.core.graphics.drawable.DrawableCompat import com.google.android.material.appbar.AppBarLayout import io.legado.app.R @@ -13,11 +16,16 @@ import kotlinx.android.synthetic.main.view_titlebar.view.* class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, attrs) { + val toolbar: Toolbar + val menu: Menu + get() = toolbar.menu + init { inflate(context, R.layout.view_titlebar, this) + toolbar = findViewById(R.id.toolbar) val a = context.obtainStyledAttributes( attrs, R.styleable.TitleBar, - 0, 0 + R.attr.titleBarStyle, 0 ) val navigationIcon = a.getDrawable(R.styleable.TitleBar_navigationIcon) val navigationContentDescription = a.getText(R.styleable.TitleBar_navigationContentDescription) @@ -40,13 +48,15 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a } } + fun setNavigationOnClickListener(clickListener: ((View) -> Unit)){ + toolbar.setNavigationOnClickListener(clickListener) + } + private fun attachToActivity(context: Context) { val activity = getCompatActivity(context) activity?.let { activity.setSupportActionBar(toolbar) - activity.supportActionBar?.let { - it.setDisplayHomeAsUpEnabled(true) - } + activity.supportActionBar?.setDisplayHomeAsUpEnabled(true) } } diff --git a/app/src/main/res/layout/app_bar_main.xml b/app/src/main/res/layout/app_bar_main.xml index ef693f0b6..ae86cf117 100644 --- a/app/src/main/res/layout/app_bar_main.xml +++ b/app/src/main/res/layout/app_bar_main.xml @@ -7,19 +7,10 @@ android:layout_height="match_parent" tools:context=".ui.main.MainActivity"> - - - - - + android:layout_height="wrap_content"/> diff --git a/app/src/main/res/layout/item_search.xml b/app/src/main/res/layout/item_search.xml new file mode 100644 index 000000000..4fe3215d2 --- /dev/null +++ b/app/src/main/res/layout/item_search.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_titlebar.xml b/app/src/main/res/layout/view_titlebar.xml index f1befb3b3..82f286582 100644 --- a/app/src/main/res/layout/view_titlebar.xml +++ b/app/src/main/res/layout/view_titlebar.xml @@ -5,4 +5,5 @@ android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" + android:theme="@style/AppTheme.AppBarOverlay" app:popupTheme="@style/AppTheme.PopupOverlay"/> diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 7456589c4..942cea81c 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -29,6 +29,8 @@ + + From b8175f9fbb326cc5bdbff8946b52e1552e43ebd9 Mon Sep 17 00:00:00 2001 From: Awesome <1760316362@qq.com> Date: Thu, 23 May 2019 23:10:07 +0800 Subject: [PATCH 03/10] TitleBar --- .../java/io/legado/app/ui/widget/TitleBar.kt | 31 +++++++++++++++++-- app/src/main/res/layout/activity_search.xml | 2 +- app/src/main/res/values/attrs.xml | 6 ++++ 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index b354266eb..be1fafbf7 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -33,16 +33,43 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a val navigationIconTintMode = a.getInt(R.styleable.TitleBar_navigationIconTintMode, 9) val showNavigationIcon = a.getBoolean(R.styleable.TitleBar_showNavigationIcon, true) val attachToActivity = a.getBoolean(R.styleable.TitleBar_attachToActivity, true) + val titleText = a.getString(R.styleable.TitleBar_title) + val subtitleText = a.getString(R.styleable.TitleBar_subtitle) a.recycle() - if (showNavigationIcon) { - toolbar.apply { + toolbar.apply { + if(showNavigationIcon){ this.navigationIcon = navigationIcon this.navigationContentDescription = navigationContentDescription wrapDrawableTint(this.navigationIcon, navigationIconTint, navigationIconTintMode) } + + if (a.hasValue(R.styleable.TitleBar_titleTextAppearance)) { + this.setTitleTextAppearance(context, a.getResourceId(R.styleable.TitleBar_titleTextAppearance, 0)) + } + + if (a.hasValue(R.styleable.TitleBar_titleTextColor)) { + this.setTitleTextColor(a.getColor(R.styleable.TitleBar_titleTextColor, -0x1)) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextAppearance)) { + this.setSubtitleTextAppearance(context, a.getResourceId(R.styleable.TitleBar_subtitleTextAppearance, 0)) + } + + if (a.hasValue(R.styleable.TitleBar_subtitleTextColor)) { + this.setSubtitleTextColor(a.getColor(R.styleable.TitleBar_subtitleTextColor, -0x1)) + } + + if(!titleText.isNullOrBlank()){ + this.title = titleText + } + + if(!subtitleText.isNullOrBlank()){ + this.subtitle = subtitleText + } } + if (attachToActivity) { attachToActivity(context) } diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index a7dfaad9b..da4d4a9c6 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -17,7 +17,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" app:layout_constraintTop_toTopOf="parent" - /> + app:title="搜索"/> + + + + + + From 7441fcd156ee26f2e04db21688e75580c8efa03e Mon Sep 17 00:00:00 2001 From: Awesome <1760316362@qq.com> Date: Thu, 23 May 2019 23:14:36 +0800 Subject: [PATCH 04/10] TitleBar --- .../java/io/legado/app/ui/widget/TitleBar.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt index be1fafbf7..548c22ec5 100644 --- a/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt +++ b/app/src/main/java/io/legado/app/ui/widget/TitleBar.kt @@ -7,6 +7,8 @@ import android.graphics.drawable.Drawable import android.util.AttributeSet import android.view.Menu import android.view.View +import androidx.annotation.ColorInt +import androidx.annotation.StyleRes import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.widget.Toolbar import androidx.core.graphics.drawable.DrawableCompat @@ -79,6 +81,38 @@ class TitleBar(context: Context, attrs: AttributeSet?) : AppBarLayout(context, a toolbar.setNavigationOnClickListener(clickListener) } + fun setTitle(title: CharSequence?) { + toolbar.title = title + } + + fun setTitle(titleId: Int) { + toolbar.setTitle(titleId) + } + + fun setSubTitle(subtitle: CharSequence?) { + toolbar.subtitle = subtitle + } + + fun setSubTitle(subtitleId: Int) { + toolbar.setSubtitle(subtitleId) + } + + fun setTitleTextColor(@ColorInt color: Int){ + toolbar.setTitleTextColor(color) + } + + fun setTitleTextAppearance(@StyleRes resId: Int){ + toolbar.setTitleTextAppearance(context, resId) + } + + fun setSubTitleTextColor(@ColorInt color: Int){ + toolbar.setSubtitleTextColor(color) + } + + fun setSubTitleTextAppearance(@StyleRes resId: Int){ + toolbar.setSubtitleTextAppearance(context, resId) + } + private fun attachToActivity(context: Context) { val activity = getCompatActivity(context) activity?.let { From 2f33336d748646ec47ab7f86190645ec4940cfd2 Mon Sep 17 00:00:00 2001 From: atbest Date: Thu, 23 May 2019 21:17:40 -0400 Subject: [PATCH 05/10] Added replace rule --- app/build.gradle | 5 + app/src/main/AndroidManifest.xml | 1 + app/src/main/java/io/legado/app/App.kt | 6 ++ .../java/io/legado/app/constant/AppConst.kt | 4 + .../java/io/legado/app/data/AppDatabase.kt | 2 + .../io/legado/app/data/dao/ReplaceRuleDao.kt | 49 ++++++++++ .../legado/app/data/entities/ReplaceRule.kt | 18 ++-- .../io/legado/app/ui/main/MainActivity.kt | 93 +++++++++++++++++-- .../app/ui/replacerule/ReplaceRuleActivity.kt | 53 +++++++++++ .../app/ui/replacerule/ReplaceRuleAdapter.kt | 70 ++++++++++++++ .../java/io/legado/app/utils/FileUtils.kt | 5 + .../io/legado/app/utils/MiscExtensions.kt | 9 ++ app/src/main/res/drawable/bg_ib_pre_round.xml | 4 + app/src/main/res/drawable/ic_clear_all.xml | 17 ++++ app/src/main/res/drawable/ic_edit.xml | 14 +++ .../main/res/layout/activity_replace_rule.xml | 30 ++++++ app/src/main/res/layout/item_relace_rule.xml | 57 ++++++++++++ .../main/res/menu/activity_main_drawer.xml | 20 ++-- app/src/main/res/values/colors.xml | 2 + app/src/main/res/values/strings.xml | 13 ++- 20 files changed, 439 insertions(+), 33 deletions(-) create mode 100644 app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt create mode 100644 app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt create mode 100644 app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt create mode 100644 app/src/main/java/io/legado/app/utils/FileUtils.kt create mode 100644 app/src/main/java/io/legado/app/utils/MiscExtensions.kt create mode 100644 app/src/main/res/drawable/bg_ib_pre_round.xml create mode 100644 app/src/main/res/drawable/ic_clear_all.xml create mode 100644 app/src/main/res/drawable/ic_edit.xml create mode 100644 app/src/main/res/layout/activity_replace_rule.xml create mode 100644 app/src/main/res/layout/item_relace_rule.xml diff --git a/app/build.gradle b/app/build.gradle index 1c6c7a570..3a59b113a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -69,6 +69,11 @@ dependencies { //协程 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' + implementation 'pub.devrel:easypermissions:3.0.0' + implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.jayway.jsonpath:json-path:2.4.0' + implementation 'org.jsoup:jsoup:1.12.1' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 10ee7aea6..7c5e32f4a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -39,6 +39,7 @@ + \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index d94919f22..6ef0e877d 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -1,6 +1,7 @@ package io.legado.app import android.app.Application +import io.legado.app.data.AppDatabase class App : Application() { @@ -8,10 +9,15 @@ class App : Application() { @JvmStatic lateinit var INSTANCE: App private set + + @JvmStatic + lateinit var db: AppDatabase + private set } override fun onCreate() { super.onCreate() INSTANCE = this + db = AppDatabase.createDatabase(INSTANCE) } } diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt index 6f9e7b6b9..89a1d204f 100644 --- a/app/src/main/java/io/legado/app/constant/AppConst.kt +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -4,4 +4,8 @@ object AppConst { const val channelIdDownload = "channel_download" const val channelIdReadAloud = "channel_read_aloud" const val channelIdWeb = "channel_web" + + const val APP_TAG = "Legado" + const val RC_IMPORT_YUEDU_DATA = 100 + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index 327651493..f5db6c908 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -7,6 +7,7 @@ import androidx.room.RoomDatabase import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import io.legado.app.data.dao.BookDao +import io.legado.app.data.dao.ReplaceRuleDao import io.legado.app.data.entities.Book import io.legado.app.data.entities.Chapter import io.legado.app.data.entities.ReplaceRule @@ -51,5 +52,6 @@ abstract class AppDatabase : RoomDatabase() { } abstract fun bookDao(): BookDao + abstract fun replaceRuleDao(): ReplaceRuleDao } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt new file mode 100644 index 000000000..9b92e4c6a --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt @@ -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 + + @Query("SELECT id FROM replace_rules ORDER BY sortOrder ASC") + fun observeAllIds(): LiveData> + + @get:Query("SELECT MAX(sortOrder) FROM replace_rules") + val maxOrder: Int + + @get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC") + val all: List + + @get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC") + val allEnabled: List + + @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 + + @Query("SELECT * FROM replace_rules WHERE isEnabled = 1 AND scope LIKE '%' || :scope || '%'") + fun findEnabledByScope(scope: String): List + + @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) + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt index 2436fb641..8b5a0fda6 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt @@ -1,6 +1,7 @@ package io.legado.app.data.entities import android.os.Parcelable +import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.Index import androidx.room.PrimaryKey @@ -11,14 +12,15 @@ import kotlinx.android.parcel.Parcelize indices = [(Index(value = ["id"]))]) data class ReplaceRule( @PrimaryKey(autoGenerate = true) - val id: Int = 0, - val summary: String? = null, - val pattern: String? = null, - val replacement: String? = null, - val scope: String? = null, - val isEnabled: Boolean = true, - val isRegex: Boolean = true, - val order: Int = 0 + var id: Int = 0, + var name: String? = null, + var pattern: String? = null, + var replacement: String? = null, + var scope: String? = null, + var isEnabled: Boolean = true, + var isRegex: Boolean = true, + @ColumnInfo(name = "sortOrder") + var order: Int = 0 ) : Parcelable diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index cf4a2329d..db76d4916 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -1,7 +1,9 @@ package io.legado.app.ui.main +import android.Manifest import android.content.Intent import android.os.Bundle +import android.util.Log import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle @@ -9,18 +11,37 @@ import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout import androidx.lifecycle.ViewModelProvider import com.google.android.material.navigation.NavigationView +import com.jayway.jsonpath.Configuration +import com.jayway.jsonpath.JsonPath +import com.jayway.jsonpath.Option +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseActivity +import io.legado.app.constant.AppConst.APP_TAG +import io.legado.app.constant.AppConst.RC_IMPORT_YUEDU_DATA +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.search.SearchActivity +import io.legado.app.utils.getSdPath +import io.legado.app.utils.readBool +import io.legado.app.utils.readInt +import io.legado.app.utils.readString import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* -import kotlinx.android.synthetic.main.view_titlebar.* +import org.jetbrains.anko.doAsync +import org.jetbrains.anko.startActivity +import org.jetbrains.anko.uiThread +import pub.devrel.easypermissions.AfterPermissionGranted +import pub.devrel.easypermissions.EasyPermissions +import java.io.File +import java.lang.Exception class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { override val viewModel: MainViewModel get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java) override val layoutID: Int get() = R.layout.activity_main + private val PERMISSONS = arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE) override fun onViewModelCreated(viewModel: MainViewModel, savedInstanceState: Bundle?) { fab.setOnClickListener { startActivity(Intent(this, SearchActivity::class.java)) } @@ -58,21 +79,17 @@ class MainActivity : BaseActivity(), NavigationV override fun onNavigationItemSelected(item: MenuItem): Boolean { // Handle navigation view item clicks here. when (item.itemId) { - R.id.nav_home -> { + R.id.nav_backup -> { // Handle the camera action } - R.id.nav_gallery -> { + R.id.nav_import -> { } - R.id.nav_slideshow -> { - - } - R.id.nav_tools -> { - - } - R.id.nav_share -> { + R.id.nav_import_old -> importYueDu() + R.id.nav_import_github -> { } + R.id.nav_replace_rule -> startActivity() R.id.nav_send -> { } @@ -81,4 +98,60 @@ class MainActivity : BaseActivity(), NavigationV drawerLayout.closeDrawer(GravityCompat.START) return true } + + /* + * import from YueDu backup data + * */ + @AfterPermissionGranted(RC_IMPORT_YUEDU_DATA) + fun importYueDu() { + if (!EasyPermissions.hasPermissions(this, Manifest.permission.WRITE_EXTERNAL_STORAGE)) { + EasyPermissions.requestPermissions(this, getString(R.string.perm_request_storage), RC_IMPORT_YUEDU_DATA, Manifest.permission.WRITE_EXTERNAL_STORAGE) + return + } + + val yuedu = File(getSdPath(), "YueDu") + val jsonPath = JsonPath.using(Configuration.builder() + .options(Option.SUPPRESS_EXCEPTIONS) + .build()) + + // Replace rules + val rFile = File(yuedu, "myBookReplaceRule.json") + val replaceRules = mutableListOf() + if (rFile.exists()) try { + val items: List> = jsonPath.parse(rFile.readText()).read("$.*") + for (item in items) { + val jsonItem = jsonPath.parse(item) + val rRule = ReplaceRule() + rRule.name = jsonItem.readString("$.replaceSummary") + rRule.pattern = jsonItem.readString("$.regex") + rRule.replacement = jsonItem.readString("$.replacement") + rRule.isRegex = jsonItem.readBool("$.isRegex") + rRule.scope = jsonItem.readString("$.useTo") + rRule.isEnabled = jsonItem.readBool("$.enable") + rRule.order = jsonItem.readInt("$.serialNumber") + replaceRules.add(rRule) + // Log.e(APP_TAG, rRule.toString()) + } + + doAsync { + App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) + val count = App.db.replaceRuleDao().all.size + val maxId = App.db.replaceRuleDao().maxOrder + uiThread { + Log.e(APP_TAG, "$count records were inserted to database, and max id is $maxId.") + } + } + + } catch (e: Exception) { + Log.e(APP_TAG, e.localizedMessage) + } + } + + + override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + // Forward results to EasyPermissions + EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this) + } + + } diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt new file mode 100644 index 000000000..304000c53 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt @@ -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>? = 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> { adapter.submitList(it) }) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt new file mode 100644 index 000000000..717e2bfff --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt @@ -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(DIFF_CALLBACK) { + + var onClickListener: OnClickListener? = null + + companion object { + + @JvmField + val DIFF_CALLBACK = object : DiffUtil.ItemCallback() { + 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) + } +} diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt new file mode 100644 index 000000000..493347429 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt @@ -0,0 +1,5 @@ +package io.legado.app.utils + +import android.os.Environment + +fun getSdPath() = Environment.getExternalStorageDirectory().absolutePath diff --git a/app/src/main/java/io/legado/app/utils/MiscExtensions.kt b/app/src/main/java/io/legado/app/utils/MiscExtensions.kt new file mode 100644 index 000000000..24bf95c9a --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/MiscExtensions.kt @@ -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) \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_ib_pre_round.xml b/app/src/main/res/drawable/bg_ib_pre_round.xml new file mode 100644 index 000000000..a370bb64b --- /dev/null +++ b/app/src/main/res/drawable/bg_ib_pre_round.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_clear_all.xml b/app/src/main/res/drawable/ic_clear_all.xml new file mode 100644 index 000000000..dfa860c00 --- /dev/null +++ b/app/src/main/res/drawable/ic_clear_all.xml @@ -0,0 +1,17 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_edit.xml b/app/src/main/res/drawable/ic_edit.xml new file mode 100644 index 000000000..2d249dde4 --- /dev/null +++ b/app/src/main/res/drawable/ic_edit.xml @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_replace_rule.xml b/app/src/main/res/layout/activity_replace_rule.xml new file mode 100644 index 000000000..773e22242 --- /dev/null +++ b/app/src/main/res/layout/activity_replace_rule.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/layout/item_relace_rule.xml b/app/src/main/res/layout/item_relace_rule.xml new file mode 100644 index 000000000..80295a529 --- /dev/null +++ b/app/src/main/res/layout/item_relace_rule.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + diff --git a/app/src/main/res/menu/activity_main_drawer.xml b/app/src/main/res/menu/activity_main_drawer.xml index 9df2b3d76..3a491985e 100644 --- a/app/src/main/res/menu/activity_main_drawer.xml +++ b/app/src/main/res/menu/activity_main_drawer.xml @@ -5,29 +5,29 @@ + android:title="@string/menu_backup"/> + android:title="@string/menu_import"/> + android:title="@string/menu_import_old"/> + android:title="@string/menu_import_github"/> + android:title="@string/menu_replace_rule"/> #008577 #00574B #D81B60 + #222222 + #66666666 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c7def64d4..05315f8a9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,14 +7,17 @@ Navigation header Settings - Home - Gallery - Slideshow - Tools - Share + Home + 导入 + 导入阅读数据 + 导入Github数据 + 净化替换 Send 点击重试 正在加载 + 阅读需要访问存储卡权限: + 编辑 + 删除 From f221e23641d1ce526d14f8679cd9de20a4542ffd Mon Sep 17 00:00:00 2001 From: Invinciblelee <1760316362@qq.com> Date: Fri, 24 May 2019 09:46:10 +0800 Subject: [PATCH 06/10] ViewModelExtensions --- .../main/java/io/legado/app/ui/main/MainActivity.kt | 6 +++--- .../java/io/legado/app/ui/search/SearchActivity.kt | 6 +++--- .../java/io/legado/app/utils/ViewModelExtensions.kt | 12 ++++++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index cf4a2329d..a1a9cdd76 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -7,18 +7,18 @@ import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.core.view.GravityCompat import androidx.drawerlayout.widget.DrawerLayout -import androidx.lifecycle.ViewModelProvider import com.google.android.material.navigation.NavigationView import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.ui.search.SearchActivity +import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.activity_main.* import kotlinx.android.synthetic.main.app_bar_main.* -import kotlinx.android.synthetic.main.view_titlebar.* class MainActivity : BaseActivity(), NavigationView.OnNavigationItemSelectedListener { override val viewModel: MainViewModel - get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(MainViewModel::class.java) + get() = getViewModel(MainViewModel::class.java) + override val layoutID: Int get() = R.layout.activity_main diff --git a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt index 14994c535..27a8450d8 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt @@ -1,15 +1,15 @@ package io.legado.app.ui.search import android.os.Bundle -import androidx.lifecycle.ViewModelProvider import io.legado.app.R import io.legado.app.base.BaseActivity -import io.legado.app.help.LayoutManager +import io.legado.app.search.SearchDataBinding +import io.legado.app.utils.getViewModel class SearchActivity : BaseActivity() { override val viewModel: SearchViewModel - get() = ViewModelProvider.AndroidViewModelFactory.getInstance(application).create(SearchViewModel::class.java) + get() = getViewModel(SearchViewModel::class.java) override val layoutID: Int get() = R.layout.activity_search diff --git a/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt new file mode 100644 index 000000000..ed963ec8e --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ViewModelExtensions.kt @@ -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 AppCompatActivity.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz) + +fun Fragment.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz) + +fun Fragment.getViewModelOfActivity(clazz: Class) = ViewModelProviders.of(requireActivity()).get(clazz) From 1c82acdf982459f29b1c55e494f66b3cf62744cc Mon Sep 17 00:00:00 2001 From: Invinciblelee <1760316362@qq.com> Date: Fri, 24 May 2019 09:52:49 +0800 Subject: [PATCH 07/10] ViewModelExtensions --- .../java/io/legado/app/ui/main/MainActivity.kt | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index a1a9cdd76..ce40a7e33 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -58,21 +58,6 @@ class MainActivity : BaseActivity(), NavigationV override fun onNavigationItemSelected(item: MenuItem): Boolean { // Handle navigation view item clicks here. when (item.itemId) { - R.id.nav_home -> { - // Handle the camera action - } - R.id.nav_gallery -> { - - } - R.id.nav_slideshow -> { - - } - R.id.nav_tools -> { - - } - R.id.nav_share -> { - - } R.id.nav_send -> { } From 654d510efdca5e331d2079b04d0870dc8b6a0c87 Mon Sep 17 00:00:00 2001 From: Invinciblelee <1760316362@qq.com> Date: Fri, 24 May 2019 09:53:58 +0800 Subject: [PATCH 08/10] ViewModelExtensions --- app/src/main/java/io/legado/app/ui/search/SearchActivity.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt index 27a8450d8..226c9207d 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchActivity.kt @@ -3,7 +3,6 @@ package io.legado.app.ui.search import android.os.Bundle import io.legado.app.R import io.legado.app.base.BaseActivity -import io.legado.app.search.SearchDataBinding import io.legado.app.utils.getViewModel class SearchActivity : BaseActivity() { From b8bc0e8b595766ada7f1561df82ae3b067b4f04f Mon Sep 17 00:00:00 2001 From: Invinciblelee <1760316362@qq.com> Date: Fri, 24 May 2019 12:06:20 +0800 Subject: [PATCH 09/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0okhttp,=20retrofit?= =?UTF-8?q?=E9=85=8D=E5=90=88=E5=8D=8F=E7=A8=8B=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/build.gradle | 13 +- .../java/io/legado/app/base/BaseViewModel.kt | 58 ++++++ .../app/base/adapter/CommonRecyclerAdapter.kt | 35 ++-- .../help/http/CoroutineCallAdapterFactory.kt | 107 +++++++++++ .../io/legado/app/help/http/HttpHelper.kt | 49 +++++ .../java/io/legado/app/help/http/SSLHelper.kt | 175 ++++++++++++++++++ .../legado/app/ui/search/SearchViewModel.kt | 16 +- app/src/main/res/layout/activity_search.xml | 1 - 8 files changed, 436 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/io/legado/app/base/BaseViewModel.kt create mode 100644 app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt create mode 100644 app/src/main/java/io/legado/app/help/http/HttpHelper.kt create mode 100644 app/src/main/java/io/legado/app/help/http/SSLHelper.kt diff --git a/app/build.gradle b/app/build.gradle index 3a59b113a..966ba8516 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -41,6 +41,12 @@ kapt { } } +kotlin{ + experimental{ + coroutines "enable" + } +} + dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" @@ -68,12 +74,17 @@ dependencies { implementation "org.jetbrains.anko:anko-sdk27-listeners:$anko_version" //协程 implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.2.1' - + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.2.1' + //规则相关 implementation 'pub.devrel:easypermissions:3.0.0' implementation 'com.google.code.gson:gson:2.8.5' implementation 'com.jayway.jsonpath:json-path:2.4.0' implementation 'org.jsoup:jsoup:1.12.1' + //Retrofit + implementation 'com.squareup.okhttp3:logging-interceptor:3.14.0'// + implementation 'com.squareup.retrofit2:retrofit:2.5.0' + testImplementation 'junit:junit:4.12' androidTestImplementation 'androidx.test:runner:1.1.1' androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt new file mode 100644 index 000000000..009aafe7d --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -0,0 +1,58 @@ +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 = mutableListOf() + + protected fun launchOnUI( + tryBlock: suspend CoroutineScope.() -> Unit, + cacheBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit, + handleCancellationExceptionManually: Boolean + ) { + launchOnUI { + tryCatch(tryBlock, cacheBlock, finallyBlock, handleCancellationExceptionManually) + } + } + + /** + * 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, + catchBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit, + handleCancellationExceptionManually: Boolean = false + ) { + try { + coroutineScope { tryBlock() } + } catch (e: Throwable) { + if (e !is CancellationException || handleCancellationExceptionManually) { + coroutineScope { catchBlock(e) } + } else { + throw e + } + } finally { + coroutineScope { finallyBlock() } + } + } + + override fun onCleared() { + super.onCleared() + launchManager.clear() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt index c9b9f0cf2..ce7b3d4a6 100644 --- a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt @@ -257,7 +257,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return footerItems?.size() ?: 0 } - fun getItem(position: Int): ITEM = items[position % items.size] + fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null fun getItems(): List = items @@ -280,7 +280,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return when { isHeader(position) -> TYPE_HEADER_VIEW + position isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() - else -> getItemViewType(getItem(getRealPosition(position)), getRealPosition(position)) + else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0 } } @@ -299,13 +299,17 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec if (itemClickListener != null) { holder.itemView.setOnClickListener { - itemClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + getItem(holder.layoutPosition)?.let { + itemClickListener?.invoke(holder, it) + } } } if (itemLongClickListener != null) { holder.itemView.setOnLongClickListener { - itemLongClickListener!!.invoke(holder, getItem(holder.layoutPosition)) + getItem(holder.layoutPosition)?.let { + itemLongClickListener?.invoke(holder, it) ?: true + } ?: true } } @@ -315,14 +319,15 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec } - override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { - onBindViewHolder(holder, position, mutableListOf()) + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { } - override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { + final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { - itemDelegates.getValue(getItemViewType(holder.layoutPosition)) - .convert(holder, getItem(holder.layoutPosition), payloads) + getItem(holder.layoutPosition)?.let { + itemDelegates.getValue(getItemViewType(holder.layoutPosition)) + .convert(holder, it, payloads) + } } } @@ -339,15 +344,17 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec if (manager is GridLayoutManager) { manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() { override fun getSpanSize(position: Int): Int { - return if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( - getItem(position), getItemViewType(position), position - ) + return getItem(position)?.let { + if (isHeader(position) || isFooter(position)) manager.spanCount else getSpanSize( + it, getItemViewType(position), position + ) + } ?: manager.spanCount } } } } - fun setAnimationConfig(item: ItemAnimation) { + fun setItemAnimation(item: ItemAnimation) { itemAnimation = item } @@ -359,7 +366,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return position >= getActualItemCount() + getHeaderCount() } - private fun getRealPosition(position: Int): Int { + private fun getActualPosition(position: Int): Int { return position - getHeaderCount() } diff --git a/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt b/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt new file mode 100644 index 000000000..78e0aa7a0 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt @@ -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 CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() { + companion object { + @JvmStatic @JvmName("create") + operator fun invoke() = CoroutineCallAdapterFactory() + } + + override fun get( + returnType: Type, + annotations: Array, + 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 or Deferred") + } + 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 or Response") + } + ResponseCallAdapter( + getParameterUpperBound( + 0, + responseType + ) + ) + } else { + BodyCallAdapter(responseType) + } + } + + private class BodyCallAdapter( + private val responseType: Type + ) : CallAdapter> { + + override fun responseType() = responseType + + override fun adapt(call: Call): Deferred { + val deferred = CompletableDeferred() + + deferred.invokeOnCompletion { + if (deferred.isCancelled) { + call.cancel() + } + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + deferred.completeExceptionally(t) + } + + override fun onResponse(call: Call, response: Response) { + if (response.isSuccessful) { + deferred.complete(response.body()!!) + } else { + deferred.completeExceptionally(HttpException(response)) + } + } + }) + + return deferred + } + } + + private class ResponseCallAdapter( + private val responseType: Type + ) : CallAdapter>> { + + override fun responseType() = responseType + + override fun adapt(call: Call): Deferred> { + val deferred = CompletableDeferred>() + + deferred.invokeOnCompletion { + if (deferred.isCancelled) { + call.cancel() + } + } + + call.enqueue(object : Callback { + override fun onFailure(call: Call, t: Throwable) { + deferred.completeExceptionally(t) + } + + override fun onResponse(call: Call, response: Response) { + deferred.complete(response) + } + }) + + return deferred + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt new file mode 100644 index 000000000..1ef3b7d7b --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -0,0 +1,49 @@ +package io.legado.app.help.http + +import okhttp3.* +import java.util.* +import java.util.concurrent.TimeUnit + +object HttpHelper { + + val client: OkHttpClient = getOkHttpClient() + + + private fun getOkHttpClient(): OkHttpClient { + val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .tlsVersions(TlsVersion.TLS_1_2) + .build() + + val specs = ArrayList() + 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) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt new file mode 100644 index 000000000..dcf5b7709 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt @@ -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, authType: String) { + } + + @Throws(CertificateException::class) + override fun checkServerTrusted(chain: Array, authType: String) { + } + + override fun getAcceptedIssuers(): Array { + 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(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? { + 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? { + 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): X509TrustManager? { + for (trustManager in trustManagers) { + if (trustManager is X509TrustManager) { + return trustManager + } + } + return null + } +} diff --git a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt index a9a847a5b..fb7b213f7 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt @@ -1,6 +1,18 @@ package io.legado.app.ui.search import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.SearchBook -class SearchViewModel(application: Application) : AndroidViewModel(application) +class SearchViewModel(application: Application) : BaseViewModel(application){ + + val searchBooks: LiveData> = MutableLiveData() + + public fun search(){ + + + } + +} diff --git a/app/src/main/res/layout/activity_search.xml b/app/src/main/res/layout/activity_search.xml index da4d4a9c6..9e2a9e07c 100644 --- a/app/src/main/res/layout/activity_search.xml +++ b/app/src/main/res/layout/activity_search.xml @@ -2,7 +2,6 @@ - Date: Fri, 24 May 2019 12:54:13 +0800 Subject: [PATCH 10/10] =?UTF-8?q?=E6=B7=BB=E5=8A=A0okhttp,=20retrofit?= =?UTF-8?q?=E9=85=8D=E5=90=88=E5=8D=8F=E7=A8=8B=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/io/legado/app/base/BaseViewModel.kt | 22 ++++++---------- .../io/legado/app/data/api/CommonHttpApi.kt | 14 +++++++++++ ...ory.kt => CoroutinesCallAdapterFactory.kt} | 4 +-- .../io/legado/app/help/http/HttpHelper.kt | 15 +++++++++++ .../legado/app/ui/search/SearchViewModel.kt | 25 +++++++++++++++++-- 5 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt rename app/src/main/java/io/legado/app/help/http/{CoroutineCallAdapterFactory.kt => CoroutinesCallAdapterFactory.kt} (95%) diff --git a/app/src/main/java/io/legado/app/base/BaseViewModel.kt b/app/src/main/java/io/legado/app/base/BaseViewModel.kt index 009aafe7d..74bc4bfc2 100644 --- a/app/src/main/java/io/legado/app/base/BaseViewModel.kt +++ b/app/src/main/java/io/legado/app/base/BaseViewModel.kt @@ -13,13 +13,12 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio private val launchManager: MutableList = mutableListOf() protected fun launchOnUI( - tryBlock: suspend CoroutineScope.() -> Unit, - cacheBlock: suspend CoroutineScope.(Throwable) -> Unit, - finallyBlock: suspend CoroutineScope.() -> Unit, - handleCancellationExceptionManually: Boolean + tryBlock: suspend CoroutineScope.() -> Unit,//成功 + errorBlock: suspend CoroutineScope.(Throwable) -> Unit,//失败 + finallyBlock: suspend CoroutineScope.() -> Unit//结束 ) { launchOnUI { - tryCatch(tryBlock, cacheBlock, finallyBlock, handleCancellationExceptionManually) + tryCatch(tryBlock, errorBlock, finallyBlock) } } @@ -27,25 +26,20 @@ open class BaseViewModel(application: Application) : AndroidViewModel(applicatio * add launch task to [launchManager] */ private fun launchOnUI(block: suspend CoroutineScope.() -> Unit) { - val job = launch { block() } + val job = launch { block() }//主线程 launchManager.add(job) job.invokeOnCompletion { launchManager.remove(job) } } private suspend fun tryCatch( tryBlock: suspend CoroutineScope.() -> Unit, - catchBlock: suspend CoroutineScope.(Throwable) -> Unit, - finallyBlock: suspend CoroutineScope.() -> Unit, - handleCancellationExceptionManually: Boolean = false + errorBlock: suspend CoroutineScope.(Throwable) -> Unit, + finallyBlock: suspend CoroutineScope.() -> Unit ) { try { coroutineScope { tryBlock() } } catch (e: Throwable) { - if (e !is CancellationException || handleCancellationExceptionManually) { - coroutineScope { catchBlock(e) } - } else { - throw e - } + coroutineScope { errorBlock(e) } } finally { coroutineScope { finallyBlock() } } diff --git a/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt b/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt new file mode 100644 index 000000000..08e06f52f --- /dev/null +++ b/app/src/main/java/io/legado/app/data/api/CommonHttpApi.kt @@ -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): Deferred + + @FormUrlEncoded + @POST + fun post(@Url url: String, @FieldMap map: Map): Deferred +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt b/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt similarity index 95% rename from app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt rename to app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt index 78e0aa7a0..bf296f5bb 100644 --- a/app/src/main/java/io/legado/app/help/http/CoroutineCallAdapterFactory.kt +++ b/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt @@ -6,10 +6,10 @@ import retrofit2.* import java.lang.reflect.ParameterizedType import java.lang.reflect.Type -class CoroutineCallAdapterFactory private constructor() : CallAdapter.Factory() { +class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() { companion object { @JvmStatic @JvmName("create") - operator fun invoke() = CoroutineCallAdapterFactory() + operator fun invoke() = CoroutinesCallAdapterFactory() } override fun get( diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index 1ef3b7d7b..59d92b07d 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -1,6 +1,7 @@ package io.legado.app.help.http import okhttp3.* +import retrofit2.Retrofit import java.util.* import java.util.concurrent.TimeUnit @@ -9,6 +10,20 @@ object HttpHelper { val client: OkHttpClient = getOkHttpClient() + fun getApiService(baseUrl: String, clazz: Class): T { + return getRetrofit(baseUrl).create(clazz) + } + + fun getRetrofit(baseUrl: String): Retrofit { + return Retrofit.Builder().baseUrl(baseUrl) + //增加返回值为字符串的支持(以实体类返回) +// .addConverterFactory(EncodeConverter.create()) + //增加返回值为Observable的支持 + .addCallAdapterFactory(CoroutinesCallAdapterFactory.invoke()) + .client(client) + .build() + } + private fun getOkHttpClient(): OkHttpClient { val cs = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) .tlsVersions(TlsVersion.TLS_1_2) diff --git a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt index fb7b213f7..0f9f2072b 100644 --- a/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/search/SearchViewModel.kt @@ -1,18 +1,39 @@ package io.legado.app.ui.search import android.app.Application +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) : BaseViewModel(application){ +class SearchViewModel(application: Application) : BaseViewModel(application) { val searchBooks: LiveData> = MutableLiveData() - public fun search(){ + 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 { +// +// } } }