SubwayTooter-Android-App/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt

293 lines
9.6 KiB
Kotlin

/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.graphics.BitmapShader
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.Rect
import android.graphics.RectF
import android.graphics.Shader
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
enum class ColorShape(val attrEnum: Int) {
Square(0),
Circle(1),
;
companion object {
fun fromInt(i: Int): ColorShape =
entries.find { it.attrEnum == i } ?: Square
}
}
/**
* This class draws a panel which which will be filled with a color which can be set. It can be used to show the
* currently selected color which you will get from the [ColorPickerView].
*/
class ColorPanelView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0,
) : View(context, attrs, defStyle) {
companion object {
private const val DEFAULT_BORDER_COLOR = -0x919192
}
/* The width in pixels of the border surrounding the color panel. */
private val borderWidthPx = context.dpToPx(1f)
private val borderPaint = Paint().apply {
isAntiAlias = true
}
private val colorPaint = Paint().apply {
isAntiAlias = true
}
private val alphaPaint = Paint().apply {
val bitmap = (ContextCompat.getDrawable(context, R.drawable.cpv_alpha) as BitmapDrawable)
.bitmap
shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
isAntiAlias = true
}
private val originalPaint = Paint()
private val centerRect = RectF()
private var drawingRect = Rect()
private var colorRect = Rect()
private var alphaPattern = TilePatternDrawable(context.dpToPx(4f))
private var showOldColor = false
var borderColor = DEFAULT_BORDER_COLOR
set(value) {
if (field != value) {
field = value
invalidate()
}
}
var color = Color.BLACK
set(value) {
if (field != value) {
field = value
invalidate()
}
}
private var shape = ColorShape.Circle
set(value) {
if (field != value) {
field = value
invalidate()
}
}
init {
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
shape = ColorShape.fromInt(a.getInt(R.styleable.ColorPanelView_cpv_colorShape, -1))
showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false)
check(!(showOldColor && shape != ColorShape.Circle)) { "Color preview is only available in circle mode" }
borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR)
a.recycle()
if (borderColor == DEFAULT_BORDER_COLOR) {
// If no specific border color has been set we take the default secondary text color as border/slider color.
// Thus it will adopt to theme changes automatically.
val value = TypedValue()
val typedArray = context.obtainStyledAttributes(
value.data,
intArrayOf(android.R.attr.textColorSecondary)
)
borderColor = typedArray.getColor(0, borderColor)
typedArray.recycle()
}
}
public override fun onSaveInstanceState(): Parcelable {
val state = Bundle()
state.putParcelable("instanceState", super.onSaveInstanceState())
state.putInt("color", color)
return state
}
public override fun onRestoreInstanceState(state: Parcelable) {
if (state is Bundle) {
color = state.getInt("color")
super.onRestoreInstanceState(state.getParcelableCompat("instanceState"))
} else {
super.onRestoreInstanceState(state)
}
}
override fun onDraw(canvas: Canvas) {
borderPaint.color = borderColor
colorPaint.color = color
if (shape == ColorShape.Square) {
if (borderWidthPx > 0) {
canvas.drawRect(drawingRect, borderPaint)
}
alphaPattern.draw(canvas)
canvas.drawRect(colorRect, colorPaint)
} else if (shape == ColorShape.Circle) {
val outerRadius = measuredWidth / 2
if (borderWidthPx > 0) {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f,
outerRadius.toFloat(),
borderPaint
)
}
if (Color.alpha(color) < 255) {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f, (
outerRadius - borderWidthPx).toFloat(), alphaPaint
)
}
if (showOldColor) {
canvas.drawArc(centerRect, 90f, 180f, true, originalPaint)
canvas.drawArc(centerRect, 270f, 180f, true, colorPaint)
} else {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f, (
outerRadius - borderWidthPx).toFloat(),
colorPaint
)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when (shape) {
ColorShape.Square -> {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
ColorShape.Circle -> {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
setMeasuredDimension(measuredWidth, measuredWidth)
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (shape == ColorShape.Square || showOldColor) {
drawingRect.set(
paddingLeft,
paddingTop,
w - paddingRight,
h - paddingBottom
)
if (showOldColor) {
setUpCenterRect()
} else {
setUpColorRect()
}
}
}
private fun setUpCenterRect() {
val dRect = drawingRect
val left = dRect.left + borderWidthPx
val top = dRect.top + borderWidthPx
val bottom = dRect.bottom - borderWidthPx
val right = dRect.right - borderWidthPx
centerRect.set(
left.toFloat(),
top.toFloat(),
right.toFloat(),
bottom.toFloat()
)
}
private fun setUpColorRect() {
val left = drawingRect.left + borderWidthPx
val top = drawingRect.top + borderWidthPx
val bottom = drawingRect.bottom - borderWidthPx
val right = drawingRect.right - borderWidthPx
colorRect.set(left, top, right, bottom)
alphaPattern.setBounds(left, top, right, bottom)
}
/**
* Set the original color. This is only used for previewing colors.
*
* @param color
* The original color
*/
fun setOriginalColor(@ColorInt color: Int) {
originalPaint.color = color
}
/**
* Show a toast message with the hex color code below the view.
*/
fun showHint() {
val screenPos = IntArray(2)
val displayFrame = Rect()
getLocationOnScreen(screenPos)
getWindowVisibleDisplayFrame(displayFrame)
val context = context
val width = width
val height = height
val midy = screenPos[1] + height / 2
var referenceX = screenPos[0] + width / 2
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
val screenWidth = context.resources.displayMetrics.widthPixels
referenceX = screenWidth - referenceX // mirror
}
val hexText = when {
Color.alpha(color) == 255 -> "%06X".format(color and 0xFFFFFF)
else -> Integer.toHexString(color)
}
val hint = "#${hexText.uppercase()}"
val cheatSheet = Toast.makeText(context, hint, Toast.LENGTH_SHORT)
if (midy < displayFrame.height()) {
// Show along the top; follow action buttons
cheatSheet.setGravity(
Gravity.TOP or GravityCompat.END, referenceX,
screenPos[1] + height - displayFrame.top
)
} else {
// Show along the bottom center
cheatSheet.setGravity(Gravity.BOTTOM or Gravity.CENTER_HORIZONTAL, 0, height)
}
cheatSheet.show()
}
}