293 lines
9.6 KiB
Kotlin
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()
|
|
}
|
|
} |