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