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