aboutsummaryrefslogtreecommitdiff
path: root/colorpicker
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-07-08 20:25:23 -0700
committerAllan Wang <me@allanwang.ca>2017-07-08 20:25:23 -0700
commit81996038462de1be86643e95d262933c4b96c551 (patch)
tree39423b28217251e7051f86e9e4f80c47bcba20b2 /colorpicker
parent880d433e475e5be4e5d91afac145b490f9a959b7 (diff)
downloadkau-81996038462de1be86643e95d262933c4b96c551.tar.gz
kau-81996038462de1be86643e95d262933c4b96c551.tar.bz2
kau-81996038462de1be86643e95d262933c4b96c551.zip
Move components to separate modules
Diffstat (limited to 'colorpicker')
-rw-r--r--colorpicker/.gitignore1
-rw-r--r--colorpicker/build.gradle59
-rw-r--r--colorpicker/progress-proguard.txt1
-rw-r--r--colorpicker/src/androidTest/java/ca/allanwang/kau/colorpicker/ExampleInstrumentedTest.java26
-rw-r--r--colorpicker/src/main/AndroidManifest.xml1
-rw-r--r--colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/CircleView.kt228
-rw-r--r--colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPalette.kt349
-rw-r--r--colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerDialog.kt82
-rw-r--r--colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerView.kt310
-rw-r--r--colorpicker/src/main/res/values/dimens.xml3
-rw-r--r--colorpicker/src/test/java/ca/allanwang/kau/colorpicker/ExampleUnitTest.java17
11 files changed, 1077 insertions, 0 deletions
diff --git a/colorpicker/.gitignore b/colorpicker/.gitignore
new file mode 100644
index 0000000..796b96d
--- /dev/null
+++ b/colorpicker/.gitignore
@@ -0,0 +1 @@
+/build
diff --git a/colorpicker/build.gradle b/colorpicker/build.gradle
new file mode 100644
index 0000000..1833119
--- /dev/null
+++ b/colorpicker/build.gradle
@@ -0,0 +1,59 @@
+plugins {
+ id 'com.gladed.androidgitversion' version '0.3.4'
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+apply plugin: 'com.github.dcendents.android-maven'
+
+group = project.APP_GROUP
+
+android {
+ compileSdkVersion Integer.parseInt(project.TARGET_SDK)
+ buildToolsVersion project.BUILD_TOOLS
+
+ androidGitVersion {
+ codeFormat = 'MMNNPPBB'
+ prefix 'v'
+ }
+
+ defaultConfig {
+ minSdkVersion Integer.parseInt(project.MIN_SDK)
+ targetSdkVersion Integer.parseInt(project.TARGET_SDK)
+ versionCode androidGitVersion.code()
+ versionName androidGitVersion.name()
+ consumerProguardFiles 'progress-proguard.txt'
+ testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
+ }
+ buildTypes {
+ release {
+ minifyEnabled false
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+ lintOptions {
+ abortOnError false
+ checkReleaseBuilds false
+ }
+ resourcePrefix "kau_color"
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ test.java.srcDirs += 'src/test/kotlin'
+ }
+}
+
+dependencies {
+ compile fileTree(dir: 'libs', include: ['*.jar'])
+ androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
+ exclude group: 'com.android.support', module: 'support-annotations'
+ })
+ testCompile 'junit:junit:4.12'
+ compile project(':core')
+
+ compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
+ testCompile "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
+
+ compile "com.afollestad.material-dialogs:commons:${MATERIAL_DIALOG}"
+}
+
+apply from: '../artifacts.gradle'
diff --git a/colorpicker/progress-proguard.txt b/colorpicker/progress-proguard.txt
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/colorpicker/progress-proguard.txt
@@ -0,0 +1 @@
+
diff --git a/colorpicker/src/androidTest/java/ca/allanwang/kau/colorpicker/ExampleInstrumentedTest.java b/colorpicker/src/androidTest/java/ca/allanwang/kau/colorpicker/ExampleInstrumentedTest.java
new file mode 100644
index 0000000..2b22d0f
--- /dev/null
+++ b/colorpicker/src/androidTest/java/ca/allanwang/kau/colorpicker/ExampleInstrumentedTest.java
@@ -0,0 +1,26 @@
+package ca.allanwang.kau.colorpicker;
+
+import android.content.Context;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.*;
+
+/**
+ * Instrumentation test, which will execute on an Android device.
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+@RunWith(AndroidJUnit4.class)
+public class ExampleInstrumentedTest {
+ @Test
+ public void useAppContext() throws Exception {
+ // Context of the app under test.
+ Context appContext = InstrumentationRegistry.getTargetContext();
+
+ assertEquals("ca.allanwang.kau.colorpicker.test", appContext.getPackageName());
+ }
+}
diff --git a/colorpicker/src/main/AndroidManifest.xml b/colorpicker/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..5fa1ce3
--- /dev/null
+++ b/colorpicker/src/main/AndroidManifest.xml
@@ -0,0 +1 @@
+<manifest package="ca.allanwang.kau.colorpicker" />
diff --git a/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/CircleView.kt b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/CircleView.kt
new file mode 100644
index 0000000..04b26cb
--- /dev/null
+++ b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/CircleView.kt
@@ -0,0 +1,228 @@
+package ca.allanwang.kau.colorpicker
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.graphics.Rect
+import android.graphics.drawable.Drawable
+import android.graphics.drawable.RippleDrawable
+import android.graphics.drawable.ShapeDrawable
+import android.graphics.drawable.StateListDrawable
+import android.graphics.drawable.shapes.OvalShape
+import android.os.Build
+import android.support.annotation.ColorInt
+import android.support.annotation.ColorRes
+import android.support.annotation.FloatRange
+import android.support.v4.view.GravityCompat
+import android.support.v4.view.ViewCompat
+import android.util.AttributeSet
+import android.view.Gravity
+import android.widget.FrameLayout
+import android.widget.Toast
+import ca.allanwang.kau.utils.color
+import ca.allanwang.kau.utils.getDip
+import ca.allanwang.kau.utils.toColor
+import ca.allanwang.kau.utils.toHSV
+
+/**
+ * Created by Allan Wang on 2017-06-10.
+ *
+ * An extension of MaterialDialog's CircleView with animation selection
+ * [https://github.com/afollestad/material-dialogs/blob/master/commons/src/main/java/com/afollestad/materialdialogs/color/CircleView.java]
+ */
+class CircleView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : FrameLayout(context, attrs, defStyleAttr) {
+
+ private val borderWidthMicro: Float = context.getDip(1f)
+ private val borderWidthSmall: Float = context.getDip(3f)
+ private val borderWidthLarge: Float = context.getDip(5f)
+ private var whiteOuterBound: Float = borderWidthLarge
+
+ private val outerPaint: Paint = Paint().apply { isAntiAlias = true }
+ private val whitePaint: Paint = Paint().apply { isAntiAlias = true; color = Color.WHITE }
+ private val innerPaint: Paint = Paint().apply { isAntiAlias = true }
+ private var selected: Boolean = false
+ var withBorder: Boolean = false
+ get() = field
+ set(value) {
+ if (field != value) {
+ field = value
+ invalidate()
+ }
+ }
+
+ init {
+ update(Color.DKGRAY)
+ setWillNotDraw(false)
+ }
+
+ private fun update(@ColorInt color: Int) {
+ innerPaint.color = color
+ outerPaint.color = shiftColorDown(color)
+
+ val selector = createSelector(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ val states = arrayOf(intArrayOf(android.R.attr.state_pressed))
+ val colors = intArrayOf(shiftColorUp(color))
+ val rippleColors = ColorStateList(states, colors)
+ foreground = RippleDrawable(rippleColors, selector, null)
+ } else {
+ foreground = selector
+ }
+ }
+
+ override fun setBackgroundColor(@ColorInt color: Int) {
+ update(color)
+ requestLayout()
+ invalidate()
+ }
+
+ override fun setBackgroundResource(@ColorRes color: Int) {
+ setBackgroundColor(context.color(color))
+ }
+
+
+ @Deprecated("")
+ override fun setBackground(background: Drawable) {
+ throw IllegalStateException("Cannot use setBackground() on CircleView.")
+ }
+
+
+ @Deprecated("")
+ override fun setBackgroundDrawable(background: Drawable) {
+ throw IllegalStateException("Cannot use setBackgroundDrawable() on CircleView.")
+ }
+
+
+ @Deprecated("")
+ override fun setActivated(activated: Boolean) {
+ throw IllegalStateException("Cannot use setActivated() on CircleView.")
+ }
+
+ override fun setSelected(selected: Boolean) {
+ this.selected = selected
+ whiteOuterBound = borderWidthLarge
+ invalidate()
+ }
+
+ fun animateSelected(selected: Boolean) {
+ if (this.selected == selected) return
+ this.selected = true // We need to draw the other bands
+ val range = if (selected) Pair(-borderWidthSmall, borderWidthLarge) else Pair(borderWidthLarge, -borderWidthSmall)
+ val animator = ValueAnimator.ofFloat(range.first, range.second)
+ with(animator) {
+ reverse()
+ duration = 150L
+ addUpdateListener { animation ->
+ whiteOuterBound = animation.animatedValue as Float
+ invalidate()
+ }
+ addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ this@CircleView.selected = selected
+ }
+
+ override fun onAnimationCancel(animation: Animator) {
+ this@CircleView.selected = selected
+ }
+ })
+ start()
+ }
+ }
+
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ super.onMeasure(widthMeasureSpec, widthMeasureSpec)
+ setMeasuredDimension(measuredWidth, measuredWidth)
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ super.onDraw(canvas)
+ val centerWidth = (measuredWidth / 2).toFloat()
+ val centerHeight = (measuredHeight / 2).toFloat()
+ if (withBorder) canvas.drawCircle(centerWidth, centerHeight, centerWidth, whitePaint)
+ if (selected) {
+ val whiteRadius = centerWidth - whiteOuterBound
+ val innerRadius = whiteRadius - borderWidthSmall
+ if (whiteRadius >= centerWidth) {
+ canvas.drawCircle(centerWidth, centerHeight, centerWidth, whitePaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, if (withBorder) centerWidth - borderWidthMicro else centerWidth, outerPaint)
+ canvas.drawCircle(centerWidth, centerHeight, whiteRadius, whitePaint)
+ }
+ canvas.drawCircle(centerWidth, centerHeight, innerRadius, innerPaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, if (withBorder) centerWidth - borderWidthMicro else centerWidth, innerPaint)
+ }
+ }
+
+ private fun createSelector(color: Int): Drawable {
+ val darkerCircle = ShapeDrawable(OvalShape())
+ darkerCircle.paint.color = translucentColor(shiftColorUp(color))
+ val stateListDrawable = StateListDrawable()
+ stateListDrawable.addState(intArrayOf(android.R.attr.state_pressed), darkerCircle)
+ return stateListDrawable
+ }
+
+ fun showHint(color: Int) {
+ 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 cheatSheet = Toast
+ .makeText(context, String.format("#%06X", 0xFFFFFF and color), 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()
+ }
+
+ companion object {
+
+ @ColorInt
+ @JvmStatic
+ private fun translucentColor(color: Int): Int {
+ val factor = 0.7f
+ val alpha = Math.round(Color.alpha(color) * factor)
+ val red = Color.red(color)
+ val green = Color.green(color)
+ val blue = Color.blue(color)
+ return Color.argb(alpha, red, green, blue)
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColor(@ColorInt color: Int,
+ @FloatRange(from = 0.0, to = 2.0) by: Float): Int {
+ if (by == 1f) return color
+ val hsv = color.toHSV()
+ hsv[2] *= by // value component
+ return hsv.toColor()
+ }
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColorDown(@ColorInt color: Int): Int = shiftColor(color, 0.9f)
+
+ @ColorInt
+ @JvmStatic
+ fun shiftColorUp(@ColorInt color: Int): Int = shiftColor(color, 1.1f)
+ }
+} \ No newline at end of file
diff --git a/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPalette.kt b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPalette.kt
new file mode 100644
index 0000000..b08767c
--- /dev/null
+++ b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPalette.kt
@@ -0,0 +1,349 @@
+package ca.allanwang.kau.colorpicker
+
+import android.graphics.Color
+
+/**
+ * @author Aidan Follestad (afollestad)
+ */
+internal object ColorPalette {
+
+ val PRIMARY_COLORS: IntArray by lazy {
+ colorArrayOf(
+ "#F44336",
+ "#E91E63",
+ "#9C27B0",
+ "#673AB7",
+ "#3F51B5",
+ "#2196F3",
+ "#03A9F4",
+ "#00BCD4",
+ "#009688",
+ "#4CAF50",
+ "#8BC34A",
+ "#CDDC39",
+ "#FFEB3B",
+ "#FFC107",
+ "#FF9800",
+ "#FF5722",
+ "#795548",
+ "#9E9E9E",
+ "#607D8B")
+ }
+
+ val PRIMARY_COLORS_SUB: Array<IntArray> by lazy {
+ arrayOf(colorArrayOf(
+ "#FFEBEE",
+ "#FFCDD2",
+ "#EF9A9A",
+ "#E57373",
+ "#EF5350",
+ "#F44336",
+ "#E53935",
+ "#D32F2F",
+ "#C62828",
+ "#B71C1C"
+ ), colorArrayOf(
+ "#FCE4EC",
+ "#F8BBD0",
+ "#F48FB1",
+ "#F06292",
+ "#EC407A",
+ "#E91E63",
+ "#D81B60",
+ "#C2185B",
+ "#AD1457",
+ "#880E4F"
+ ), colorArrayOf(
+ "#F3E5F5",
+ "#E1BEE7",
+ "#CE93D8",
+ "#BA68C8",
+ "#AB47BC",
+ "#9C27B0",
+ "#8E24AA",
+ "#7B1FA2",
+ "#6A1B9A",
+ "#4A148C"
+ ), colorArrayOf(
+ "#EDE7F6",
+ "#D1C4E9",
+ "#B39DDB",
+ "#9575CD",
+ "#7E57C2",
+ "#673AB7",
+ "#5E35B1",
+ "#512DA8",
+ "#4527A0",
+ "#311B92"
+ ), colorArrayOf(
+ "#E8EAF6",
+ "#C5CAE9",
+ "#9FA8DA",
+ "#7986CB",
+ "#5C6BC0",
+ "#3F51B5",
+ "#3949AB",
+ "#303F9F",
+ "#283593",
+ "#1A237E"
+ ), colorArrayOf(
+ "#E3F2FD",
+ "#BBDEFB",
+ "#90CAF9",
+ "#64B5F6",
+ "#42A5F5",
+ "#2196F3",
+ "#1E88E5",
+ "#1976D2",
+ "#1565C0",
+ "#0D47A1"
+ ), colorArrayOf(
+ "#E1F5FE",
+ "#B3E5FC",
+ "#81D4FA",
+ "#4FC3F7",
+ "#29B6F6",
+ "#03A9F4",
+ "#039BE5",
+ "#0288D1",
+ "#0277BD",
+ "#01579B"
+ ), colorArrayOf(
+ "#E0F7FA",
+ "#B2EBF2",
+ "#80DEEA",
+ "#4DD0E1",
+ "#26C6DA",
+ "#00BCD4",
+ "#00ACC1",
+ "#0097A7",
+ "#00838F",
+ "#006064"
+ ), colorArrayOf(
+ "#E0F2F1",
+ "#B2DFDB",
+ "#80CBC4",
+ "#4DB6AC",
+ "#26A69A",
+ "#009688",
+ "#00897B",
+ "#00796B",
+ "#00695C",
+ "#004D40"
+ ), colorArrayOf(
+ "#E8F5E9",
+ "#C8E6C9",
+ "#A5D6A7",
+ "#81C784",
+ "#66BB6A",
+ "#4CAF50",
+ "#43A047",
+ "#388E3C",
+ "#2E7D32",
+ "#1B5E20"
+ ), colorArrayOf(
+ "#F1F8E9",
+ "#DCEDC8",
+ "#C5E1A5",
+ "#AED581",
+ "#9CCC65",
+ "#8BC34A",
+ "#7CB342",
+ "#689F38",
+ "#558B2F",
+ "#33691E"
+ ), colorArrayOf(
+ "#F9FBE7",
+ "#F0F4C3",
+ "#E6EE9C",
+ "#DCE775",
+ "#D4E157",
+ "#CDDC39",
+ "#C0CA33",
+ "#AFB42B",
+ "#9E9D24",
+ "#827717"
+ ), colorArrayOf(
+ "#FFFDE7",
+ "#FFF9C4",
+ "#FFF59D",
+ "#FFF176",
+ "#FFEE58",
+ "#FFEB3B",
+ "#FDD835",
+ "#FBC02D",
+ "#F9A825",
+ "#F57F17"
+ ), colorArrayOf(
+ "#FFF8E1",
+ "#FFECB3",
+ "#FFE082",
+ "#FFD54F",
+ "#FFCA28",
+ "#FFC107",
+ "#FFB300",
+ "#FFA000",
+ "#FF8F00",
+ "#FF6F00"
+ ), colorArrayOf(
+ "#FFF3E0",
+ "#FFE0B2",
+ "#FFCC80",
+ "#FFB74D",
+ "#FFA726",
+ "#FF9800",
+ "#FB8C00",
+ "#F57C00",
+ "#EF6C00",
+ "#E65100"
+ ), colorArrayOf(
+ "#FBE9E7",
+ "#FFCCBC",
+ "#FFAB91",
+ "#FF8A65",
+ "#FF7043",
+ "#FF5722",
+ "#F4511E",
+ "#E64A19",
+ "#D84315",
+ "#BF360C"
+ ), colorArrayOf(
+ "#EFEBE9",
+ "#D7CCC8",
+ "#BCAAA4",
+ "#A1887F",
+ "#8D6E63",
+ "#795548",
+ "#6D4C41",
+ "#5D4037",
+ "#4E342E",
+ "#3E2723"
+ ), colorArrayOf(
+ "#FAFAFA",
+ "#F5F5F5",
+ "#EEEEEE",
+ "#E0E0E0",
+ "#BDBDBD",
+ "#9E9E9E",
+ "#757575",
+ "#616161",
+ "#424242",
+ "#212121"
+ ), colorArrayOf(
+ "#ECEFF1",
+ "#CFD8DC",
+ "#B0BEC5",
+ "#90A4AE",
+ "#78909C",
+ "#607D8B",
+ "#546E7A",
+ "#455A64",
+ "#37474F",
+ "#263238"))
+ }
+
+ val ACCENT_COLORS: IntArray by lazy {
+ colorArrayOf(
+ "#FF1744",
+ "#F50057",
+ "#D500F9",
+ "#651FFF",
+ "#3D5AFE",
+ "#2979FF",
+ "#00B0FF",
+ "#00E5FF",
+ "#1DE9B6",
+ "#00E676",
+ "#76FF03",
+ "#C6FF00",
+ "#FFEA00",
+ "#FFC400",
+ "#FF9100",
+ "#FF3D00")
+ }
+
+ val ACCENT_COLORS_SUB: Array<IntArray> by lazy {
+ arrayOf(colorArrayOf("#FF8A80",
+ "#FF5252",
+ "#FF1744",
+ "#D50000"
+ ), colorArrayOf(
+ "#FF80AB",
+ "#FF4081",
+ "#F50057",
+ "#C51162"
+ ), colorArrayOf(
+ "#EA80FC",
+ "#E040FB",
+ "#D500F9",
+ "#AA00FF"
+ ), colorArrayOf(
+ "#B388FF",
+ "#7C4DFF",
+ "#651FFF",
+ "#6200EA"
+ ), colorArrayOf(
+ "#8C9EFF",
+ "#536DFE",
+ "#3D5AFE",
+ "#304FFE"
+ ), colorArrayOf(
+ "#82B1FF",
+ "#448AFF",
+ "#2979FF",
+ "#2962FF"
+ ), colorArrayOf(
+ "#80D8FF",
+ "#40C4FF",
+ "#00B0FF",
+ "#0091EA"
+ ), colorArrayOf(
+ "#84FFFF",
+ "#18FFFF",
+ "#00E5FF",
+ "#00B8D4"
+ ), colorArrayOf(
+ "#A7FFEB",
+ "#64FFDA",
+ "#1DE9B6",
+ "#00BFA5"
+ ), colorArrayOf(
+ "#B9F6CA",
+ "#69F0AE",
+ "#00E676",
+ "#00C853"
+ ), colorArrayOf(
+ "#CCFF90",
+ "#B2FF59",
+ "#76FF03",
+ "#64DD17"
+ ), colorArrayOf(
+ "#F4FF81",
+ "#EEFF41",
+ "#C6FF00",
+ "#AEEA00"
+ ), colorArrayOf(
+ "#FFFF8D",
+ "#FFFF00",
+ "#FFEA00",
+ "#FFD600"
+ ), colorArrayOf(
+ "#FFE57F",
+ "#FFD740",
+ "#FFC400",
+ "#FFAB00"
+ ), colorArrayOf(
+ "#FFD180",
+ "#FFAB40",
+ "#FF9100",
+ "#FF6D00"
+ ), colorArrayOf(
+ "#FF9E80",
+ "#FF6E40",
+ "#FF3D00",
+ "#DD2C00"))
+ }
+
+ fun colorArrayOf(vararg colors: String) = colors.map { Color.parseColor(it) }.toIntArray()
+}
+
diff --git a/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerDialog.kt b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerDialog.kt
new file mode 100644
index 0000000..6d23ade
--- /dev/null
+++ b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerDialog.kt
@@ -0,0 +1,82 @@
+package ca.allanwang.kau.colorpicker
+
+import android.content.Context
+import android.graphics.Color
+import android.support.annotation.DimenRes
+import android.support.annotation.StringRes
+import ca.allanwang.kau.R
+import ca.allanwang.kau.utils.string
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.Theme
+
+class ColorBuilder : ColorContract {
+ override var title: String? = null
+ override var titleRes: Int = -1
+ override var allowCustom: Boolean = true
+ override var allowCustomAlpha: Boolean = false
+ override var isAccent: Boolean = false
+ override var defaultColor: Int = Color.BLACK
+ override var doneText: Int = R.string.kau_done
+ override var backText: Int = R.string.kau_back
+ override var cancelText: Int = R.string.kau_cancel
+ override var presetText: Int = R.string.kau_md_presets
+ override var customText: Int = R.string.kau_md_custom
+ get() = if (allowCustom) field else 0
+ override var dynamicButtonColors: Boolean = true
+ override var circleSizeRes: Int = R.dimen.kau_color_circle_size
+ override var colorCallback: ((selectedColor: Int) -> Unit)? = null
+ override var colorsTop: IntArray? = null
+ override var colorsSub: Array<IntArray>? = null
+ override var theme: Theme? = null
+}
+
+interface ColorContract {
+ var title: String?
+ var titleRes: Int @StringRes set
+ var allowCustom: Boolean
+ var allowCustomAlpha: Boolean
+ var isAccent: Boolean
+ var defaultColor: Int @StringRes set
+ var doneText: Int @StringRes set
+ var backText: Int @StringRes set
+ var cancelText: Int @StringRes set
+ var presetText: Int
+ @StringRes set
+ var customText: Int @StringRes set
+ var dynamicButtonColors: Boolean
+ var circleSizeRes: Int @DimenRes set
+ var colorCallback: ((selectedColor: Int) -> Unit)?
+ var colorsTop: IntArray?
+ var colorsSub: Array<IntArray>?
+ var theme: Theme?
+}
+
+/**
+ * This is the extension that allows us to initialize the dialog
+ * Note that this returns just the dialog; you still need to call .show() to show it
+ */
+fun Context.colorPickerDialog(action: ColorContract.() -> Unit): MaterialDialog {
+ val b = ColorBuilder()
+ b.action()
+ return colorPickerDialog(b)
+}
+
+fun Context.colorPickerDialog(contract: ColorContract): MaterialDialog {
+ val view = ColorPickerView(this)
+ val dialog = with(MaterialDialog.Builder(this)) {
+ title(string(contract.titleRes, contract.title) ?: string(R.string.kau_md_color_palette))
+ customView(view, false)
+ autoDismiss(false)
+ positiveText(contract.doneText)
+ negativeText(contract.cancelText)
+ if (contract.allowCustom) neutralText(contract.presetText)
+ onPositive { dialog, _ -> contract.colorCallback?.invoke(view.selectedColor); dialog.dismiss() }
+ onNegative { _, _ -> view.backOrCancel() }
+ if (contract.allowCustom) onNeutral { _, _ -> view.toggleCustom() }
+ showListener { view.refreshColors() }
+ if (contract.theme != null) theme(contract.theme!!)
+ build()
+ }
+ view.bind(contract, dialog)
+ return dialog
+} \ No newline at end of file
diff --git a/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerView.kt b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerView.kt
new file mode 100644
index 0000000..d86518c
--- /dev/null
+++ b/colorpicker/src/main/kotlin/ca/allanwang/kau/colorpicker/ColorPickerView.kt
@@ -0,0 +1,310 @@
+package ca.allanwang.kau.colorpicker
+
+import android.content.Context
+import android.graphics.Color
+import android.support.annotation.ColorInt
+import android.support.v4.content.res.ResourcesCompat
+import android.text.Editable
+import android.text.InputFilter
+import android.text.TextWatcher
+import android.util.AttributeSet
+import android.view.View
+import android.view.ViewGroup
+import android.widget.*
+import ca.allanwang.kau.R
+import ca.allanwang.kau.colorpicker.CircleView
+import ca.allanwang.kau.utils.*
+import com.afollestad.materialdialogs.DialogAction
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.color.FillGridView
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ *
+ * ColorPicker component of the ColorPickerDialog
+ */
+internal class ColorPickerView @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : ScrollView(context, attrs, defStyleAttr) {
+ var selectedColor: Int = -1
+ var isInSub: Boolean = false
+ var isInCustom: Boolean = false
+ var circleSize: Int = context.dimen(R.dimen.kau_cp_circle_size).toInt()
+ val backgroundColor = context.resolveColor(R.attr.md_background_color,
+ if (context.resolveColor(android.R.attr.textColorPrimary).isColorDark) Color.WHITE else 0xff424242.toInt())
+ val backgroundColorTint = backgroundColor.colorToForeground()
+ lateinit var dialog: MaterialDialog
+ lateinit var builder: ColorContract
+ lateinit var colorsTop: IntArray
+ var colorsSub: Array<IntArray>? = null
+ var topIndex: Int = -1
+ var subIndex: Int = -1
+ var colorIndex: Int
+ get() = if (isInSub) subIndex else topIndex
+ set(value) {
+ if (isInSub) subIndex = value
+ else {
+ topIndex = value
+ if (colorsSub != null && colorsSub!!.size > value) {
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.backText)
+ isInSub = true
+ invalidateGrid()
+ }
+ }
+ }
+
+
+ val gridView: FillGridView by bindView(R.id.md_grid)
+ val customFrame: LinearLayout by bindView(R.id.md_colorChooserCustomFrame)
+ val customColorIndicator: View by bindView(R.id.md_colorIndicator)
+ val hexInput: EditText by bindView(R.id.md_hexInput)
+ val alphaLabel: TextView by bindView(R.id.md_colorALabel)
+ val alphaSeekbar: SeekBar by bindView(R.id.md_colorA)
+ val alphaValue: TextView by bindView(R.id.md_colorAValue)
+ val redSeekbar: SeekBar by bindView(R.id.md_colorR)
+ val redValue: TextView by bindView(R.id.md_colorRValue)
+ val greenSeekbar: SeekBar by bindView(R.id.md_colorG)
+ val greenValue: TextView by bindView(R.id.md_colorGValue)
+ val blueSeekbar: SeekBar by bindView(R.id.md_colorB)
+ val blueValue: TextView by bindView(R.id.md_colorBValue)
+
+ var customHexTextWatcher: TextWatcher? = null
+ var customRgbListener: SeekBar.OnSeekBarChangeListener? = null
+
+ init {
+ View.inflate(context, R.layout.md_dialog_colorchooser, this)
+ }
+
+ fun bind(builder: ColorContract, dialog: MaterialDialog) {
+ this.builder = builder
+ this.dialog = dialog
+ this.colorsTop = with(builder) {
+ if (colorsTop != null) colorsTop!!
+ else if (isAccent) ColorPalette.ACCENT_COLORS
+ else ColorPalette.PRIMARY_COLORS
+ }
+ this.colorsSub = with(builder) {
+ if (colorsTop != null) colorsSub
+ else if (isAccent) ColorPalette.ACCENT_COLORS_SUB
+ else ColorPalette.PRIMARY_COLORS_SUB
+ }
+ this.selectedColor = builder.defaultColor
+ if (builder.allowCustom) {
+ if (!builder.allowCustomAlpha) {
+ alphaLabel.gone()
+ alphaSeekbar.gone()
+ alphaValue.gone()
+ hexInput.hint = String.format("%06X", selectedColor)
+ hexInput.filters = arrayOf(InputFilter.LengthFilter(6))
+ } else {
+ hexInput.hint = String.format("%08X", selectedColor)
+ hexInput.filters = arrayOf(InputFilter.LengthFilter(8))
+ }
+ }
+ if (findColor(builder.defaultColor) || !builder.allowCustom) isInCustom = true //when toggled this will be false
+ toggleCustom()
+ }
+
+ fun backOrCancel() {
+ if (isInSub) {
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.cancelText)
+ //to top
+ isInSub = false
+ subIndex = -1
+ invalidateGrid()
+ } else {
+ dialog.cancel()
+ }
+ }
+
+ fun toggleCustom() {
+ isInCustom = !isInCustom
+ if (isInCustom) {
+ isInSub = false
+ if (builder.allowCustom) dialog.setActionButton(DialogAction.NEUTRAL, builder.presetText)
+ dialog.setActionButton(DialogAction.NEGATIVE, builder.cancelText)
+ customHexTextWatcher = object : TextWatcher {
+ override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
+
+ override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
+ try {
+ selectedColor = Color.parseColor("#" + s.toString())
+ } catch (e: IllegalArgumentException) {
+ selectedColor = Color.BLACK
+ }
+
+ customColorIndicator.setBackgroundColor(selectedColor)
+ if (alphaSeekbar.isVisible()) {
+ val alpha = Color.alpha(selectedColor)
+ alphaSeekbar.progress = alpha
+ alphaValue.text = String.format(Locale.CANADA, "%d", alpha)
+ }
+ redSeekbar.progress = Color.red(selectedColor)
+ greenSeekbar.progress = Color.green(selectedColor)
+ blueSeekbar.progress = Color.blue(selectedColor)
+ isInSub = false
+ topIndex = -1
+ subIndex = -1
+ refreshColors()
+ }
+
+ override fun afterTextChanged(s: Editable?) {}
+ }
+ hexInput.setText(selectedColor.toHexString(builder.allowCustomAlpha, false))
+ hexInput.addTextChangedListener(customHexTextWatcher)
+ customRgbListener = object : SeekBar.OnSeekBarChangeListener {
+ override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
+ if (fromUser) {
+ val color = if (builder.allowCustomAlpha)
+ Color.argb(alphaSeekbar.progress,
+ redSeekbar.progress,
+ greenSeekbar.progress,
+ blueSeekbar.progress)
+ else Color.rgb(redSeekbar.progress,
+ greenSeekbar.progress,
+ blueSeekbar.progress)
+
+ hexInput.setText(color.toHexString(builder.allowCustomAlpha, false))
+ }
+ if (builder.allowCustomAlpha) alphaValue.text = alphaSeekbar.progress.toString()
+ redValue.text = redSeekbar.progress.toString()
+ greenValue.text = greenSeekbar.progress.toString()
+ blueValue.text = blueSeekbar.progress.toString()
+ }
+
+ override fun onStartTrackingTouch(seekBar: SeekBar) {}
+
+ override fun onStopTrackingTouch(seekBar: SeekBar) {}
+ }
+ redSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ greenSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ blueSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ if (alphaSeekbar.isVisible())
+ alphaSeekbar.setOnSeekBarChangeListener(customRgbListener)
+ hexInput.setText(selectedColor.toHexString(alphaSeekbar.isVisible(), false))
+ gridView.fadeOut(onFinish = { gridView.gone() })
+ customFrame.fadeIn()
+ } else {
+ findColor(selectedColor)
+ if (builder.allowCustom) dialog.setActionButton(DialogAction.NEUTRAL, builder.customText)
+ dialog.setActionButton(DialogAction.NEGATIVE, if (isInSub) builder.backText else builder.cancelText)
+ gridView.fadeIn(onStart = { invalidateGrid() })
+ customFrame.fadeOut(onFinish = { customFrame.gone() })
+ hexInput.removeTextChangedListener(customHexTextWatcher)
+ customHexTextWatcher = null
+ alphaSeekbar.setOnSeekBarChangeListener(null)
+ redSeekbar.setOnSeekBarChangeListener(null)
+ greenSeekbar.setOnSeekBarChangeListener(null)
+ blueSeekbar.setOnSeekBarChangeListener(null)
+ customRgbListener = null
+ }
+ }
+
+ fun refreshColors() {
+ if (!isInCustom) findColor(selectedColor)
+ //Ensure that our tinted color is still visible against the background
+ val visibleColor = if (selectedColor.isColorVisibleOn(backgroundColor)) selectedColor else backgroundColorTint
+ if (builder.dynamicButtonColors) {
+ dialog.getActionButton(DialogAction.POSITIVE).setTextColor(visibleColor)
+ dialog.getActionButton(DialogAction.NEGATIVE).setTextColor(visibleColor)
+ dialog.getActionButton(DialogAction.NEUTRAL).setTextColor(visibleColor)
+ }
+ if (!builder.allowCustom || !isInCustom) return
+ if (builder.allowCustomAlpha)
+ alphaSeekbar.visible().tint(visibleColor)
+ redSeekbar.tint(visibleColor)
+ greenSeekbar.tint(visibleColor)
+ blueSeekbar.tint(visibleColor)
+ hexInput.tint(visibleColor)
+ }
+
+ fun findColor(@ColorInt color: Int): Boolean {
+ topIndex = -1
+ subIndex = -1
+ colorsTop.forEachIndexed {
+ index, topColor ->
+ if (findSubColor(color, index)) {
+ topIndex = index
+ return true
+ }
+ if (topColor == color) { // If no sub colors exists and top color matches
+ topIndex = index
+ return true
+ }
+ }
+ return false
+ }
+
+ fun findSubColor(@ColorInt color: Int, topIndex: Int): Boolean {
+ if (colorsSub == null || colorsSub!!.size <= topIndex) return false
+ colorsSub!![topIndex].forEachIndexed {
+ index, subColor ->
+ if (subColor == color) {
+ subIndex = index
+ return true
+ }
+ }
+ return false
+ }
+
+ fun invalidateGrid() {
+ if (gridView.adapter == null) {
+ gridView.adapter = ColorGridAdapter()
+ gridView.selector = ResourcesCompat.getDrawable(resources, R.drawable.kau_transparent, null)
+ } else {
+ (gridView.adapter as BaseAdapter).notifyDataSetChanged()
+ }
+ }
+
+ inner class ColorGridAdapter : BaseAdapter(), OnClickListener, OnLongClickListener {
+ override fun onClick(v: View) {
+ if (v.tag != null && v.tag is String) {
+ val tags = (v.tag as String).split(":")
+ if (colorIndex == tags[0].toInt()) {
+ colorIndex = tags[0].toInt() //Go to sub list if exists
+ return
+ }
+ if (colorIndex != -1) (gridView.getChildAt(colorIndex) as CircleView).animateSelected(false)
+ selectedColor = tags[1].toInt()
+ refreshColors()
+ val currentSub = isInSub
+ colorIndex = tags[0].toInt()
+ if (currentSub == isInSub) (gridView.getChildAt(colorIndex) as CircleView).animateSelected(true)
+ //Otherwise we are invalidating our grid, so there is no point in animating
+ }
+ }
+
+ override fun onLongClick(v: View): Boolean {
+ if (v.tag != null && v.tag is String) {
+ val tag = (v.tag as String).split(":")
+ val color = tag[1].toInt()
+ (v as CircleView).showHint(color)
+ return true
+ }
+ return false
+ }
+
+ override fun getItem(position: Int): Any = if (isInSub) colorsSub!![topIndex][position] else colorsTop[position]
+
+ override fun getCount(): Int = if (isInSub) colorsSub!![topIndex].size else colorsTop.size
+
+ override fun getItemId(position: Int): Long = position.toLong()
+
+ override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
+ val view: CircleView = if (convertView == null)
+ CircleView(context).apply { layoutParams = AbsListView.LayoutParams(circleSize, circleSize) }
+ else
+ convertView as CircleView
+ val color: Int = if (isInSub) colorsSub!![topIndex][position] else colorsTop[position]
+ return view.apply {
+ setBackgroundColor(color)
+ isSelected = colorIndex == position
+ tag = "$position:$color"
+ setOnClickListener(this@ColorGridAdapter)
+ setOnLongClickListener(this@ColorGridAdapter)
+ }
+ }
+
+ }
+} \ No newline at end of file
diff --git a/colorpicker/src/main/res/values/dimens.xml b/colorpicker/src/main/res/values/dimens.xml
new file mode 100644
index 0000000..193940e
--- /dev/null
+++ b/colorpicker/src/main/res/values/dimens.xml
@@ -0,0 +1,3 @@
+<resources>
+ <dimen name="kau_color_circle_size">56dp</dimen>
+</resources>
diff --git a/colorpicker/src/test/java/ca/allanwang/kau/colorpicker/ExampleUnitTest.java b/colorpicker/src/test/java/ca/allanwang/kau/colorpicker/ExampleUnitTest.java
new file mode 100644
index 0000000..f974ff7
--- /dev/null
+++ b/colorpicker/src/test/java/ca/allanwang/kau/colorpicker/ExampleUnitTest.java
@@ -0,0 +1,17 @@
+package ca.allanwang.kau.colorpicker;
+
+import org.junit.Test;
+
+import static org.junit.Assert.*;
+
+/**
+ * Example local unit test, which will execute on the development machine (host).
+ *
+ * @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
+ */
+public class ExampleUnitTest {
+ @Test
+ public void addition_isCorrect() throws Exception {
+ assertEquals(4, 2 + 2);
+ }
+} \ No newline at end of file