From 6dc743c0ba91904d27fba42a4e8e2de6a72c719a Mon Sep 17 00:00:00 2001 From: Allan Wang Date: Sat, 10 Jun 2017 19:36:48 -0700 Subject: Add color picker animations --- .../ca/allanwang/kau/dialogs/color/CircleView.kt | 214 +++++++++++++++++++++ .../kau/dialogs/color/ColorPickerDialog.kt | 14 +- .../kotlin/ca/allanwang/kau/utils/ColorUtils.kt | 11 +- .../kotlin/ca/allanwang/kau/utils/ContextUtils.kt | 4 +- 4 files changed, 235 insertions(+), 8 deletions(-) create mode 100644 library/src/main/kotlin/ca/allanwang/kau/dialogs/color/CircleView.kt (limited to 'library/src/main/kotlin/ca') 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 -- cgit v1.2.3