diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt
new file mode 100644
index 000000000..fd737e162
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt
@@ -0,0 +1,387 @@
+package io.legado.app.ui.widget.image
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.*
+import android.graphics.drawable.BitmapDrawable
+import android.graphics.drawable.ColorDrawable
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.Build
+import android.util.AttributeSet
+import android.view.MotionEvent
+import android.view.View
+import android.view.ViewOutlineProvider
+import androidx.annotation.ColorInt
+import androidx.annotation.ColorRes
+import androidx.annotation.DrawableRes
+import androidx.annotation.RequiresApi
+import androidx.appcompat.widget.AppCompatImageView
+import io.legado.app.R
+import kotlin.math.min
+import kotlin.math.pow
+
+class CircleImageView : AppCompatImageView {
+
+ private val mDrawableRect = RectF()
+ private val mBorderRect = RectF()
+
+ private val mShaderMatrix = Matrix()
+ private val mBitmapPaint = Paint()
+ private val mBorderPaint = Paint()
+ private val mCircleBackgroundPaint = Paint()
+
+ private var mBorderColor = DEFAULT_BORDER_COLOR
+ private var mBorderWidth = DEFAULT_BORDER_WIDTH
+ private var mCircleBackgroundColor = DEFAULT_CIRCLE_BACKGROUND_COLOR
+
+ private var mBitmap: Bitmap? = null
+ private var mBitmapShader: BitmapShader? = null
+ private var mBitmapWidth: Int = 0
+ private var mBitmapHeight: Int = 0
+
+ private var mDrawableRadius: Float = 0.toFloat()
+ private var mBorderRadius: Float = 0.toFloat()
+
+ private var mColorFilter: ColorFilter? = null
+
+ private var mReady: Boolean = false
+ private var mSetupPending: Boolean = false
+ private var mBorderOverlay: Boolean = false
+ var isDisableCircularTransformation: Boolean = false
+ set(disableCircularTransformation) {
+ if (isDisableCircularTransformation == disableCircularTransformation) {
+ return
+ }
+
+ field = disableCircularTransformation
+ initializeBitmap()
+ }
+
+ var borderColor: Int
+ get() = mBorderColor
+ set(@ColorInt borderColor) {
+ if (borderColor == mBorderColor) {
+ return
+ }
+
+ mBorderColor = borderColor
+ mBorderPaint.color = mBorderColor
+ invalidate()
+ }
+
+ var circleBackgroundColor: Int
+ get() = mCircleBackgroundColor
+ set(@ColorInt circleBackgroundColor) {
+ if (circleBackgroundColor == mCircleBackgroundColor) {
+ return
+ }
+
+ mCircleBackgroundColor = circleBackgroundColor
+ mCircleBackgroundPaint.color = circleBackgroundColor
+ invalidate()
+ }
+
+ var borderWidth: Int
+ get() = mBorderWidth
+ set(borderWidth) {
+ if (borderWidth == mBorderWidth) {
+ return
+ }
+
+ mBorderWidth = borderWidth
+ setup()
+ }
+
+ var isBorderOverlay: Boolean
+ get() = mBorderOverlay
+ set(borderOverlay) {
+ if (borderOverlay == mBorderOverlay) {
+ return
+ }
+
+ mBorderOverlay = borderOverlay
+ setup()
+ }
+
+ constructor(context: Context) : super(context) {
+
+ init()
+ }
+
+ @JvmOverloads
+ constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(context, attrs, defStyle) {
+
+ val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0)
+
+ mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH)
+ mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR)
+ mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY)
+ mCircleBackgroundColor =
+ a.getColor(R.styleable.CircleImageView_civ_circle_background_color, DEFAULT_CIRCLE_BACKGROUND_COLOR)
+
+ a.recycle()
+
+ init()
+ }
+
+ private fun init() {
+ super.setScaleType(SCALE_TYPE)
+ mReady = true
+
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ outlineProvider = OutlineProvider()
+ }
+
+ if (mSetupPending) {
+ setup()
+ mSetupPending = false
+ }
+ }
+
+ override fun getScaleType(): ScaleType {
+ return SCALE_TYPE
+ }
+
+ override fun setScaleType(scaleType: ScaleType) {
+ if (scaleType != SCALE_TYPE) {
+ throw IllegalArgumentException(String.format("ScaleType %s not supported.", scaleType))
+ }
+ }
+
+ override fun setAdjustViewBounds(adjustViewBounds: Boolean) {
+ if (adjustViewBounds) {
+ throw IllegalArgumentException("adjustViewBounds not supported.")
+ }
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ if (isDisableCircularTransformation) {
+ super.onDraw(canvas)
+ return
+ }
+
+ if (mBitmap == null) {
+ return
+ }
+
+ if (mCircleBackgroundColor != Color.TRANSPARENT) {
+ canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint)
+ }
+ canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint)
+ if (mBorderWidth > 0) {
+ canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint)
+ }
+ }
+
+ override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
+ super.onSizeChanged(w, h, oldw, oldh)
+ setup()
+ }
+
+ override fun setPadding(left: Int, top: Int, right: Int, bottom: Int) {
+ super.setPadding(left, top, right, bottom)
+ setup()
+ }
+
+ override fun setPaddingRelative(start: Int, top: Int, end: Int, bottom: Int) {
+ super.setPaddingRelative(start, top, end, bottom)
+ setup()
+ }
+
+ fun setCircleBackgroundColorResource(@ColorRes circleBackgroundRes: Int) {
+ circleBackgroundColor = context.resources.getColor(circleBackgroundRes)
+ }
+
+ override fun setImageBitmap(bm: Bitmap) {
+ super.setImageBitmap(bm)
+ initializeBitmap()
+ }
+
+ override fun setImageDrawable(drawable: Drawable?) {
+ super.setImageDrawable(drawable)
+ initializeBitmap()
+ }
+
+ override fun setImageResource(@DrawableRes resId: Int) {
+ super.setImageResource(resId)
+ initializeBitmap()
+ }
+
+ override fun setImageURI(uri: Uri?) {
+ super.setImageURI(uri)
+ initializeBitmap()
+ }
+
+ override fun setColorFilter(cf: ColorFilter) {
+ if (cf === mColorFilter) {
+ return
+ }
+
+ mColorFilter = cf
+ applyColorFilter()
+ invalidate()
+ }
+
+ override fun getColorFilter(): ColorFilter? {
+ return mColorFilter
+ }
+
+ private fun applyColorFilter() {
+ mBitmapPaint.colorFilter = mColorFilter
+ }
+
+ private fun getBitmapFromDrawable(drawable: Drawable?): Bitmap? {
+ if (drawable == null) {
+ return null
+ }
+
+ if (drawable is BitmapDrawable) {
+ return drawable.bitmap
+ }
+
+ try {
+ val bitmap: Bitmap
+
+ if (drawable is ColorDrawable) {
+ bitmap = Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG)
+ } else {
+ bitmap = Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG)
+ }
+
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+ return bitmap
+ } catch (e: Exception) {
+ e.printStackTrace()
+ return null
+ }
+
+ }
+
+ private fun initializeBitmap() {
+ if (isDisableCircularTransformation) {
+ mBitmap = null
+ } else {
+ mBitmap = getBitmapFromDrawable(drawable)
+ }
+ setup()
+ }
+
+ private fun setup() {
+ if (!mReady) {
+ mSetupPending = true
+ return
+ }
+
+ if (width == 0 && height == 0) {
+ return
+ }
+
+ if (mBitmap == null) {
+ invalidate()
+ return
+ }
+
+ mBitmapShader = BitmapShader(mBitmap!!, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)
+
+ mBitmapPaint.isAntiAlias = true
+ mBitmapPaint.shader = mBitmapShader
+
+ mBorderPaint.style = Paint.Style.STROKE
+ mBorderPaint.isAntiAlias = true
+ mBorderPaint.color = mBorderColor
+ mBorderPaint.strokeWidth = mBorderWidth.toFloat()
+
+ mCircleBackgroundPaint.style = Paint.Style.FILL
+ mCircleBackgroundPaint.isAntiAlias = true
+ mCircleBackgroundPaint.color = mCircleBackgroundColor
+
+ mBitmapHeight = mBitmap!!.height
+ mBitmapWidth = mBitmap!!.width
+
+ mBorderRect.set(calculateBounds())
+ mBorderRadius =
+ min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f)
+
+ mDrawableRect.set(mBorderRect)
+ if (!mBorderOverlay && mBorderWidth > 0) {
+ mDrawableRect.inset(mBorderWidth - 1.0f, mBorderWidth - 1.0f)
+ }
+ mDrawableRadius = min(mDrawableRect.height() / 2.0f, mDrawableRect.width() / 2.0f)
+
+ applyColorFilter()
+ updateShaderMatrix()
+ invalidate()
+ }
+
+ private fun calculateBounds(): RectF {
+ val availableWidth = width - paddingLeft - paddingRight
+ val availableHeight = height - paddingTop - paddingBottom
+
+ val sideLength = min(availableWidth, availableHeight)
+
+ val left = paddingLeft + (availableWidth - sideLength) / 2f
+ val top = paddingTop + (availableHeight - sideLength) / 2f
+
+ return RectF(left, top, left + sideLength, top + sideLength)
+ }
+
+ private fun updateShaderMatrix() {
+ val scale: Float
+ var dx = 0f
+ var dy = 0f
+
+ mShaderMatrix.set(null)
+
+ if (mBitmapWidth * mDrawableRect.height() > mDrawableRect.width() * mBitmapHeight) {
+ scale = mDrawableRect.height() / mBitmapHeight.toFloat()
+ dx = (mDrawableRect.width() - mBitmapWidth * scale) * 0.5f
+ } else {
+ scale = mDrawableRect.width() / mBitmapWidth.toFloat()
+ dy = (mDrawableRect.height() - mBitmapHeight * scale) * 0.5f
+ }
+
+ mShaderMatrix.setScale(scale, scale)
+ mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top)
+
+ mBitmapShader!!.setLocalMatrix(mShaderMatrix)
+ }
+
+ @SuppressLint("ClickableViewAccessibility")
+ override fun onTouchEvent(event: MotionEvent): Boolean {
+ return inTouchableArea(event.x, event.y) && super.onTouchEvent(event)
+ }
+
+ private fun inTouchableArea(x: Float, y: Float): Boolean {
+ return (x - mBorderRect.centerX()).toDouble()
+ .pow(2.0) + (y - mBorderRect.centerY()).toDouble()
+ .pow(2.0) <= mBorderRadius.toDouble().pow(2.0)
+ }
+
+ @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
+ private inner class OutlineProvider : ViewOutlineProvider() {
+
+ override fun getOutline(view: View, outline: Outline) {
+ val bounds = Rect()
+ mBorderRect.roundOut(bounds)
+ outline.setRoundRect(bounds, bounds.width() / 2.0f)
+ }
+
+ }
+
+ companion object {
+
+ private val SCALE_TYPE = ScaleType.CENTER_CROP
+
+ private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888
+ private const val COLORDRAWABLE_DIMENSION = 2
+
+ private const val DEFAULT_BORDER_WIDTH = 0
+ private const val DEFAULT_BORDER_COLOR = Color.BLACK
+ private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT
+ private const val DEFAULT_BORDER_OVERLAY = false
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/pop_read_book_style.xml b/app/src/main/res/layout/pop_read_book_style.xml
new file mode 100644
index 000000000..d829e291c
--- /dev/null
+++ b/app/src/main/res/layout/pop_read_book_style.xml
@@ -0,0 +1,7 @@
+
+
+
+
\ 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 67c52d7c9..dbac9f574 100644
--- a/app/src/main/res/values/attrs.xml
+++ b/app/src/main/res/values/attrs.xml
@@ -114,4 +114,11 @@
+
+
+
+
+
+
+
\ No newline at end of file