851 lines
29 KiB
Kotlin
851 lines
29 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.annotation.SuppressLint
|
|
import android.content.Context
|
|
import android.graphics.Bitmap
|
|
import android.graphics.Canvas
|
|
import android.graphics.Color
|
|
import android.graphics.ComposeShader
|
|
import android.graphics.LinearGradient
|
|
import android.graphics.Paint
|
|
import android.graphics.Paint.Align
|
|
import android.graphics.Point
|
|
import android.graphics.PorterDuff
|
|
import android.graphics.Rect
|
|
import android.graphics.RectF
|
|
import android.graphics.Shader
|
|
import android.graphics.Shader.TileMode
|
|
import android.os.Bundle
|
|
import android.os.Parcelable
|
|
import android.util.AttributeSet
|
|
import android.util.TypedValue
|
|
import android.view.MotionEvent
|
|
import android.view.View
|
|
import androidx.annotation.ColorInt
|
|
import kotlin.math.max
|
|
import kotlin.math.min
|
|
|
|
/**
|
|
* Displays a color picker to the user and allow them to select a color. A slider for the alpha channel is also available.
|
|
* Enable it by setting setAlphaSliderVisible(boolean) to true.
|
|
*/
|
|
class ColorPickerView @JvmOverloads constructor(
|
|
context: Context,
|
|
attrs: AttributeSet? = null,
|
|
defStyle: Int = 0,
|
|
) : View(context, attrs, defStyle) {
|
|
|
|
companion object {
|
|
private const val DEFAULT_BORDER_COLOR = -0x919192
|
|
private const val DEFAULT_SLIDER_COLOR = -0x424243
|
|
private const val HUE_PANEL_WDITH_DP = 30
|
|
private const val ALPHA_PANEL_HEIGH_DP = 20
|
|
private const val PANEL_SPACING_DP = 10
|
|
private const val CIRCLE_TRACKER_RADIUS_DP = 5
|
|
private const val SLIDER_TRACKER_SIZE_DP = 4
|
|
private const val SLIDER_TRACKER_OFFSET_DP = 2
|
|
|
|
/**
|
|
* The width in pixels of the border
|
|
* surrounding all color panels.
|
|
*/
|
|
private const val BORDER_WIDTH_PX = 1
|
|
}
|
|
|
|
fun interface OnColorChangedListener {
|
|
fun onColorChanged(newColor: Int)
|
|
}
|
|
|
|
private class BitmapCache(
|
|
var canvas: Canvas? = null,
|
|
var bitmap: Bitmap? = null,
|
|
var value: Float = 0f,
|
|
)
|
|
|
|
/**
|
|
* The width in px of the hue panel.
|
|
*/
|
|
private val huePanelWidthPx = context.dpToPx(HUE_PANEL_WDITH_DP)
|
|
|
|
/**
|
|
* The height in px of the alpha panel
|
|
*/
|
|
private val alphaPanelHeightPx = context.dpToPx(ALPHA_PANEL_HEIGH_DP)
|
|
|
|
/**
|
|
* The distance in px between the different
|
|
* color panels.
|
|
*/
|
|
private val panelSpacingPx = context.dpToPx(PANEL_SPACING_DP)
|
|
|
|
/**
|
|
* The radius in px of the color palette tracker circle.
|
|
*/
|
|
private val circleTrackerRadiusPx = context.dpToPx(CIRCLE_TRACKER_RADIUS_DP)
|
|
|
|
/**
|
|
* The px which the tracker of the hue or alpha panel
|
|
* will extend outside of its bounds.
|
|
*/
|
|
private val sliderTrackerOffsetPx = context.dpToPx(SLIDER_TRACKER_OFFSET_DP)
|
|
|
|
/**
|
|
* Height of slider tracker on hue panel,
|
|
* width of slider on alpha panel.
|
|
*/
|
|
private val sliderTrackerSizePx = context.dpToPx(SLIDER_TRACKER_SIZE_DP)
|
|
|
|
/**
|
|
* the current value of the text that will be shown in the alpha slider.
|
|
* null to disable text.
|
|
*/
|
|
@Suppress("MemberVisibilityCanBePrivate")
|
|
var alphaSliderText: String? = null
|
|
set(value) {
|
|
if (field != value) {
|
|
field = value
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
// the color the view should show.
|
|
var color: Int
|
|
get() = Color.HSVToColor(alpha, floatArrayOf(hue, sat, bri))
|
|
set(color) {
|
|
setColor(color, false)
|
|
}
|
|
|
|
@ColorInt
|
|
var sliderTrackerColor = DEFAULT_SLIDER_COLOR
|
|
set(value) {
|
|
field = value
|
|
hueAlphaTrackerPaint.color = value
|
|
invalidate()
|
|
}
|
|
|
|
@ColorInt
|
|
var borderColor = DEFAULT_BORDER_COLOR
|
|
set(value) {
|
|
if (field != value) {
|
|
field = value
|
|
invalidate()
|
|
}
|
|
}
|
|
|
|
private val satValPaint = Paint()
|
|
|
|
private val satValTrackerPaint = Paint().apply {
|
|
style = Paint.Style.STROKE
|
|
strokeWidth = context.dpToPx(2f).toFloat()
|
|
isAntiAlias = true
|
|
}
|
|
|
|
private val alphaPaint = Paint()
|
|
|
|
private val alphaTextPaint = Paint().apply {
|
|
color = -0xe3e3e4
|
|
textSize = context.dpToPx(14f).toFloat()
|
|
isAntiAlias = true
|
|
textAlign = Align.CENTER
|
|
isFakeBoldText = true
|
|
}
|
|
|
|
private val hueAlphaTrackerPaint = Paint().apply {
|
|
color = sliderTrackerColor
|
|
style = Paint.Style.STROKE
|
|
strokeWidth = context.dpToPx(2f).toFloat()
|
|
isAntiAlias = true
|
|
}
|
|
|
|
private val borderPaint = Paint()
|
|
|
|
private var valShader: Shader? = null
|
|
private var satShader: Shader? = null
|
|
private var alphaShader: Shader? = null
|
|
|
|
/*
|
|
* We cache a bitmap of the sat/val panel which is expensive to draw each time.
|
|
* We can reuse it when the user is sliding the circle picker as long as the hue isn't changed.
|
|
*/
|
|
private var satValBackgroundCache: BitmapCache? = null
|
|
|
|
/* We cache the hue background to since its also very expensive now. */
|
|
private var hueBackgroundCache: BitmapCache? = null
|
|
|
|
/* Current values */
|
|
private var alpha = 0xff
|
|
private var hue = 360f
|
|
private var sat = 0f
|
|
private var bri = 0f
|
|
private var showAlphaPanel = false
|
|
|
|
/**
|
|
* Minimum required padding. The offset from the
|
|
* edge we must have or else the finger tracker will
|
|
* get clipped when it's drawn outside of the view.
|
|
*/
|
|
private val mRequiredPadding =
|
|
context.resources.getDimensionPixelSize(R.dimen.cpv_required_padding)
|
|
|
|
/**
|
|
* The Rect in which we are allowed to draw.
|
|
* Trackers can extend outside slightly,
|
|
* due to the required padding we have set.
|
|
*/
|
|
private var drawingRect: Rect? = null
|
|
private var satValRect: Rect? = null
|
|
private var hueRect: Rect? = null
|
|
private var alphaRect: Rect? = null
|
|
private var startTouchPoint: Point? = null
|
|
private var tilePatternDrawable: TilePatternDrawable? = null
|
|
|
|
/**
|
|
* OnColorChangedListener to get notified when the color selected by the user has changed.
|
|
*/
|
|
var onColorChangedListener: OnColorChangedListener? = null
|
|
|
|
public override fun onSaveInstanceState(): Parcelable {
|
|
val state = Bundle()
|
|
state.putParcelable("instanceState", super.onSaveInstanceState())
|
|
state.putInt("alpha", alpha)
|
|
state.putFloat("hue", hue)
|
|
state.putFloat("sat", sat)
|
|
state.putFloat("val", bri)
|
|
state.putBoolean("show_alpha", showAlphaPanel)
|
|
state.putString("alpha_text", alphaSliderText)
|
|
return state
|
|
}
|
|
|
|
public override fun onRestoreInstanceState(state: Parcelable) {
|
|
if (state is Bundle) {
|
|
alpha = state.getInt("alpha")
|
|
hue = state.getFloat("hue")
|
|
sat = state.getFloat("sat")
|
|
bri = state.getFloat("val")
|
|
showAlphaPanel = state.getBoolean("show_alpha")
|
|
alphaSliderText = state.getString("alpha_text")
|
|
super.onRestoreInstanceState(state.getParcelableCompat("instanceState"))
|
|
} else {
|
|
super.onRestoreInstanceState(state)
|
|
}
|
|
}
|
|
|
|
init {
|
|
//Load those if set in xml resource file.
|
|
var a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerView)
|
|
showAlphaPanel = a.getBoolean(R.styleable.ColorPickerView_cpv_alphaChannelVisible, false)
|
|
alphaSliderText = a.getString(R.styleable.ColorPickerView_cpv_alphaChannelText)
|
|
sliderTrackerColor = a.getColor(R.styleable.ColorPickerView_cpv_sliderColor, -0x424243)
|
|
borderColor = a.getColor(R.styleable.ColorPickerView_cpv_borderColor, -0x919192)
|
|
a.recycle()
|
|
|
|
// If no specific border/slider 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()
|
|
a = context.obtainStyledAttributes(
|
|
value.data,
|
|
intArrayOf(android.R.attr.textColorSecondary)
|
|
)
|
|
if (borderColor == DEFAULT_BORDER_COLOR) {
|
|
borderColor = a.getColor(0, DEFAULT_BORDER_COLOR)
|
|
}
|
|
if (sliderTrackerColor == DEFAULT_SLIDER_COLOR) {
|
|
sliderTrackerColor = a.getColor(0, DEFAULT_SLIDER_COLOR)
|
|
}
|
|
a.recycle()
|
|
|
|
//Needed for receiving trackball motion events.
|
|
isFocusable = true
|
|
isFocusableInTouchMode = true
|
|
}
|
|
|
|
override fun onDraw(canvas: Canvas) {
|
|
val drawingRect = this.drawingRect
|
|
if (drawingRect == null || drawingRect.width() <= 0 || drawingRect.height() <= 0) {
|
|
return
|
|
}
|
|
drawSatValPanel(canvas)
|
|
drawHuePanel(canvas)
|
|
drawAlphaPanel(canvas)
|
|
}
|
|
|
|
private fun drawSatValPanel(canvas: Canvas) {
|
|
val rect = this.satValRect ?: return
|
|
val drawingRect = this.drawingRect ?: return
|
|
|
|
@Suppress("KotlinConstantConditions")
|
|
if (BORDER_WIDTH_PX > 0) {
|
|
borderPaint.color = borderColor
|
|
canvas.drawRect(
|
|
drawingRect.left.toFloat(),
|
|
drawingRect.top.toFloat(),
|
|
(rect.right + BORDER_WIDTH_PX).toFloat(),
|
|
(rect.bottom + BORDER_WIDTH_PX).toFloat(), borderPaint
|
|
)
|
|
}
|
|
if (valShader == null) {
|
|
//Black gradient has either not been created or the view has been resized.
|
|
valShader = LinearGradient(
|
|
rect.left.toFloat(),
|
|
rect.top.toFloat(),
|
|
rect.left.toFloat(),
|
|
rect.bottom.toFloat(),
|
|
-0x1,
|
|
-0x1000000,
|
|
TileMode.CLAMP
|
|
)
|
|
}
|
|
|
|
//If the hue has changed we need to recreate the cache.
|
|
if (satValBackgroundCache == null || satValBackgroundCache!!.value != hue) {
|
|
if (satValBackgroundCache == null) {
|
|
satValBackgroundCache = BitmapCache()
|
|
}
|
|
|
|
//We create our bitmap in the cache if it doesn't exist.
|
|
if (satValBackgroundCache!!.bitmap == null) {
|
|
satValBackgroundCache!!.bitmap = Bitmap
|
|
.createBitmap(rect.width(), rect.height(), Bitmap.Config.ARGB_8888)
|
|
}
|
|
|
|
//We create the canvas once so we can draw on our bitmap and the hold on to it.
|
|
if (satValBackgroundCache!!.canvas == null) {
|
|
satValBackgroundCache!!.canvas = Canvas(satValBackgroundCache!!.bitmap!!)
|
|
}
|
|
val rgb = Color.HSVToColor(floatArrayOf(hue, 1f, 1f))
|
|
satShader = LinearGradient(
|
|
rect.left.toFloat(),
|
|
rect.top.toFloat(),
|
|
rect.right.toFloat(),
|
|
rect.top.toFloat(),
|
|
-0x1,
|
|
rgb,
|
|
TileMode.CLAMP
|
|
)
|
|
satValPaint.shader = ComposeShader(
|
|
valShader!!,
|
|
satShader!!,
|
|
PorterDuff.Mode.MULTIPLY
|
|
)
|
|
|
|
// Finally we draw on our canvas, the result will be
|
|
// stored in our bitmap which is already in the cache.
|
|
// Since this is drawn on a canvas not rendered on
|
|
// screen it will automatically not be using the
|
|
// hardware acceleration. And this was the code that
|
|
// wasn't supported by hardware acceleration which mean
|
|
// there is no need to turn it of anymore. The rest of
|
|
// the view will still be hw accelerated.
|
|
satValBackgroundCache!!.canvas!!.drawRect(
|
|
0f, 0f,
|
|
satValBackgroundCache!!.bitmap!!.width.toFloat(),
|
|
satValBackgroundCache!!.bitmap!!.height.toFloat(),
|
|
satValPaint
|
|
)
|
|
|
|
//We set the hue value in our cache to which hue it was drawn with,
|
|
//then we know that if it hasn't changed we can reuse our cached bitmap.
|
|
satValBackgroundCache!!.value = hue
|
|
}
|
|
|
|
// We draw our bitmap from the cached, if the hue has changed
|
|
// then it was just recreated otherwise the old one will be used.
|
|
canvas.drawBitmap(satValBackgroundCache!!.bitmap!!, null, rect, null)
|
|
val p = satValToPoint(sat, bri)
|
|
satValTrackerPaint.color = -0x1000000
|
|
canvas.drawCircle(
|
|
p.x.toFloat(),
|
|
p.y.toFloat(),
|
|
(circleTrackerRadiusPx - context.dpToPx(1f)).toFloat(),
|
|
satValTrackerPaint
|
|
)
|
|
satValTrackerPaint.color = -0x222223
|
|
canvas.drawCircle(
|
|
p.x.toFloat(),
|
|
p.y.toFloat(),
|
|
circleTrackerRadiusPx.toFloat(),
|
|
satValTrackerPaint
|
|
)
|
|
}
|
|
|
|
private fun drawHuePanel(canvas: Canvas) {
|
|
val rect = hueRect
|
|
@Suppress("KotlinConstantConditions")
|
|
if (BORDER_WIDTH_PX > 0) {
|
|
borderPaint.color = borderColor
|
|
canvas.drawRect(
|
|
(rect!!.left - BORDER_WIDTH_PX).toFloat(), (
|
|
rect.top - BORDER_WIDTH_PX).toFloat(), (
|
|
rect.right + BORDER_WIDTH_PX).toFloat(), (
|
|
rect.bottom + BORDER_WIDTH_PX).toFloat(),
|
|
borderPaint
|
|
)
|
|
}
|
|
if (hueBackgroundCache == null) {
|
|
val hueBackgroundCache = BitmapCache()
|
|
.also { this.hueBackgroundCache = it }
|
|
|
|
Bitmap.createBitmap(rect!!.width(), rect.height(), Bitmap.Config.ARGB_8888)
|
|
.also {
|
|
hueBackgroundCache.bitmap = it
|
|
hueBackgroundCache.canvas = Canvas(it)
|
|
}
|
|
|
|
val hueColors = IntArray((rect.height() + 0.5f).toInt())
|
|
|
|
// Generate array of all colors, will be drawn as individual lines.
|
|
var h = 360f
|
|
for (i in hueColors.indices) {
|
|
hueColors[i] = Color.HSVToColor(floatArrayOf(h, 1f, 1f))
|
|
h -= 360f / hueColors.size
|
|
}
|
|
|
|
// Time to draw the hue color gradient,
|
|
// its drawn as individual lines which
|
|
// will be quite many when the resolution is high
|
|
// and/or the panel is large.
|
|
val linePaint = Paint()
|
|
linePaint.strokeWidth = 0f
|
|
for (i in hueColors.indices) {
|
|
linePaint.color = hueColors[i]
|
|
hueBackgroundCache.canvas?.drawLine(
|
|
0f,
|
|
i.toFloat(),
|
|
hueBackgroundCache.bitmap?.width?.toFloat() ?: 0f,
|
|
i.toFloat(),
|
|
linePaint
|
|
)
|
|
}
|
|
}
|
|
canvas.drawBitmap(hueBackgroundCache!!.bitmap!!, null, rect!!, null)
|
|
val p = hueToPoint(hue)
|
|
val r = RectF()
|
|
r.left = (rect.left - sliderTrackerOffsetPx).toFloat()
|
|
r.right = (rect.right + sliderTrackerOffsetPx).toFloat()
|
|
r.top = p.y - sliderTrackerSizePx / 2f
|
|
r.bottom = p.y + sliderTrackerSizePx / 2f
|
|
canvas.drawRoundRect(r, 2f, 2f, hueAlphaTrackerPaint)
|
|
}
|
|
|
|
private fun drawAlphaPanel(canvas: Canvas) {
|
|
if (!showAlphaPanel) return
|
|
val rect = this.alphaRect ?: return
|
|
val alphaPatternDrawable = this.tilePatternDrawable ?: return
|
|
|
|
/*
|
|
* Will be drawn with hw acceleration, very fast.
|
|
* Also the AlphaPatternDrawable is backed by a bitmap
|
|
* generated only once if the size does not change.
|
|
*/
|
|
@Suppress("KotlinConstantConditions")
|
|
if (BORDER_WIDTH_PX > 0) {
|
|
borderPaint.color = borderColor
|
|
canvas.drawRect(
|
|
(rect.left - BORDER_WIDTH_PX).toFloat(), (
|
|
rect.top - BORDER_WIDTH_PX).toFloat(), (
|
|
rect.right + BORDER_WIDTH_PX).toFloat(), (
|
|
rect.bottom + BORDER_WIDTH_PX).toFloat(),
|
|
borderPaint
|
|
)
|
|
}
|
|
alphaPatternDrawable.draw(canvas)
|
|
val hsv = floatArrayOf(hue, sat, bri)
|
|
val color = Color.HSVToColor(hsv)
|
|
val acolor = Color.HSVToColor(0, hsv)
|
|
alphaShader = LinearGradient(
|
|
rect.left.toFloat(),
|
|
rect.top.toFloat(),
|
|
rect.right.toFloat(),
|
|
rect.top.toFloat(),
|
|
color, acolor, TileMode.CLAMP
|
|
)
|
|
alphaPaint.shader = alphaShader
|
|
canvas.drawRect(rect, alphaPaint)
|
|
|
|
alphaSliderText
|
|
?.takeIf { it.isNotEmpty() }
|
|
?.let {
|
|
canvas.drawText(
|
|
it,
|
|
rect.centerX().toFloat(),
|
|
(rect.centerY() + context.dpToPx(4f)).toFloat(),
|
|
alphaTextPaint
|
|
)
|
|
}
|
|
|
|
val p = alphaToPoint(alpha)
|
|
val r = RectF()
|
|
r.left = p.x - sliderTrackerSizePx / 2f
|
|
r.right = p.x + sliderTrackerSizePx / 2f
|
|
r.top = (rect.top - sliderTrackerOffsetPx).toFloat()
|
|
r.bottom = (rect.bottom + sliderTrackerOffsetPx).toFloat()
|
|
canvas.drawRoundRect(r, 2f, 2f, hueAlphaTrackerPaint)
|
|
}
|
|
|
|
private fun hueToPoint(hue: Float): Point {
|
|
val rect = hueRect
|
|
val height = rect!!.height().toFloat()
|
|
val p = Point()
|
|
p.y = (height - hue * height / 360f + rect.top).toInt()
|
|
p.x = rect.left
|
|
return p
|
|
}
|
|
|
|
private fun satValToPoint(sat: Float, inValue: Float): Point {
|
|
val rect = satValRect!!
|
|
val height = rect.height().toFloat()
|
|
val width = rect.width().toFloat()
|
|
val p = Point()
|
|
p.x = (sat * width + rect.left).toInt()
|
|
p.y = ((1f - inValue) * height + rect.top).toInt()
|
|
return p
|
|
}
|
|
|
|
private fun alphaToPoint(alpha: Int): Point {
|
|
val rect = alphaRect
|
|
val width = rect!!.width().toFloat()
|
|
val p = Point()
|
|
p.x = (width - alpha * width / 0xff + rect.left).toInt()
|
|
p.y = rect.top
|
|
return p
|
|
}
|
|
|
|
private fun pointToSatVal(xArg: Float, yArg: Float): FloatArray {
|
|
var x = xArg
|
|
var y = yArg
|
|
val rect = satValRect
|
|
val result = FloatArray(2)
|
|
val width = rect!!.width().toFloat()
|
|
val height = rect.height().toFloat()
|
|
x = when {
|
|
x < rect.left -> 0f
|
|
x > rect.right -> width
|
|
else -> x - rect.left
|
|
}
|
|
y = when {
|
|
y < rect.top -> 0f
|
|
y > rect.bottom -> height
|
|
else -> y - rect.top
|
|
}
|
|
result[0] = 1f / width * x
|
|
result[1] = 1f - 1f / height * y
|
|
return result
|
|
}
|
|
|
|
private fun pointToHue(yArg: Float): Float {
|
|
var y = yArg
|
|
val rect = hueRect
|
|
val height = rect!!.height().toFloat()
|
|
y = when {
|
|
y < rect.top -> 0f
|
|
y > rect.bottom -> height
|
|
else -> y - rect.top
|
|
}
|
|
return 360f - y * 360f / height
|
|
}
|
|
|
|
private fun pointToAlpha(xArg: Int): Int {
|
|
var x = xArg
|
|
val rect = alphaRect
|
|
val width = rect!!.width()
|
|
x = when {
|
|
x < rect.left -> 0
|
|
x > rect.right -> width
|
|
else -> x - rect.left
|
|
}
|
|
return 0xff - x * 0xff / width
|
|
}
|
|
|
|
@SuppressLint("ClickableViewAccessibility")
|
|
override fun onTouchEvent(event: MotionEvent): Boolean {
|
|
try {
|
|
this.parent.requestDisallowInterceptTouchEvent(true)
|
|
} catch (ignored: Throwable) {
|
|
}
|
|
var update = false
|
|
when (event.action) {
|
|
MotionEvent.ACTION_DOWN -> {
|
|
startTouchPoint = Point(event.x.toInt(), event.y.toInt())
|
|
update = moveTrackersIfNeeded(event)
|
|
}
|
|
|
|
MotionEvent.ACTION_MOVE -> update = moveTrackersIfNeeded(event)
|
|
MotionEvent.ACTION_UP -> {
|
|
startTouchPoint = null
|
|
update = moveTrackersIfNeeded(event)
|
|
}
|
|
}
|
|
if (update) {
|
|
onColorChangedListener?.onColorChanged(
|
|
Color.HSVToColor(
|
|
alpha,
|
|
floatArrayOf(hue, sat, bri)
|
|
)
|
|
)
|
|
invalidate()
|
|
return true
|
|
}
|
|
return super.onTouchEvent(event)
|
|
}
|
|
|
|
private fun moveTrackersIfNeeded(event: MotionEvent): Boolean {
|
|
val startTouchPoint = this.startTouchPoint ?: return false
|
|
val startX = startTouchPoint.x
|
|
val startY = startTouchPoint.y
|
|
return when {
|
|
hueRect?.contains(startX, startY) == true -> {
|
|
hue = pointToHue(event.y)
|
|
true
|
|
}
|
|
|
|
satValRect?.contains(startX, startY) == true -> {
|
|
val result = pointToSatVal(event.x, event.y)
|
|
sat = result[0]
|
|
bri = result[1]
|
|
true
|
|
}
|
|
|
|
alphaRect?.contains(startX, startY) == true -> {
|
|
alpha = pointToAlpha(event.x.toInt())
|
|
true
|
|
}
|
|
|
|
else -> false
|
|
}
|
|
}
|
|
|
|
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
|
val finalWidth: Int
|
|
val finalHeight: Int
|
|
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
|
|
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
|
|
val widthAllowed = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight
|
|
val heightAllowed = MeasureSpec.getSize(heightMeasureSpec) - paddingBottom - paddingTop
|
|
if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) {
|
|
//A exact value has been set in either direction, we need to stay within this size.
|
|
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
|
|
//The with has been specified exactly, we need to adopt the height to fit.
|
|
var h = widthAllowed - panelSpacingPx - huePanelWidthPx
|
|
if (showAlphaPanel) {
|
|
h += panelSpacingPx + alphaPanelHeightPx
|
|
}
|
|
|
|
//We can't fit the view in this container, set the size to whatever was allowed.
|
|
finalHeight = min(h, heightAllowed)
|
|
finalWidth = widthAllowed
|
|
} else if (widthMode != MeasureSpec.EXACTLY) {
|
|
//The height has been specified exactly, we need to stay within this height and adopt the width.
|
|
var w = heightAllowed + panelSpacingPx + huePanelWidthPx
|
|
if (showAlphaPanel) {
|
|
w -= panelSpacingPx + alphaPanelHeightPx
|
|
}
|
|
|
|
//we can't fit within this container, set the size to whatever was allowed.
|
|
finalWidth = min(w, widthAllowed)
|
|
finalHeight = heightAllowed
|
|
} else {
|
|
//If we get here the dev has set the width and height to exact sizes. For example match_parent or 300dp.
|
|
//This will mean that the sat/val panel will not be square but it doesn't matter. It will work anyway.
|
|
//In all other senarios our goal is to make that panel square.
|
|
|
|
//We set the sizes to exactly what we were told.
|
|
finalWidth = widthAllowed
|
|
finalHeight = heightAllowed
|
|
}
|
|
} else {
|
|
//If no exact size has been set we try to make our view as big as possible
|
|
//within the allowed space.
|
|
|
|
//Calculate the needed width to layout using max allowed height.
|
|
var widthNeeded = heightAllowed + panelSpacingPx + huePanelWidthPx
|
|
|
|
//Calculate the needed height to layout using max allowed width.
|
|
var heightNeeded = widthAllowed - panelSpacingPx - huePanelWidthPx
|
|
if (showAlphaPanel) {
|
|
widthNeeded -= panelSpacingPx + alphaPanelHeightPx
|
|
heightNeeded += panelSpacingPx + alphaPanelHeightPx
|
|
}
|
|
val widthOk = widthNeeded <= widthAllowed
|
|
val heightOk = heightNeeded <= heightAllowed
|
|
when {
|
|
widthOk && heightOk -> {
|
|
finalWidth = widthAllowed
|
|
finalHeight = heightNeeded
|
|
}
|
|
|
|
widthOk -> {
|
|
finalHeight = heightAllowed
|
|
finalWidth = widthNeeded
|
|
}
|
|
|
|
heightOk -> {
|
|
finalHeight = heightNeeded
|
|
finalWidth = widthAllowed
|
|
}
|
|
|
|
else -> {
|
|
finalHeight = heightAllowed
|
|
finalWidth = widthAllowed
|
|
}
|
|
}
|
|
}
|
|
setMeasuredDimension(
|
|
finalWidth + paddingLeft + paddingRight,
|
|
finalHeight + paddingTop + paddingBottom
|
|
)
|
|
}
|
|
|
|
override fun getPaddingTop(): Int =
|
|
max(super.getPaddingTop(), mRequiredPadding)
|
|
|
|
override fun getPaddingBottom(): Int =
|
|
max(super.getPaddingBottom(), mRequiredPadding)
|
|
|
|
override fun getPaddingLeft(): Int =
|
|
max(super.getPaddingLeft(), mRequiredPadding)
|
|
|
|
override fun getPaddingRight(): Int =
|
|
max(super.getPaddingRight(), mRequiredPadding)
|
|
|
|
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
|
|
super.onSizeChanged(w, h, oldw, oldh)
|
|
val drawingRect = Rect().also { this.drawingRect = it }
|
|
drawingRect.left = paddingLeft
|
|
drawingRect.right = w - paddingRight
|
|
drawingRect.top = paddingTop
|
|
drawingRect.bottom = h - paddingBottom
|
|
|
|
//The need to be recreated because they depend on the size of the view.
|
|
valShader = null
|
|
satShader = null
|
|
alphaShader = null
|
|
|
|
// Clear those bitmap caches since the size may have changed.
|
|
satValBackgroundCache = null
|
|
hueBackgroundCache = null
|
|
setUpSatValRect()
|
|
setUpHueRect()
|
|
setUpAlphaRect()
|
|
}
|
|
|
|
private fun setUpSatValRect() {
|
|
//Calculate the size for the big color rectangle.
|
|
val dRect = drawingRect!!
|
|
val left = dRect.left + BORDER_WIDTH_PX
|
|
val top = dRect.top + BORDER_WIDTH_PX
|
|
var bottom = dRect.bottom - BORDER_WIDTH_PX
|
|
val right = dRect.right - BORDER_WIDTH_PX - panelSpacingPx - huePanelWidthPx
|
|
if (showAlphaPanel) {
|
|
bottom -= alphaPanelHeightPx + panelSpacingPx
|
|
}
|
|
satValRect = Rect(left, top, right, bottom)
|
|
}
|
|
|
|
private fun setUpHueRect() {
|
|
//Calculate the size for the hue slider on the left.
|
|
val dRect = drawingRect!!
|
|
val left = dRect.right - huePanelWidthPx + BORDER_WIDTH_PX
|
|
val top = dRect.top + BORDER_WIDTH_PX
|
|
val bottom = dRect.bottom - BORDER_WIDTH_PX -
|
|
if (showAlphaPanel) panelSpacingPx + alphaPanelHeightPx else 0
|
|
val right = dRect.right - BORDER_WIDTH_PX
|
|
hueRect = Rect(left, top, right, bottom)
|
|
}
|
|
|
|
private fun setUpAlphaRect() {
|
|
if (!showAlphaPanel) return
|
|
|
|
val dRect = drawingRect ?: return
|
|
|
|
val left = dRect.left + BORDER_WIDTH_PX
|
|
val top = dRect.bottom - alphaPanelHeightPx + BORDER_WIDTH_PX
|
|
val bottom = dRect.bottom - BORDER_WIDTH_PX
|
|
val right = dRect.right - BORDER_WIDTH_PX
|
|
|
|
val alphaRect = Rect(left, top, right, bottom)
|
|
.also { this.alphaRect = it }
|
|
|
|
tilePatternDrawable = TilePatternDrawable(context.dpToPx(4f))
|
|
.apply {
|
|
setBounds(
|
|
alphaRect.left,
|
|
alphaRect.top,
|
|
alphaRect.right,
|
|
alphaRect.bottom
|
|
)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the color this view should show.
|
|
*
|
|
* @param color The color that should be selected. #argb
|
|
* @param callback If you want to get a callback to your OnColorChangedListener.
|
|
*/
|
|
fun setColor(color: Int, callback: Boolean) {
|
|
val alpha = Color.alpha(color)
|
|
val red = Color.red(color)
|
|
val blue = Color.blue(color)
|
|
val green = Color.green(color)
|
|
val hsv = FloatArray(3)
|
|
Color.RGBToHSV(red, green, blue, hsv)
|
|
this.alpha = alpha
|
|
hue = hsv[0]
|
|
sat = hsv[1]
|
|
bri = hsv[2]
|
|
if (callback) {
|
|
onColorChangedListener
|
|
?.onColorChanged(Color.HSVToColor(this.alpha, floatArrayOf(hue, sat, bri)))
|
|
}
|
|
invalidate()
|
|
}
|
|
|
|
/**
|
|
* Set if the user is allowed to adjust the alpha panel. Default is false.
|
|
* If it is set to false no alpha will be set.
|
|
*
|
|
* @param visible `true` to show the alpha slider
|
|
*/
|
|
fun setAlphaSliderVisible(visible: Boolean) {
|
|
if (showAlphaPanel != visible) {
|
|
showAlphaPanel = visible
|
|
|
|
/*
|
|
* Force recreation.
|
|
*/
|
|
valShader = null
|
|
satShader = null
|
|
alphaShader = null
|
|
hueBackgroundCache = null
|
|
satValBackgroundCache = null
|
|
requestLayout()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set the text that should be shown in the
|
|
* alpha slider. Set to null to disable text.
|
|
*
|
|
* @param res string resource id.
|
|
*/
|
|
fun setAlphaSliderText(res: Int) {
|
|
alphaSliderText = context.getString(res)
|
|
}
|
|
} |