aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAllan Wang <me@allanwang.ca>2017-06-10 19:36:48 -0700
committerAllan Wang <me@allanwang.ca>2017-06-10 19:36:48 -0700
commit6dc743c0ba91904d27fba42a4e8e2de6a72c719a (patch)
tree50622ee7ee18e0717d122732cec8bedcd5dfc42d
parente5ea70b7efe271a9fd9fc2eea0e5b111c26a430c (diff)
downloadkau-6dc743c0ba91904d27fba42a4e8e2de6a72c719a.tar.gz
kau-6dc743c0ba91904d27fba42a4e8e2de6a72c719a.tar.bz2
kau-6dc743c0ba91904d27fba42a4e8e2de6a72c719a.zip
Add color picker animations
-rw-r--r--.idea/vcs.xml6
-rw-r--r--LICENSE38
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt214
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt14
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt11
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt4
6 files changed, 255 insertions, 32 deletions
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+ <component name="VcsDirectoryMappings">
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
+ </component>
+</project> \ No newline at end of file
diff --git a/LICENSE b/LICENSE
index b9535bf..91d8092 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,3 +1,17 @@
+ Copyright 2017 Allan Wang
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
@@ -175,27 +189,3 @@
END OF TERMS AND CONDITIONS
- APPENDIX: How to apply the Apache License to your work.
-
- To apply the Apache License to your work, attach the following
- boilerplate notice, with the fields enclosed by brackets "{}"
- replaced with your own identifying information. (Don't include
- the brackets!) The text should be enclosed in the appropriate
- comment syntax for the file format. We also recommend that a
- file or class name and description of purpose be included on the
- same "printed page" as the copyright notice for easier
- identification within third-party archives.
-
- Copyright 2017 Allan Wang
-
- Licensed under the Apache License, Version 2.0 (the "License");
- you may not use this file except in compliance with the License.
- You may obtain a copy of the License at
-
- http://www.apache.org/licenses/LICENSE-2.0
-
- Unless required by applicable law or agreed to in writing, software
- distributed under the License is distributed on an "AS IS" BASIS,
- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- See the License for the specific language governing permissions and
- limitations under the License.
diff --git a/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt
new file mode 100644
index 0000000..a40895e
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt
@@ -0,0 +1,214 @@
+package ca.allanwang.kau.dialogs.color
+
+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 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
+
+ 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 (selected) {
+ val whiteRadius = centerWidth - whiteOuterBound
+ val innerRadius = whiteRadius - borderWidthSmall
+ if (whiteRadius >= centerWidth) {
+ canvas.drawCircle(centerWidth, centerHeight, centerWidth, whitePaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, centerWidth, outerPaint)
+ canvas.drawCircle(centerWidth, centerHeight, whiteRadius, whitePaint)
+ }
+ canvas.drawCircle(centerWidth, centerHeight, innerRadius, innerPaint)
+ } else {
+ canvas.drawCircle(centerWidth, centerHeight, 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
+ 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
+ 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
+ fun shiftColorDown(@ColorInt color: Int): Int = shiftColor(color, 0.9f)
+
+ @ColorInt
+ fun shiftColorUp(@ColorInt color: Int): Int = shiftColor(color, 1.1f)
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
index f78cde0..4961211 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
@@ -12,12 +12,10 @@ import android.view.View
import android.view.ViewGroup
import android.widget.*
import ca.allanwang.kau.R
-import ca.allanwang.kau.logging.SL
import ca.allanwang.kau.utils.*
import com.afollestad.materialdialogs.DialogAction
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.Theme
-import com.afollestad.materialdialogs.color.CircleView
import com.afollestad.materialdialogs.color.FillGridView
import java.util.*
@@ -46,6 +44,7 @@ internal class ColorPickerView @JvmOverloads constructor(
if (colorsSub != null && colorsSub!!.size > value) {
dialog.setActionButton(DialogAction.NEGATIVE, builder.backText)
isInSub = true
+ invalidateGrid()
}
}
}
@@ -143,7 +142,6 @@ internal class ColorPickerView @JvmOverloads constructor(
hexInput.addTextChangedListener(customHexTextWatcher)
customRgbListener = object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
- SL.d("Progress $progress")
if (fromUser) {
val color = if (builder.allowCustomAlpha)
Color.argb(alphaSeekbar.progress,
@@ -255,10 +253,14 @@ internal class ColorPickerView @JvmOverloads constructor(
override fun onClick(v: View) {
if (v.tag != null && v.tag is String) {
val tags = (v.tag as String).split(":")
- colorIndex = tags[0].toInt()
+ if (colorIndex == tags[0].toInt()) return
+ if (colorIndex != -1) (gridView.getChildAt(colorIndex) as CircleView).animateSelected(false)
selectedColor = tags[1].toInt()
refreshColors()
- invalidateGrid()
+ 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
}
}
@@ -286,7 +288,7 @@ internal class ColorPickerView @JvmOverloads constructor(
val color: Int = if (isInSub) colorsSub!![topIndex][position] else colorsTop[position]
return view.apply {
setBackgroundColor(color)
- isSelected = (if (isInSub) subIndex else topIndex) == position
+ isSelected = colorIndex == position
tag = "$position:$color"
setOnClickListener(this@ColorGridAdapter)
setOnLongClickListener(this@ColorGridAdapter)
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
index 8e396c7..c8fc90d 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
@@ -7,6 +7,7 @@ import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.os.Build
import android.support.annotation.ColorInt
+import android.support.annotation.FloatRange
import android.support.annotation.IntRange
import android.support.v4.content.ContextCompat
import android.support.v4.graphics.drawable.DrawableCompat
@@ -20,12 +21,20 @@ import com.afollestad.materialdialogs.R
fun Int.isColorDark(): Boolean
= (0.299 * Color.red(this) + 0.587 * Color.green(this) + 0.114 * Color.blue(this)) / 255.0 < 0.5
-fun Int.toHexString(withAlpha: Boolean = false, withHexPrefix:Boolean = true): String {
+fun Int.toHexString(withAlpha: Boolean = false, withHexPrefix: Boolean = true): String {
val hex = if (withAlpha) String.format("#%08X", this)
else String.format("#%06X", 0xFFFFFF and this)
return if (withHexPrefix) hex else hex.substring(1)
}
+fun Int.toHSV(): FloatArray {
+ val hsv = FloatArray(3)
+ Color.colorToHSV(this, hsv)
+ return hsv
+}
+
+fun FloatArray.toColor():Int=Color.HSVToColor(this)
+
fun Int.isColorVisibleOn(@ColorInt color: Int, @IntRange(from = 0L, to = 255L) delta: Int = 8,
@IntRange(from = 0L, to = 255L) minAlpha: Int = 50): Boolean =
if (Color.alpha(this) < minAlpha) false
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
index 1bb8b52..16804a5 100644
--- a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
@@ -90,4 +90,6 @@ fun Context.isNetworkAvailable(): Boolean {
val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
val activeNetworkInfo = connectivityManager.activeNetworkInfo
return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
-} \ No newline at end of file
+}
+
+fun Context.getDip(value: Float): Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, value, resources.displayMetrics) \ No newline at end of file