pull/32/head
parent
e056b071c5
commit
14e93a181a
@ -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 |
||||
} |
||||
|
||||
} |
@ -0,0 +1,7 @@ |
||||
<?xml version="1.0" encoding="utf-8"?> |
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
android:orientation="vertical" |
||||
android:layout_width="match_parent" |
||||
android:layout_height="match_parent"> |
||||
|
||||
</LinearLayout> |
Loading…
Reference in new issue