diff --git a/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt b/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt
new file mode 100644
index 000000000..5f09a8744
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt
@@ -0,0 +1,233 @@
+package io.legado.app.ui.widget
+
+import android.content.Context
+import android.graphics.Color
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.shapes.RoundRectShape
+import android.text.TextUtils
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup
+import android.widget.FrameLayout
+import android.widget.FrameLayout.LayoutParams
+import android.widget.TabWidget
+import androidx.appcompat.widget.AppCompatTextView
+import io.legado.app.R
+
+
+/**
+ * Created by milad heydari on 5/6/2016.
+ */
+class BadgeView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyle: Int = android.R.attr.textViewStyle
+) : AppCompatTextView(context, attrs, defStyle) {
+
+ /**
+ * @return Returns true if view is hidden on badge value 0 or null;
+ */
+ /**
+ * @param hideOnNull the hideOnNull to set
+ */
+ var isHideOnNull = true
+ set(hideOnNull) {
+ field = hideOnNull
+ text = text
+ }
+ private var radius: Float = 0.toFloat()
+
+ val badgeCount: Int?
+ get() {
+ if (text == null) {
+ return null
+ }
+ val text = text.toString()
+ try {
+ return Integer.parseInt(text)
+ } catch (e: NumberFormatException) {
+ return null
+ }
+
+ }
+
+ var badgeGravity: Int
+ get() {
+ val params = layoutParams as LayoutParams
+ return params.gravity
+ }
+ set(gravity) {
+ val params = layoutParams as LayoutParams
+ params.gravity = gravity
+ layoutParams = params
+ }
+
+ val badgeMargin: IntArray
+ get() {
+ val params = layoutParams as LayoutParams
+ return intArrayOf(params.leftMargin, params.topMargin, params.rightMargin, params.bottomMargin)
+ }
+
+ init {
+
+ init()
+ }
+
+ private fun init() {
+ if (layoutParams !is LayoutParams) {
+ val layoutParams = LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ Gravity.CENTER
+ )
+ setLayoutParams(layoutParams)
+ }
+
+ // set default font
+ setTextColor(Color.WHITE)
+ //setTypeface(Typeface.DEFAULT_BOLD);
+ setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
+ setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
+ radius = 8f
+
+ // set default background
+ setBackground(radius, Color.parseColor("#d3321b"))
+
+ gravity = Gravity.CENTER
+
+ // default values
+ isHideOnNull = true
+ setBadgeCount(0)
+ minWidth = dip2Px(16f)
+ minHeight = dip2Px(16f)
+ }
+
+ fun setBackground(dipRadius: Float, badgeColor: Int) {
+ val radius = dip2Px(dipRadius)
+ val radiusArray = floatArrayOf(
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ radius.toFloat()
+ )
+
+ val roundRect = RoundRectShape(radiusArray, null, null)
+ val bgDrawable = ShapeDrawable(roundRect)
+ bgDrawable.paint.color = badgeColor
+ background = bgDrawable
+ }
+
+ fun setBackground(badgeColor: Int) {
+ setBackground(radius, badgeColor)
+ }
+
+ /**
+ * @see android.widget.TextView.setText
+ */
+ override fun setText(text: CharSequence, type: BufferType) {
+ if (isHideOnNull && TextUtils.isEmpty(text)) {
+ visibility = View.GONE
+ } else {
+ visibility = View.VISIBLE
+ }
+ super.setText(text, type)
+ }
+
+ fun setBadgeCount(count: Int) {
+ text = count.toString()
+ if (count == 0) {
+ visibility = View.GONE
+ }
+ }
+
+ fun setHighlight(highlight: Boolean) {
+ setBackground(resources.getColor(if (highlight) R.color.highlight else R.color.darker_gray))
+ }
+
+ fun setBadgeMargin(dipMargin: Int) {
+ setBadgeMargin(dipMargin, dipMargin, dipMargin, dipMargin)
+ }
+
+ fun setBadgeMargin(leftDipMargin: Int, topDipMargin: Int, rightDipMargin: Int, bottomDipMargin: Int) {
+ val params = layoutParams as LayoutParams
+ params.leftMargin = dip2Px(leftDipMargin.toFloat())
+ params.topMargin = dip2Px(topDipMargin.toFloat())
+ params.rightMargin = dip2Px(rightDipMargin.toFloat())
+ params.bottomMargin = dip2Px(bottomDipMargin.toFloat())
+ layoutParams = params
+ }
+
+ fun incrementBadgeCount(increment: Int) {
+ val count = badgeCount
+ if (count == null) {
+ setBadgeCount(increment)
+ } else {
+ setBadgeCount(increment + count)
+ }
+ }
+
+ fun decrementBadgeCount(decrement: Int) {
+ incrementBadgeCount(-decrement)
+ }
+
+ /**
+ * Attach the BadgeView to the TabWidget
+ * @param target the TabWidget to attach the BadgeView
+ * @param tabIndex index of the tab
+ */
+ fun setTargetView(target: TabWidget, tabIndex: Int) {
+ val tabView = target.getChildTabViewAt(tabIndex)
+ setTargetView(tabView)
+ }
+
+ /**
+ * Attach the BadgeView to the target view
+ * @param target the view to attach the BadgeView
+ */
+ fun setTargetView(target: View?) {
+ if (parent != null) {
+ (parent as ViewGroup).removeView(this)
+ }
+
+ if (target == null) {
+ return
+ }
+
+ if (target.parent is FrameLayout) {
+ (target.parent as FrameLayout).addView(this)
+
+ } else if (target.parent is ViewGroup) {
+ // use a new FrameLayout container for adding badge
+ val parentContainer = target.parent as ViewGroup
+ val groupIndex = parentContainer.indexOfChild(target)
+ parentContainer.removeView(target)
+
+ val badgeContainer = FrameLayout(context)
+ val parentLayoutParams = target.layoutParams
+
+ badgeContainer.layoutParams = parentLayoutParams
+ target.layoutParams = ViewGroup.LayoutParams(
+ ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT
+ )
+
+ parentContainer.addView(badgeContainer, groupIndex, parentLayoutParams)
+ badgeContainer.addView(target)
+
+ badgeContainer.addView(this)
+ }
+
+ }
+
+ /**
+ * converts dip to px
+ */
+ private fun dip2Px(dip: Float): Int {
+ return (dip * context.resources.displayMetrics.density + 0.5f).toInt()
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt b/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt
new file mode 100644
index 000000000..c17bc7b5d
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/RotateLoading.kt
@@ -0,0 +1,207 @@
+package io.legado.app.ui.widget
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.RectF
+import android.util.AttributeSet
+import android.util.TypedValue
+import android.view.View
+import android.view.animation.LinearInterpolator
+import io.legado.app.R
+
+/**
+ * RotateLoading
+ * Created by Victor on 2015/4/28.
+ */
+class RotateLoading : View {
+
+ private var mPaint: Paint? = null
+
+ private var loadingRectF: RectF? = null
+ private var shadowRectF: RectF? = null
+
+ private var topDegree = 10
+ private var bottomDegree = 190
+
+ private var arc: Float = 0.toFloat()
+
+ private var thisWidth: Int = 0
+
+ private var changeBigger = true
+
+ private var shadowPosition: Int = 0
+
+ var isStart = false
+ private set
+
+ var loadingColor: Int = 0
+
+ private var speedOfDegree: Int = 0
+
+ private var speedOfArc: Float = 0.toFloat()
+
+ constructor(context: Context) : super(context) {
+ initView(context, null)
+ }
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ initView(context, attrs)
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ initView(context, attrs)
+ }
+
+ private fun initView(context: Context, attrs: AttributeSet?) {
+ loadingColor = Color.WHITE
+ thisWidth = dpToPx(context, DEFAULT_WIDTH.toFloat())
+ shadowPosition = dpToPx(getContext(), DEFAULT_SHADOW_POSITION.toFloat())
+ speedOfDegree = DEFAULT_SPEED_OF_DEGREE
+
+ if (null != attrs) {
+ val typedArray = context.obtainStyledAttributes(attrs, R.styleable.RotateLoading)
+ loadingColor = typedArray.getColor(R.styleable.RotateLoading_loading_color, Color.WHITE)
+ thisWidth = typedArray.getDimensionPixelSize(
+ R.styleable.RotateLoading_loading_width,
+ dpToPx(context, DEFAULT_WIDTH.toFloat())
+ )
+ shadowPosition = typedArray.getInt(R.styleable.RotateLoading_shadow_position, DEFAULT_SHADOW_POSITION)
+ speedOfDegree = typedArray.getInt(R.styleable.RotateLoading_loading_speed, DEFAULT_SPEED_OF_DEGREE)
+ typedArray.recycle()
+ }
+ speedOfArc = (speedOfDegree / 4).toFloat()
+ mPaint = Paint()
+ mPaint!!.color = loadingColor
+ mPaint!!.isAntiAlias = true
+ mPaint!!.style = Paint.Style.STROKE
+ mPaint!!.strokeWidth = thisWidth.toFloat()
+ mPaint!!.strokeCap = Paint.Cap.ROUND
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+
+ arc = 10f
+
+ loadingRectF =
+ RectF((2 * thisWidth).toFloat(), (2 * thisWidth).toFloat(), (w - 2 * thisWidth).toFloat(), (h - 2 * thisWidth).toFloat())
+ shadowRectF = RectF(
+ (2 * thisWidth + shadowPosition).toFloat(),
+ (2 * thisWidth + shadowPosition).toFloat(),
+ (w - 2 * thisWidth + shadowPosition).toFloat(),
+ (h - 2 * thisWidth + shadowPosition).toFloat()
+ )
+ }
+
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+
+ if (!isStart) {
+ return
+ }
+
+ mPaint!!.color = Color.parseColor("#1a000000")
+ canvas.drawArc(shadowRectF!!, topDegree.toFloat(), arc, false, mPaint!!)
+ canvas.drawArc(shadowRectF!!, bottomDegree.toFloat(), arc, false, mPaint!!)
+
+ mPaint!!.color = loadingColor
+ canvas.drawArc(loadingRectF!!, topDegree.toFloat(), arc, false, mPaint!!)
+ canvas.drawArc(loadingRectF!!, bottomDegree.toFloat(), arc, false, mPaint!!)
+
+ topDegree += speedOfDegree
+ bottomDegree += speedOfDegree
+ if (topDegree > 360) {
+ topDegree = topDegree - 360
+ }
+ if (bottomDegree > 360) {
+ bottomDegree = bottomDegree - 360
+ }
+
+ if (changeBigger) {
+ if (arc < 160) {
+ arc += speedOfArc
+ invalidate()
+ }
+ } else {
+ if (arc > speedOfDegree) {
+ arc -= 2 * speedOfArc
+ invalidate()
+ }
+ }
+ if (arc >= 160 || arc <= 10) {
+ changeBigger = !changeBigger
+ invalidate()
+ }
+ }
+
+ fun start() {
+ startAnimator()
+ isStart = true
+ invalidate()
+ }
+
+ fun stop() {
+ stopAnimator()
+ invalidate()
+ }
+
+ private fun startAnimator() {
+ val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 0.0f, 1f)
+ val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 0.0f, 1f)
+ scaleXAnimator.setDuration(300)
+ scaleXAnimator.setInterpolator(LinearInterpolator())
+ scaleYAnimator.setDuration(300)
+ scaleYAnimator.setInterpolator(LinearInterpolator())
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(scaleXAnimator, scaleYAnimator)
+ animatorSet.start()
+ }
+
+ private fun stopAnimator() {
+ val scaleXAnimator = ObjectAnimator.ofFloat(this, "scaleX", 1f, 0f)
+ val scaleYAnimator = ObjectAnimator.ofFloat(this, "scaleY", 1f, 0f)
+ scaleXAnimator.setDuration(300)
+ scaleXAnimator.setInterpolator(LinearInterpolator())
+ scaleYAnimator.setDuration(300)
+ scaleYAnimator.setInterpolator(LinearInterpolator())
+ val animatorSet = AnimatorSet()
+ animatorSet.playTogether(scaleXAnimator, scaleYAnimator)
+ animatorSet.addListener(object : Animator.AnimatorListener {
+ override fun onAnimationStart(animation: Animator) {
+
+ }
+
+ override fun onAnimationEnd(animation: Animator) {
+ isStart = false
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+
+ }
+
+ override fun onAnimationRepeat(animation: Animator) {
+
+ }
+ })
+ animatorSet.start()
+ }
+
+
+ fun dpToPx(context: Context, dpVal: Float): Int {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dpVal, context.resources.displayMetrics).toInt()
+ }
+
+ companion object {
+
+ private val DEFAULT_WIDTH = 6
+ private val DEFAULT_SHADOW_POSITION = 2
+ private val DEFAULT_SPEED_OF_DEGREE = 10
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt
new file mode 100644
index 000000000..289790a1a
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt
@@ -0,0 +1,56 @@
+package io.legado.app.ui.widget.image
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Path
+import android.util.AttributeSet
+
+
+class CoverImageView : androidx.appcompat.widget.AppCompatImageView {
+ internal var width: Float = 0.toFloat()
+ internal var height: Float = 0.toFloat()
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs)
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr)
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ width = getWidth().toFloat()
+ height = getHeight().toFloat()
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ val measuredWidth = MeasureSpec.getSize(widthMeasureSpec)
+ val measuredHeight = measuredWidth * 7 / 5
+ super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY))
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (width >= 10 && height > 10) {
+ @SuppressLint("DrawAllocation")
+ val path = Path()
+ //四个圆角
+ path.moveTo(10f, 0f)
+ path.lineTo(width - 10, 0f)
+ path.quadTo(width, 0f, width, 10f)
+ path.lineTo(width, height - 10)
+ path.quadTo(width, height, width - 10, height)
+ path.lineTo(10f, height)
+ path.quadTo(0f, height, 0f, height - 10)
+ path.lineTo(0f, 10f)
+ path.quadTo(0f, 0f, 10f, 0f)
+
+ canvas.clipPath(path)
+ }
+ super.onDraw(canvas)
+ }
+
+ fun setHeight(height: Int) {
+ val width = height * 5 / 7
+ minimumWidth = width
+ }
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt
new file mode 100644
index 000000000..bb6a4461e
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/image/FilletImageView.kt
@@ -0,0 +1,92 @@
+package io.legado.app.ui.widget.image
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Path
+import android.util.AttributeSet
+import androidx.appcompat.widget.AppCompatImageView
+import io.legado.app.R
+
+class FilletImageView : AppCompatImageView {
+ internal var width: Float = 0.toFloat()
+ internal var height: Float = 0.toFloat()
+ private var leftTopRadius: Int = 0
+ private var rightTopRadius: Int = 0
+ private var rightBottomRadius: Int = 0
+ private var leftBottomRadius: Int = 0
+
+ constructor(context: Context) : super(context)
+
+ constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
+ init(context, attrs)
+ }
+
+ constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
+ init(context, attrs)
+ }
+
+ private fun init(context: Context, attrs: AttributeSet) {
+ // 读取配置
+ val array = context.obtainStyledAttributes(attrs, R.styleable.FilletImageView)
+ val defaultRadius = 5
+ val radius = array.getDimensionPixelOffset(R.styleable.FilletImageView_radius, defaultRadius)
+ leftTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_top_radius, defaultRadius)
+ rightTopRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_right_top_radius, defaultRadius)
+ rightBottomRadius =
+ array.getDimensionPixelOffset(R.styleable.FilletImageView_right_bottom_radius, defaultRadius)
+ leftBottomRadius = array.getDimensionPixelOffset(R.styleable.FilletImageView_left_bottom_radius, defaultRadius)
+
+ //如果四个角的值没有设置,那么就使用通用的radius的值。
+ if (defaultRadius == leftTopRadius) {
+ leftTopRadius = radius
+ }
+ if (defaultRadius == rightTopRadius) {
+ rightTopRadius = radius
+ }
+ if (defaultRadius == rightBottomRadius) {
+ rightBottomRadius = radius
+ }
+ if (defaultRadius == leftBottomRadius) {
+ leftBottomRadius = radius
+ }
+ array.recycle()
+
+ }
+
+ override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) {
+ super.onLayout(changed, left, top, right, bottom)
+ width = getWidth().toFloat()
+ height = getHeight().toFloat()
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ //这里做下判断,只有图片的宽高大于设置的圆角距离的时候才进行裁剪
+ val maxLeft = Math.max(leftTopRadius, leftBottomRadius)
+ val maxRight = Math.max(rightTopRadius, rightBottomRadius)
+ val minWidth = maxLeft + maxRight
+ val maxTop = Math.max(leftTopRadius, rightTopRadius)
+ val maxBottom = Math.max(leftBottomRadius, rightBottomRadius)
+ val minHeight = maxTop + maxBottom
+ if (width >= minWidth && height > minHeight) {
+ @SuppressLint("DrawAllocation") val path = Path()
+ //四个角:右上,右下,左下,左上
+ path.moveTo(leftTopRadius.toFloat(), 0f)
+ path.lineTo(width - rightTopRadius, 0f)
+ path.quadTo(width, 0f, width, rightTopRadius.toFloat())
+
+ path.lineTo(width, height - rightBottomRadius)
+ path.quadTo(width, height, width - rightBottomRadius, height)
+
+ path.lineTo(leftBottomRadius.toFloat(), height)
+ path.quadTo(0f, height, 0f, height - leftBottomRadius)
+
+ path.lineTo(0f, leftTopRadius.toFloat())
+ path.quadTo(0f, 0f, leftTopRadius.toFloat(), 0f)
+
+ canvas.clipPath(path)
+ }
+ super.onDraw(canvas)
+ }
+
+}
diff --git a/app/src/main/res/layout/item_bookshelf_list.xml b/app/src/main/res/layout/item_bookshelf_list.xml
new file mode 100644
index 000000000..03845fa18
--- /dev/null
+++ b/app/src/main/res/layout/item_bookshelf_list.xml
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml
index 75b4d171f..42b4963d2 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -101,4 +101,10 @@
+
+
+
+
+
+
\ No newline at end of file