Created
August 31, 2019 20:40
-
-
Save DrMetallius/af94ac8949e12297fdae6a20e11e967a to your computer and use it in GitHub Desktop.
LivingCells color picker activity
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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