diff options
author | Allan Wang <me@allanwang.ca> | 2017-07-08 20:25:23 -0700 |
---|---|---|
committer | Allan Wang <me@allanwang.ca> | 2017-07-08 20:25:23 -0700 |
commit | 81996038462de1be86643e95d262933c4b96c551 (patch) | |
tree | 39423b28217251e7051f86e9e4f80c47bcba20b2 /colorpicker | |
parent | 880d433e475e5be4e5d91afac145b490f9a959b7 (diff) | |
download | kau-81996038462de1be86643e95d262933c4b96c551.tar.gz kau-81996038462de1be86643e95d262933c4b96c551.tar.bz2 kau-81996038462de1be86643e95d262933c4b96c551.zip |
Move components to separate modules
Diffstat (limited to 'colorpicker')
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 |