Skip to content

Instantly share code, notes, and snippets.

@DrMetallius
Created August 31, 2019 20:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DrMetallius/af94ac8949e12297fdae6a20e11e967a to your computer and use it in GitHub Desktop.
Save DrMetallius/af94ac8949e12297fdae6a20e11e967a to your computer and use it in GitHub Desktop.
LivingCells color picker activity
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="@dimen/margin_edge"
>
<View
android:id="@+id/colorPickerSaturationNone"
android:layout_width="@dimen/color_picker_strip_short_side"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/colorPickerSaturationStrip"
app:layout_constraintEnd_toStartOf="@id/colorPickerSaturationStrip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@id/colorPickerSaturationStrip"
tools:background="@android:color/black"
/>
<View
android:id="@+id/colorPickerSaturationStrip"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/margin_between_elements"
app:layout_constraintBottom_toTopOf="@id/colorPickerHueWheel"
app:layout_constraintEnd_toStartOf="@id/colorPickerSaturationFull"
app:layout_constraintStart_toEndOf="@id/colorPickerSaturationNone"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_weight="1"
tools:background="@android:color/holo_red_dark"
/>
<View
android:id="@+id/colorPickerSaturationFull"
android:layout_width="@dimen/color_picker_strip_short_side"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@id/colorPickerSaturationStrip"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/colorPickerSaturationStrip"
app:layout_constraintTop_toTopOf="@id/colorPickerSaturationStrip"
tools:background="@android:color/black"
/>
<View
android:id="@+id/colorPickerHueWheel"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/margin_between_elements"
app:layout_constraintBottom_toTopOf="@+id/colorPickerBrightnessStrip"
app:layout_constraintDimensionRatio="1:1"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/colorPickerSaturationStrip"
app:layout_constraintVertical_weight="4"
tools:background="@android:color/holo_green_dark"
/>
<View
android:id="@+id/colorPickerBrightnessNone"
android:layout_width="@dimen/color_picker_strip_short_side"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/colorPickerBrightnessStrip"
app:layout_constraintEnd_toStartOf="@id/colorPickerBrightnessStrip"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/colorPickerBrightnessStrip"
tools:background="@android:color/black"
/>
<View
android:id="@+id/colorPickerBrightnessStrip"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_margin="@dimen/margin_between_elements"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@id/colorPickerBrightnessFull"
app:layout_constraintStart_toEndOf="@+id/colorPickerBrightnessNone"
app:layout_constraintTop_toBottomOf="@id/colorPickerHueWheel"
app:layout_constraintVertical_weight="1"
tools:background="@android:color/holo_red_dark"
/>
<View
android:id="@+id/colorPickerBrightnessFull"
android:layout_width="@dimen/color_picker_strip_short_side"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="@+id/colorPickerBrightnessStrip"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/colorPickerBrightnessStrip"
app:layout_constraintTop_toTopOf="@+id/colorPickerBrightnessStrip"
tools:background="@android:color/black"
/>
<View
android:id="@+id/colorPickerSelectedColor"
android:layout_width="48dp"
android:layout_height="48dp"
app:layout_constraintBottom_toBottomOf="@+id/colorPickerHueWheel"
app:layout_constraintEnd_toEndOf="@+id/colorPickerHueWheel"
app:layout_constraintStart_toStartOf="@+id/colorPickerHueWheel"
app:layout_constraintTop_toTopOf="@+id/colorPickerHueWheel"
tools:background="@android:color/holo_blue_dark"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
package com.malcolmsoft.livingcells
import android.app.Activity
import android.content.Intent
import android.content.res.Configuration
import android.graphics.Color
import android.graphics.SweepGradient
import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.graphics.drawable.GradientDrawable.Orientation
import android.graphics.drawable.InsetDrawable
import android.graphics.drawable.LayerDrawable
import android.graphics.drawable.ShapeDrawable
import android.graphics.drawable.shapes.OvalShape
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.doOnLayout
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModelProviders
import kotlin.math.PI
import kotlin.math.atan2
import kotlin.math.hypot
class ColorPickerActivity : AppCompatActivity() {
private lateinit var model: ColorPickerModel
private val saturationNoneViewDrawable = ColorDrawable()
private val saturationStripViewDrawable = GradientDrawable()
private val saturationFullViewDrawable = ColorDrawable()
private val brightnessNoneViewDrawable = ColorDrawable()
private val brightnessStripViewDrawable = GradientDrawable()
private val brightnessFullViewDrawable = ColorDrawable()
private val selectedColorViewDrawable = ShapeDrawable(OvalShape())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.color_picker)
model = ViewModelProviders.of(this)[ColorPickerModel::class.java]
model.setInitialColor(intent.getIntExtra(EXTRA_COLOR, 0xFFFFFF))
val saturationNoneView: View = findViewById(R.id.colorPickerSaturationNone)
saturationNoneView.setOnClickListener {
model.setSaturation(0F)
}
saturationNoneView.background = saturationNoneViewDrawable.makeOutlined()
val saturationStripView: View = findViewById(R.id.colorPickerSaturationStrip)
saturationStripView.setOnTouchListener { v, event ->
model.setSaturation(getStripPositionFraction(v, event))
true
}
saturationStripView.background = saturationStripViewDrawable.makeOutlined()
val saturationFullView: View = findViewById(R.id.colorPickerSaturationFull)
saturationFullView.setOnClickListener {
model.setSaturation(1F)
}
saturationFullView.background = saturationFullViewDrawable.makeOutlined()
val brightnessNoneView: View = findViewById(R.id.colorPickerBrightnessNone)
brightnessNoneView.setOnClickListener {
model.setBrightness(0F)
}
brightnessNoneView.background = brightnessNoneViewDrawable.makeOutlined()
val brightnessStripView: View = findViewById(R.id.colorPickerBrightnessStrip)
brightnessStripView.background = brightnessStripViewDrawable.makeOutlined()
brightnessStripView.setOnTouchListener { v, event ->
model.setBrightness(getStripPositionFraction(v, event))
true
}
val brightnessFullView: View = findViewById(R.id.colorPickerBrightnessFull)
brightnessFullView.setOnClickListener {
model.setBrightness(1F)
}
brightnessFullView.background = brightnessFullViewDrawable.makeOutlined()
val hueWheelView: View = findViewById(R.id.colorPickerHueWheel)
hueWheelView.doOnLayout {
it.background = ShapeDrawable(OvalShape()).apply {
paint.apply {
val colors = listOf(0xFFFF0000, 0xFFFF00FF, 0xFF0000FF, 0xFF00FFFF, 0xFF00FF00, 0xFFFFFF00, 0xFFFF0000)
.map(Number::toInt)
.toIntArray()
shader = SweepGradient(it.width.toFloat() / 2, it.height.toFloat() / 2, colors, null)
}
}
}
hueWheelView.setOnTouchListener { v, event ->
val radius = v.width.toFloat() / 2
val x = event.x - radius
val y = event.y - radius
if (hypot(x, y) > radius) return@setOnTouchListener false
val angle = -atan2(y, x)
var angleDegrees = angle * 360 / (2 * PI.toFloat())
if (angleDegrees < 0) angleDegrees += 360F
model.setHue(angleDegrees)
true
}
val selectedColorView: View = findViewById(R.id.colorPickerSelectedColor)
selectedColorView.background = selectedColorViewDrawable.makeOutlined()
model.colorComponentsLiveData.observe(this, Observer(::generateBackgrounds))
}
private fun Drawable.makeOutlined(): LayerDrawable {
val outlineColor = ResourcesCompat.getColor(resources, R.color.color_picker_element_outline, null)
val outlineDrawable = when (this) {
is ShapeDrawable -> ShapeDrawable(shape.clone()).apply {
paint.color = outlineColor
}
else -> ColorDrawable(outlineColor)
}
return LayerDrawable(
arrayOf(
outlineDrawable,
InsetDrawable(this, resources.getDimensionPixelSize(R.dimen.color_picker_element_stroke_width))
)
)
}
private fun getStripPositionFraction(view: View, event: MotionEvent): Float {
val value = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
1F - event.y / view.height
} else {
event.x / view.width
}
return value.coerceIn(0F, 1F)
}
private fun generateBackgrounds(colorComponents: FloatArray) {
fun getComponentExtremes(index: Int): Pair<Int, Int> {
val buf = colorComponents.clone()
buf[index] = 0F
val zeroComponentColor = Color.HSVToColor(buf)
buf[index] = 1F
val fullComponentColor = Color.HSVToColor(buf)
return Pair(zeroComponentColor, fullComponentColor)
}
val gradientOrientation = if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
Orientation.BOTTOM_TOP
} else {
Orientation.LEFT_RIGHT
}
val (zeroSaturationColor, fullSaturationColor) = getComponentExtremes(1)
saturationNoneViewDrawable.color = zeroSaturationColor
saturationFullViewDrawable.color = fullSaturationColor
saturationStripViewDrawable.orientation = gradientOrientation
saturationStripViewDrawable.colors = intArrayOf(zeroSaturationColor, fullSaturationColor)
val (zeroBrightnessColor, fullBrightnessColor) = getComponentExtremes(2)
brightnessNoneViewDrawable.color = zeroBrightnessColor
brightnessFullViewDrawable.color = fullBrightnessColor
brightnessStripViewDrawable.orientation = gradientOrientation
brightnessStripViewDrawable.colors = intArrayOf(zeroBrightnessColor, fullBrightnessColor)
selectedColorViewDrawable.paint.color = Color.HSVToColor(colorComponents)
for (drawable in listOf(
saturationNoneViewDrawable,
saturationStripViewDrawable,
saturationFullViewDrawable,
brightnessNoneViewDrawable,
brightnessStripViewDrawable,
brightnessFullViewDrawable,
selectedColorViewDrawable
)) {
drawable.invalidateSelf()
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
menuInflater.inflate(R.menu.color_picker, menu)
return true
}
override fun onOptionsItemSelected(item: MenuItem) = when (item.itemId) {
R.id.menu_color_picker_confirm -> {
returnResult()
true
}
else -> false
}
private fun returnResult() {
setResult(Activity.RESULT_OK, Intent().apply {
putExtra(EXTRA_COLOR, model.color)
})
finish()
}
companion object {
const val CATEGORY_COLOR = "org.openintents.category.COLOR"
const val INTENT_PICK_COLOR = "org.openintents.action.PICK_COLOR"
const val EXTRA_COLOR = "org.openintents.extra.COLOR"
}
}
package com.malcolmsoft.livingcells
import android.app.Application
import android.graphics.Color
import androidx.annotation.ColorInt
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import com.malcolmsoft.utils.LiveDataPublishingProperty
class ColorPickerModel(application: Application) : AndroidViewModel(application) {
private var initialized = false
val colorComponentsLiveData: LiveData<FloatArray> = MutableLiveData<FloatArray>()
private var colorComponents: FloatArray by LiveDataPublishingProperty(FloatArray(3), colorComponentsLiveData)
val color
get() = Color.HSVToColor(colorComponents)
fun setInitialColor(@ColorInt color: Int) {
if (initialized) return
colorComponents = FloatArray(3).apply {
Color.colorToHSV(color, this)
}
initialized = true
}
fun setHue(value: Float) = setComponent(0, value)
fun setSaturation(value: Float) = setComponent(1, value)
fun setBrightness(value: Float) = setComponent(2, value)
private fun setComponent(index: Int, value: Float) {
colorComponents = colorComponents.clone().apply {
this[index] = value
}
}
}
package com.malcolmsoft.utils
import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
class LiveDataPublishingProperty<T>(private var value: T, liveData: LiveData<T>) : ReadWriteProperty<Any, T> {
private val mutableLiveData = liveData as MutableLiveData<T>
init {
mutableLiveData.setValueFromAnyThread(value)
}
override fun getValue(thisRef: Any, property: KProperty<*>): T = synchronized(thisRef) {
value
}
override fun setValue(thisRef: Any, property: KProperty<*>, value: T) = synchronized(thisRef) {
this.value = value
mutableLiveData.setValueFromAnyThread(value)
}
}
package com.malcolmsoft.utils
import android.os.Looper
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Transformations
import java.util.concurrent.ExecutorService
import java.util.concurrent.atomic.AtomicReference
private fun <T, R> makeInputNullable(fn: (T) -> R?): (T?) -> R? = { input -> input?.let { fn(input) } }
fun <T, R> LiveData<T>.map(fn: (T) -> R?): LiveData<R> = Transformations.map(this, makeInputNullable(fn))
fun <T, R> LiveData<T>.switchMap(fn: (T) -> LiveData<R>): LiveData<R> = Transformations.switchMap(this, makeInputNullable(fn))
fun <T> MutableLiveData<T>.setValueFromAnyThread(value: T?) {
if (Looper.getMainLooper() == Looper.myLooper()) {
this.value = value
} else {
postValue(value)
}
}
fun <T, U, R> combine(first: LiveData<T>, second: LiveData<U>, executor: ExecutorService? = null, merger: (T?, U?) -> R?): MediatorLiveData<R> =
combine<T, U, Nothing, Nothing, R>(first, second, executor = executor) { firstValue, secondValue, _, _ ->
merger(firstValue, secondValue)
}
fun <T, U, V, W, R> combine(
first: LiveData<T>,
second: LiveData<U>,
third: LiveData<V>? = null,
fourth: LiveData<W>? = null,
executor: ExecutorService? = null,
merger: (T?, U?, V?, W?) -> R?
): MediatorLiveData<R> {
fun MediatorLiveData<R>.calculate(firstValue: T?, secondValue: U?, thirdValue: V?, fourthValue: W?) {
if (executor != null) {
executor.submit {
merger(firstValue, secondValue, thirdValue, fourthValue)?.let { result ->
postValue(result)
}
}
} else {
merger(firstValue, secondValue, thirdValue, fourthValue)?.let { result ->
value = result
}
}
}
return MediatorLiveData<R>().apply {
addSource(first) { calculate(it, second.value, third?.value, fourth?.value) }
addSource(second) { calculate(first.value, it, third?.value, fourth?.value) }
third?.let {
addSource(third) { calculate(first.value, second.value, it, fourth?.value) }
}
fourth?.let {
addSource(fourth) { calculate(first.value, second.value, third?.value, it) }
}
}
}
class UiMessage<T>(value: T) {
private val reference = AtomicReference<T>(value)
fun takeValue(): T? = reference.getAndSet(null)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment