aboutsummaryrefslogtreecommitdiff
path: root/library/src/main/kotlin/ca
diff options
context:
space:
mode:
Diffstat (limited to 'library/src/main/kotlin/ca')
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt73
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt349
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt352
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerPreference.kt72
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt32
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt46
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt83
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt34
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt35
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt22
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt35
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt83
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/logging/SL.kt6
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt15
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt125
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt183
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/Const.kt6
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt93
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt24
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt19
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt137
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/LazyResettable.kt51
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt22
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt11
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt73
-rw-r--r--library/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt79
26 files changed, 2060 insertions, 0 deletions
diff --git a/library/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt b/library/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt
new file mode 100644
index 0000000..14b8c4e
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/changelog/Changelog.kt
@@ -0,0 +1,73 @@
+package com.pitchedapps.frost.utils
+
+import android.content.Context
+import android.content.res.XmlResourceParser
+import android.support.annotation.LayoutRes
+import android.support.annotation.XmlRes
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.TextView
+import ca.allanwang.kau.R
+import org.xmlpull.v1.XmlPullParser
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+internal class ChangelogAdapter(val items: List<Pair<String, ChangelogType>>) : RecyclerView.Adapter<ChangelogAdapter.ChangelogVH>() {
+
+ override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = ChangelogVH(LayoutInflater.from(parent.context)
+ .inflate(getLayout(viewType), parent, false))
+
+ private fun getLayout(position: Int) = items[position].second.layout
+
+ override fun onBindViewHolder(holder: ChangelogVH, position: Int) {
+ holder.text.text = items[position].first
+ }
+
+ override fun getItemId(position: Int) = position.toLong()
+
+ override fun getItemViewType(position: Int) = position
+
+ override fun getItemCount() = items.size
+
+ internal class ChangelogVH(itemView: View) : RecyclerView.ViewHolder(itemView) {
+ val text: TextView = itemView.findViewById(R.id.kau_changelog_text) as TextView
+ }
+}
+
+internal fun parse(context: Context, @XmlRes xmlRes: Int): List<Pair<String, ChangelogType>> {
+ val items = mutableListOf<Pair<String, ChangelogType>>()
+ context.resources.getXml(xmlRes).use {
+ parser ->
+ var eventType = parser.eventType
+ while (eventType != XmlPullParser.END_DOCUMENT) {
+ if (eventType == XmlPullParser.START_TAG)
+ ChangelogType.values.any { it.add(parser, items) }
+ eventType = parser.next()
+ }
+ }
+ return items
+}
+
+internal enum class ChangelogType(val tag: String, val attr: String, @LayoutRes val layout: Int) {
+ TITLE("title", "version", R.layout.kau_changelog_title),
+ ITEM("item", "text", R.layout.kau_changelog_content);
+
+ companion object {
+ val values = values()
+ }
+
+ /**
+ * Returns true if tag matches; false otherwise
+ */
+ fun add(parser: XmlResourceParser, list: MutableList<Pair<String, ChangelogType>>): Boolean {
+ if (parser.name != tag) return false
+ if (parser.getAttributeValue(null, attr).isNotBlank())
+ list.add(Pair(parser.getAttributeValue(null, attr), this))
+ return true
+ }
+}
+
diff --git a/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt
new file mode 100644
index 0000000..22bd0d4
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPalette.kt
@@ -0,0 +1,349 @@
+package ca.allanwang.kau.dialogs.color
+
+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/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
new file mode 100644
index 0000000..f78cde0
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerDialog.kt
@@ -0,0 +1,352 @@
+package ca.allanwang.kau.dialogs.color
+
+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.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.*
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+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_color_circle_size).toInt()
+ lateinit var dialog: MaterialDialog
+ lateinit var builder: Builder
+ 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
+ }
+ }
+ }
+
+
+ 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: Builder, dialog: MaterialDialog) {
+ this.builder = builder
+ this.dialog = dialog
+ this.colorsTop = builder.colorsTop()
+ this.colorsSub = builder.colorsSub()
+ 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)) 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
+ 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) {
+ SL.d("Progress $progress")
+ 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)
+ 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)
+ if (builder.dynamicButtonColors) {
+ dialog.getActionButton(DialogAction.POSITIVE).setTextColor(selectedColor)
+ dialog.getActionButton(DialogAction.NEGATIVE).setTextColor(selectedColor)
+ dialog.getActionButton(DialogAction.NEUTRAL).setTextColor(selectedColor)
+ }
+ if (!builder.allowCustom || !isInCustom) return
+ // Once we get close to white or transparent,
+ // the action buttons and seekbars will be a very light gray.
+ // TODO change by dialog theme
+ val visibleColor = if (Color.alpha(selectedColor) < 64 ||
+ Color.red(selectedColor) > 247 && Color.green(selectedColor) > 247 && Color.blue(selectedColor) > 247)
+ "#DEDEDE".toColor() else selectedColor
+
+ 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(":")
+ colorIndex = tags[0].toInt()
+ selectedColor = tags[1].toInt()
+ refreshColors()
+ invalidateGrid()
+ }
+ }
+
+ 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 = (if (isInSub) subIndex else topIndex) == position
+ tag = "$position:$color"
+ setOnClickListener(this@ColorGridAdapter)
+ setOnLongClickListener(this@ColorGridAdapter)
+ }
+ }
+
+ }
+}
+
+class Builder {
+ var title: String? = null
+ var titleRes: Int = -1
+ var allowCustom: Boolean = true
+ var allowCustomAlpha: Boolean = false
+ var isAccent: Boolean = false
+ var defaultColor: Int = Color.BLACK
+ var doneText: Int = R.string.kau_done
+ var backText: Int = R.string.kau_back
+ var cancelText: Int = R.string.kau_cancel
+ var presetText: Int = R.string.kau_md_presets
+ var customText: Int = R.string.kau_md_custom
+ get() = if (allowCustom) field else 0
+ var dynamicButtonColors: Boolean = true
+ var circleSizeRes: Int = R.dimen.kau_color_circle_size
+ var colorCallbacks: MutableList<((selectedColor: Int) -> Unit)> = mutableListOf()
+ var colorsTop: IntArray? = null
+ internal fun colorsTop(): IntArray =
+ if (colorsTop != null) colorsTop!!
+ else if (isAccent) ColorPalette.ACCENT_COLORS
+ else ColorPalette.PRIMARY_COLORS
+
+ var colorsSub: Array<IntArray>? = null
+ internal fun colorsSub(): Array<IntArray>? =
+ if (colorsTop != null) colorsSub
+ else if (isAccent) ColorPalette.ACCENT_COLORS_SUB
+ else ColorPalette.PRIMARY_COLORS_SUB
+
+ var theme: Theme? = null
+
+}
+
+
+fun Context.colorPickerDialog(action: Builder.() -> Unit): MaterialDialog {
+ val b = Builder()
+ b.action()
+ val view = ColorPickerView(this)
+ val dialog = with(MaterialDialog.Builder(this)) {
+ title(string(b.titleRes, b.title) ?: string(R.string.kau_md_color_palette))
+ customView(view, false)
+ autoDismiss(false)
+ positiveText(b.doneText)
+ negativeText(b.cancelText)
+ neutralText(b.presetText)
+ onPositive { dialog, _ -> b.colorCallbacks.forEach { it.invoke(view.selectedColor) }; dialog.dismiss() }
+ onNegative { dialog, _ -> view.backOrCancel() }
+ if (b.allowCustom) onNeutral { dialog, _ -> view.toggleCustom() }
+ showListener { view.refreshColors() }
+ if (b.theme != null) theme(b.theme!!)
+ build()
+ }
+ view.bind(b, dialog)
+ return dialog
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerPreference.kt b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerPreference.kt
new file mode 100644
index 0000000..043e287
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/dialogs/color/ColorPickerPreference.kt
@@ -0,0 +1,72 @@
+package ca.allanwang.kau.dialogs.color
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.os.Parcelable
+import android.support.annotation.ColorInt
+import android.support.annotation.StringRes
+import android.support.v4.content.res.ResourcesCompat
+import android.support.v7.app.AppCompatActivity
+import android.support.v7.preference.Preference
+import android.text.Editable
+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.utils.ANDROID_NAMESPACE
+import ca.allanwang.kau.utils.integer
+import ca.allanwang.kau.utils.resolveColor
+import ca.allanwang.kau.utils.toColor
+import com.afollestad.materialdialogs.DialogAction
+import com.afollestad.materialdialogs.MaterialDialog
+import com.afollestad.materialdialogs.color.CircleView
+import com.afollestad.materialdialogs.color.ColorChooserDialog
+import com.afollestad.materialdialogs.internal.MDTintHelper
+import com.afollestad.materialdialogs.util.DialogUtils
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+class ColorPickerPreference @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0, defStyleRes: Int = 0
+) : Preference(context, attrs, defStyleAttr, defStyleRes), Preference.OnPreferenceClickListener {
+
+ var defaultColor: Int = Color.BLACK
+ var currentColor: Int
+ var accentMode = false
+ var dialogTitle: Int = 0
+
+ init {
+ onPreferenceClickListener = this
+ if (attrs != null) {
+ val defaultValue = attrs.getAttributeValue(ANDROID_NAMESPACE, "defaultValue")
+ if (defaultValue?.startsWith("#") ?: false) {
+ try {
+ defaultColor = defaultValue.toColor()
+ } catch (e: IllegalArgumentException) {
+ throw IllegalArgumentException("ColorPickerPreference $key has a default value that is not a color resource: $defaultValue")
+ }
+ } else {
+ val resourceId = attrs.getAttributeResourceValue(ANDROID_NAMESPACE, "defaultValue", 0)
+ if (resourceId != 0)
+ defaultColor = context.integer(resourceId)
+ else
+ throw IllegalArgumentException("ColorPickerPreference $key has a default value that is not a color resource: $defaultValue")
+ }
+ }
+ currentColor = getPersistedInt(defaultColor)
+ }
+
+ override fun onPreferenceClick(preference: Preference): Boolean {
+ context.colorPickerDialog {
+
+ }.show()
+ return true
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt
new file mode 100644
index 0000000..add79b9
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPref.kt
@@ -0,0 +1,32 @@
+package ca.allanwang.kau.kpref
+
+import android.content.Context
+import android.content.SharedPreferences
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+open class KPref {
+
+ lateinit private var c: Context
+ lateinit internal var PREFERENCE_NAME: String
+ private var initialized = false
+
+ fun initialize(c: Context, preferenceName: String) {
+ if (initialized) throw KPrefException("KPref object $preferenceName has already been initialized; please only do so once")
+ initialized = true
+ this.c = c.applicationContext
+ PREFERENCE_NAME = preferenceName
+ }
+
+ internal val sp: SharedPreferences by lazy {
+ if (!initialized) throw KPrefException("KPref object has not yet been initialized; please initialize it with a context and preference name")
+ c.getSharedPreferences(PREFERENCE_NAME, Context.MODE_PRIVATE) }
+
+ internal val prefMap: MutableMap<String, KPrefDelegate<*>> = mutableMapOf()
+
+ fun reset() {
+ prefMap.values.forEach { it.invalidate() }
+ }
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
new file mode 100644
index 0000000..e346e77
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefBinder.kt
@@ -0,0 +1,46 @@
+package ca.allanwang.kau.kpref
+
+import android.support.annotation.StringRes
+import android.support.v7.widget.LinearLayoutManager
+import android.support.v7.widget.RecyclerView
+import ca.allanwang.kau.kpref.items.KPrefCheckbox
+import ca.allanwang.kau.kpref.items.KPrefColorPicker
+import ca.allanwang.kau.kpref.items.KPrefHeader
+import ca.allanwang.kau.kpref.items.KPrefItemCore
+import com.mikepenz.fastadapter.commons.adapters.FastItemAdapter
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+fun RecyclerView.setKPrefAdapter(builder: KPrefAdapterBuilder.() -> Unit): FastItemAdapter<KPrefItemCore> {
+ layoutManager = LinearLayoutManager(context)
+ val adapter = FastItemAdapter<KPrefItemCore>()
+ adapter.withOnClickListener { v, _, item, _ -> item.onClick(v) }
+ val items = KPrefAdapterBuilder()
+ builder.invoke(items)
+ adapter.add(items.list)
+ this.adapter = adapter
+ return adapter
+}
+
+class KPrefAdapterBuilder {
+
+ fun header(@StringRes title: Int) = list.add(KPrefHeader(title))
+
+ fun checkbox(@StringRes title: Int,
+ @StringRes description: Int = -1,
+ iicon: IIcon? = null,
+ enabled: Boolean = true,
+ getter: () -> Boolean,
+ setter: (value: Boolean) -> Unit) = list.add(KPrefCheckbox(title, description, iicon, enabled, getter, setter))
+
+ fun colorPicker(@StringRes title: Int,
+ @StringRes description: Int = -1,
+ iicon: IIcon? = null,
+ enabled: Boolean = true,
+ getter: () -> Int,
+ setter: (value: Int) -> Unit)= list.add(KPrefColorPicker(title, description, iicon, enabled, getter, setter))
+
+ internal val list: MutableList<KPrefItemCore> = mutableListOf()
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt
new file mode 100644
index 0000000..f897660
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/KPrefDelegate.kt
@@ -0,0 +1,83 @@
+package ca.allanwang.kau.kpref
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+private object UNINITIALIZED
+
+fun KPref.kpref(key: String, fallback: Boolean): KPrefDelegate<Boolean> = KPrefDelegate(key, fallback, this)
+fun KPref.kpref(key: String, fallback: Double): KPrefDelegate<Float> = KPrefDelegate(key, fallback.toFloat(), this)
+fun KPref.kpref(key: String, fallback: Float): KPrefDelegate<Float> = KPrefDelegate(key, fallback, this)
+fun KPref.kpref(key: String, fallback: Int): KPrefDelegate<Int> = KPrefDelegate(key, fallback, this)
+fun KPref.kpref(key: String, fallback: Long): KPrefDelegate<Long> = KPrefDelegate(key, fallback, this)
+fun KPref.kpref(key: String, fallback: Set<String>): KPrefDelegate<StringSet> = KPrefDelegate(key, StringSet(fallback), this)
+fun KPref.kpref(key: String, fallback: String): KPrefDelegate<String> = KPrefDelegate(key, fallback, this)
+
+class StringSet(set: Collection<String>) : LinkedHashSet<String>(set)
+
+class KPrefDelegate<T : Any> internal constructor(private val key: String, private val fallback: T, private val pref: KPref, lock: Any? = null) : Lazy<T>, java.io.Serializable {
+
+ @Volatile private var _value: Any = UNINITIALIZED
+ private val lock = lock ?: this
+
+ init {
+ if (pref.prefMap.containsKey(key))
+ throw KPrefException("$key is already used elsewhere in preference ${pref.PREFERENCE_NAME}")
+ pref.prefMap.put(key, this@KPrefDelegate)
+ }
+
+ fun invalidate() {
+ _value = UNINITIALIZED
+ }
+
+ override val value: T
+ get() {
+ val _v1 = _value
+ if (_v1 !== UNINITIALIZED)
+ @Suppress("UNCHECKED_CAST")
+ return _v1 as T
+
+ return synchronized(lock) {
+ val _v2 = _value
+ if (_v2 !== UNINITIALIZED) {
+ @Suppress("UNCHECKED_CAST")
+ _v2 as T
+ } else {
+ _value = when (fallback) {
+ is Boolean -> pref.sp.getBoolean(key, fallback)
+ is Float -> pref.sp.getFloat(key, fallback)
+ is Int -> pref.sp.getInt(key, fallback)
+ is Long -> pref.sp.getLong(key, fallback)
+ is StringSet -> pref.sp.getStringSet(key, fallback)
+ is String -> pref.sp.getString(key, fallback)
+ else -> throw KPrefException(fallback)
+ }
+ @Suppress("UNCHECKED_CAST")
+ _value as T
+ }
+ }
+ }
+
+ override fun isInitialized(): Boolean = _value !== UNINITIALIZED
+
+ override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
+
+ operator fun setValue(any: Any, property: kotlin.reflect.KProperty<*>, t: T) {
+ _value = t
+ val editor = pref.sp.edit()
+ when (t) {
+ is Boolean -> editor.putBoolean(key, t)
+ is Float -> editor.putFloat(key, t)
+ is Int -> editor.putInt(key, t)
+ is Long -> editor.putLong(key, t)
+ is StringSet -> editor.putStringSet(key, t)
+ is String -> editor.putString(key, t)
+ else -> throw KPrefException(t)
+ }
+ editor.apply()
+ }
+}
+
+class KPrefException(message: String) : IllegalAccessException(message) {
+ constructor(element: Any?) : this("Invalid type in pref cache: ${element?.javaClass?.simpleName ?: "null"}")
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt
new file mode 100644
index 0000000..3c87571
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefCheckbox.kt
@@ -0,0 +1,34 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.StringRes
+import android.view.View
+import android.widget.CheckBox
+import ca.allanwang.kau.R
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+class KPrefCheckbox(@StringRes title: Int,
+ @StringRes description: Int = -1,
+ iicon: IIcon? = null,
+ enabled: Boolean = true,
+ getter: () -> Boolean,
+ setter: (value: Boolean) -> Unit) : KPrefItemBase<Boolean>(title, description, iicon, enabled, getter, setter) {
+
+ override fun onPostBindView(viewHolder: KPrefItemCore.ViewHolder) {
+ super.onPostBindView(viewHolder)
+ viewHolder.addInnerView(R.layout.kau_preference_checkbox)
+ (viewHolder[R.id.kau_pref_checkbox] as CheckBox).isChecked = pref
+ }
+
+ override fun onClick(itemView: View): Boolean {
+ val checkbox = itemView.findViewById(R.id.kau_pref_checkbox) as CheckBox
+ pref = !pref
+ checkbox.isChecked = pref
+ return true
+ }
+
+ override fun getType(): Int = R.id.kau_item_pref_checkbox
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt
new file mode 100644
index 0000000..cca35b0
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefColorPicker.kt
@@ -0,0 +1,35 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.StringRes
+import android.view.View
+import ca.allanwang.kau.R
+import ca.allanwang.kau.dialogs.color.colorPickerDialog
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+class KPrefColorPicker(@StringRes title: Int,
+ @StringRes description: Int = -1,
+ iicon: IIcon? = null,
+ enabled: Boolean = true,
+ getter: () -> Int,
+ setter: (value: Int) -> Unit) : KPrefItemBase<Int>(title, description, iicon, enabled, getter, setter) {
+
+ override fun onPostBindView(viewHolder: KPrefItemCore.ViewHolder) {
+ super.onPostBindView(viewHolder)
+ //TODO add color circle view
+ }
+
+ override fun onClick(itemView: View): Boolean {
+ itemView.context.colorPickerDialog {
+ titleRes = this@KPrefColorPicker.title
+ defaultColor = pref
+ colorCallbacks.add { pref = it }
+ }.show()
+ return true
+ }
+
+ override fun getType(): Int = R.id.kau_item_pref_color_picker
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt
new file mode 100644
index 0000000..9c22469
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefHeader.kt
@@ -0,0 +1,22 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.StringRes
+import android.view.View
+import ca.allanwang.kau.R
+
+/**
+ * Created by Allan Wang on 2017-06-07.
+ */
+class KPrefHeader(@StringRes title: Int) : KPrefItemCore(title = title) {
+
+ override fun getLayoutRes(): Int = R.layout.kau_preference_header
+
+ override fun onPostBindView(viewHolder: ViewHolder) {
+ viewHolder.itemView.isClickable = false
+ }
+
+ override fun onClick(itemView: View): Boolean = true
+
+ override fun getType() = R.id.kau_item_pref_header
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt
new file mode 100644
index 0000000..c86f3b6
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemBase.kt
@@ -0,0 +1,35 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.CallSuper
+import android.support.annotation.StringRes
+import android.util.Log
+import ca.allanwang.kau.R
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-05.
+ *
+ * Base class for pref setters that include the Shared Preference hooks
+ */
+
+abstract class KPrefItemBase<T>(@StringRes title: Int,
+ @StringRes description: Int = -1,
+ iicon: IIcon? = null,
+ val enabled: Boolean = true,
+ private val getter: () -> T,
+ private val setter: (value: T) -> Unit) : KPrefItemCore(title, description, iicon) {
+
+ var pref: T
+ get() = getter.invoke()
+ set(value) {
+ setter.invoke(value)
+ }
+
+ @CallSuper
+ override fun onPostBindView(viewHolder: ViewHolder) {
+ viewHolder.itemView.isEnabled = enabled
+ viewHolder.itemView.alpha = if (enabled) 1.0f else 0.3f
+ }
+
+ override final fun getLayoutRes(): Int = R.layout.kau_preference
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt
new file mode 100644
index 0000000..8766234
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/kpref/items/KPrefItemCore.kt
@@ -0,0 +1,83 @@
+package ca.allanwang.kau.kpref.items
+
+import android.support.annotation.CallSuper
+import android.support.annotation.IdRes
+import android.support.annotation.LayoutRes
+import android.support.annotation.StringRes
+import android.support.v7.widget.RecyclerView
+import android.view.LayoutInflater
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import butterknife.ButterKnife
+import ca.allanwang.kau.R
+import ca.allanwang.kau.logging.SL
+import ca.allanwang.kau.utils.*
+import com.mikepenz.fastadapter.items.AbstractItem
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-06-05.
+ *
+ * Core class containing nothing but the view items
+ */
+
+abstract class KPrefItemCore(@StringRes val title: Int,
+ @StringRes val description: Int = -1,
+ val iicon: IIcon? = null) : AbstractItem<KPrefItemCore, KPrefItemCore.ViewHolder>() {
+
+ override final fun getViewHolder(v: View) = ViewHolder(v)
+
+ @CallSuper
+ override fun bindView(viewHolder: ViewHolder, payloads: List<Any>) {
+ super.bindView(viewHolder, payloads)
+ with(viewHolder) {
+ val context = itemView.context
+ title.text = context.string(this@KPrefItemCore.title)
+ if (description > 0)
+ desc?.visible()?.setText(description)
+ else
+ desc?.gone()
+ if (iicon != null) {
+ iconFrame?.visible()
+ icon?.setIcon(iicon, 48)
+ } else iconFrame?.gone()
+ onPostBindView(this)
+ }
+ }
+
+ abstract fun onPostBindView(viewHolder: ViewHolder)
+
+ abstract fun onClick(itemView: View): Boolean
+
+ override fun unbindView(holder: ViewHolder) {
+ super.unbindView(holder)
+ with(holder) {
+ title.text = null
+ desc?.text = null
+ icon?.setImageDrawable(null)
+ innerFrame?.removeAllViews()
+ itemView.isEnabled = true
+ itemView.alpha = 1.0f
+ }
+ }
+
+ class ViewHolder(v: View) : RecyclerView.ViewHolder(v) {
+ val title: TextView by bindView(R.id.kau_pref_title)
+ val desc: TextView? by bindOptionalView(R.id.kau_pref_desc)
+ val icon: ImageView? by bindOptionalView(R.id.kau_pref_icon)
+ val iconFrame: LinearLayout? by bindOptionalView(R.id.kau_pref_icon_frame)
+ val innerFrame: LinearLayout? by bindOptionalView(R.id.kau_pref_inner_frame)
+
+ init {
+ ButterKnife.bind(v)
+ }
+
+ fun addInnerView(@LayoutRes id: Int) {
+ LayoutInflater.from(innerFrame!!.context).inflate(id, innerFrame)
+ }
+
+ operator fun get(@IdRes id: Int): View = itemView.findViewById(id)
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/logging/SL.kt b/library/src/main/kotlin/ca/allanwang/kau/logging/SL.kt
new file mode 100644
index 0000000..5f67e23
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/logging/SL.kt
@@ -0,0 +1,6 @@
+package ca.allanwang.kau.logging
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+object SL: TimberLogger("KAU Sample") \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt b/library/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
new file mode 100644
index 0000000..2bbd4a6
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/logging/TimberLogger.kt
@@ -0,0 +1,15 @@
+package ca.allanwang.kau.logging
+
+import timber.log.Timber
+
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+open class TimberLogger(tag: String) {
+ internal val TAG = "$tag: %s"
+ fun e(s: String) = Timber.e(TAG, s)
+ fun d(s: String) = Timber.d(TAG, s)
+ fun i(s: String) = Timber.i(TAG, s)
+ fun v(s: String) = Timber.v(TAG, s)
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
new file mode 100644
index 0000000..d1e527e
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/AnimUtils.kt
@@ -0,0 +1,125 @@
+package ca.allanwang.kau.utils
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.support.annotation.StringRes
+import android.view.View
+import android.view.ViewAnimationUtils
+import android.view.animation.Animation
+import android.view.animation.AnimationUtils
+import android.view.animation.DecelerateInterpolator
+import android.widget.TextView
+
+/**
+ * Created by Allan Wang on 2017-06-01.
+ *
+ * Animation extension functions for Views
+ */
+fun View.rootCircularReveal(x: Int = 0, y: Int = 0, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ this.addOnLayoutChangeListener(object : View.OnLayoutChangeListener {
+ override fun onLayoutChange(v: View, left: Int, top: Int, right: Int, bottom: Int, oldLeft: Int, oldTop: Int,
+ oldRight: Int, oldBottom: Int) {
+ v.removeOnLayoutChangeListener(this)
+ var x2 = x
+ var y2 = y
+ if (x2 > right) x2 = 0
+ if (y2 > bottom) y2 = 0
+ val radius = Math.hypot(Math.max(x2, right - x2).toDouble(), Math.max(y2, bottom - y2).toDouble()).toInt()
+ val reveal = ViewAnimationUtils.createCircularReveal(v, x2, y2, 0f, radius.toFloat())
+ reveal.interpolator = DecelerateInterpolator(1f)
+ reveal.duration = duration
+ reveal.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator?) {
+ visible()
+ onStart?.invoke()
+ }
+
+ override fun onAnimationEnd(animation: Animator?) = onFinish?.invoke() ?: Unit
+ override fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ reveal.start()
+ }
+ })
+}
+
+fun View.circularReveal(x: Int = 0, y: Int = 0, offset: Long = 0L, radius: Float = -1.0f, duration: Long = 500L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ visible()
+ onFinish?.invoke()
+ return
+ }
+ var r = radius
+ if (r < 0.0f) {
+ r = Math.max(Math.hypot(x.toDouble(), y.toDouble()), Math.hypot((width - x.toDouble()), (height - y.toDouble()))).toFloat()
+ }
+ val anim = ViewAnimationUtils.createCircularReveal(this, x, y, 0f, r).setDuration(duration)
+ anim.startDelay = offset
+ anim.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animation: Animator?) {
+ visible()
+ onStart?.invoke()
+ }
+
+ override fun onAnimationEnd(animation: Animator?) = onFinish?.invoke() ?: Unit
+ override fun onAnimationCancel(animation: Animator?) = onFinish?.invoke() ?: Unit
+ })
+ anim.start()
+}
+
+fun View.fadeIn(offset: Long = 0L, duration: Long = 200L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ visible()
+ onFinish?.invoke()
+ return
+ }
+ if (isAttachedToWindow) {
+ val anim = AnimationUtils.loadAnimation(context, android.R.anim.fade_in)
+ anim.startOffset = offset
+ anim.duration = duration
+ anim.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationRepeat(animation: Animation?) {}
+ override fun onAnimationEnd(animation: Animation?) = onFinish?.invoke() ?: Unit
+ override fun onAnimationStart(animation: Animation?) {
+ visible()
+ onStart?.invoke()
+ }
+ })
+ startAnimation(anim)
+ }
+}
+
+fun View.fadeOut(offset: Long = 0L, duration: Long = 200L, onStart: (() -> Unit)? = null, onFinish: (() -> Unit)? = null) {
+ if (!isAttachedToWindow) {
+ onStart?.invoke()
+ invisible()
+ onFinish?.invoke()
+ return
+ }
+ val anim = AnimationUtils.loadAnimation(context, android.R.anim.fade_out)
+ anim.startOffset = offset
+ anim.duration = duration
+ anim.setAnimationListener(object : Animation.AnimationListener {
+ override fun onAnimationRepeat(animation: Animation?) {}
+ override fun onAnimationEnd(animation: Animation?) {
+ invisible()
+ onFinish?.invoke()
+ }
+
+ override fun onAnimationStart(animation: Animation?) {
+ onStart?.invoke()
+ }
+ })
+ startAnimation(anim)
+}
+
+fun TextView.setTextWithFade(text: String, duration: Long = 200, onFinish: (() -> Unit)? = null) {
+ fadeOut(duration = duration, onFinish = {
+ setText(text)
+ fadeIn(duration = duration, onFinish = onFinish)
+ })
+}
+
+fun TextView.setTextWithFade(@StringRes textId: Int, duration: Long = 200, onFinish: (() -> Unit)? = null) = setTextWithFade(context.getString(textId), duration, onFinish)
+
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
new file mode 100644
index 0000000..8e396c7
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ColorUtils.kt
@@ -0,0 +1,183 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.PorterDuff
+import android.graphics.drawable.Drawable
+import android.os.Build
+import android.support.annotation.ColorInt
+import android.support.annotation.IntRange
+import android.support.v4.content.ContextCompat
+import android.support.v4.graphics.drawable.DrawableCompat
+import android.support.v7.widget.AppCompatEditText
+import android.widget.*
+import com.afollestad.materialdialogs.R
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+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 {
+ 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.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
+ else (Math.abs(Color.red(this) - Color.red(color)) < delta
+ && Math.abs(Color.green(this) - Color.green(color)) < delta
+ && Math.abs(Color.blue(this) - Color.blue(color)) < delta)
+
+
+@ColorInt
+fun Context.getDisabledColor(): Int {
+ val primaryColor = resolveColor(android.R.attr.textColorPrimary)
+ val disabledColor = if (primaryColor.isColorDark()) Color.BLACK else Color.WHITE
+ return disabledColor.adjustAlpha(0.3f)
+}
+
+@ColorInt
+fun Int.adjustAlpha(factor: Float): Int {
+ val alpha = Math.round(Color.alpha(this) * factor)
+ return Color.argb(alpha, Color.red(this), Color.green(this), Color.blue(this))
+}
+
+@Throws(IllegalArgumentException::class)
+fun String.toColor(): Int {
+ val toParse: String
+ if (startsWith("#") && length == 4)
+ toParse = "#${this[1]}${this[1]}${this[2]}${this[2]}${this[3]}${this[3]}"
+ else
+ toParse = this
+ return Color.parseColor(toParse)
+}
+
+//Get ColorStateList
+fun Context.colorStateList(@ColorInt color: Int): ColorStateList {
+ val disabledColor = getDisabledColor()
+ return ColorStateList(arrayOf(intArrayOf(android.R.attr.state_enabled, -android.R.attr.state_checked),
+ intArrayOf(android.R.attr.state_enabled, android.R.attr.state_checked),
+ intArrayOf(-android.R.attr.state_enabled, -android.R.attr.state_checked),
+ intArrayOf(-android.R.attr.state_enabled, android.R.attr.state_checked)),
+ intArrayOf(this.resolveColor(R.attr.colorControlNormal), color, disabledColor, disabledColor))
+}
+
+/*
+ * Tint Helpers
+ * Kotlin tint bindings that start with 'tint' so it doesn't conflict with existing methods
+ * Largely based on MDTintHelper
+ * https://github.com/afollestad/material-dialogs/blob/master/core/src/main/java/com/afollestad/materialdialogs/internal/MDTintHelper.java
+ */
+fun RadioButton.tint(colors: ColorStateList) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ buttonTintList = colors
+ } else {
+ val radioDrawable = context.drawable(R.drawable.abc_btn_radio_material)
+ val d = DrawableCompat.wrap(radioDrawable)
+ DrawableCompat.setTintList(d, colors)
+ buttonDrawable = d
+ }
+}
+
+fun RadioButton.tint(@ColorInt color: Int) = tint(context.colorStateList(color))
+
+fun CheckBox.tint(colors: ColorStateList) {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ buttonTintList = colors
+ } else {
+ val checkDrawable = context.drawable(R.drawable.abc_btn_check_material)
+ val drawable = DrawableCompat.wrap(checkDrawable)
+ DrawableCompat.setTintList(drawable, colors)
+ buttonDrawable = drawable
+ }
+}
+
+fun CheckBox.tint(@ColorInt color: Int) = tint(context.colorStateList(color))
+
+fun SeekBar.tint(@ColorInt color: Int) {
+ val s1 = ColorStateList.valueOf(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ thumbTintList = s1
+ progressTintList = s1
+ } else if (Build.VERSION.SDK_INT > Build.VERSION_CODES.GINGERBREAD_MR1) {
+ val progressDrawable = DrawableCompat.wrap(progressDrawable)
+ this.progressDrawable = progressDrawable
+ DrawableCompat.setTintList(progressDrawable, s1)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+ val thumbDrawable = DrawableCompat.wrap(thumb)
+ DrawableCompat.setTintList(thumbDrawable, s1)
+ thumb = thumbDrawable
+ }
+ } else {
+ val mode: PorterDuff.Mode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1)
+ PorterDuff.Mode.MULTIPLY else PorterDuff.Mode.SRC_IN
+ indeterminateDrawable?.setColorFilter(color, mode)
+ progressDrawable?.setColorFilter(color, mode)
+ }
+}
+
+fun ProgressBar.tint(@ColorInt color: Int, skipIndeterminate: Boolean = false) {
+ val sl = ColorStateList.valueOf(color)
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ progressTintList = sl
+ secondaryProgressTintList = sl
+ if (!skipIndeterminate) indeterminateTintList = sl
+ } else {
+ val mode: PorterDuff.Mode = if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD_MR1)
+ PorterDuff.Mode.MULTIPLY else PorterDuff.Mode.SRC_IN
+ indeterminateDrawable?.setColorFilter(color, mode)
+ progressDrawable?.setColorFilter(color, mode)
+ }
+}
+
+fun Context.textColorStateList(@ColorInt color: Int): ColorStateList {
+ val states = arrayOf(
+ intArrayOf(-android.R.attr.state_enabled),
+ intArrayOf(-android.R.attr.state_pressed, -android.R.attr.state_focused),
+ intArrayOf()
+ )
+ val colors = intArrayOf(
+ resolveColor(R.attr.colorControlNormal),
+ resolveColor(R.attr.colorControlNormal),
+ color
+ )
+ return ColorStateList(states, colors)
+}
+
+fun EditText.tint(@ColorInt color: Int) {
+ val editTextColorStateList = context.textColorStateList(color)
+ if (this is AppCompatEditText) {
+ supportBackgroundTintList = editTextColorStateList
+ } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+ backgroundTintList = editTextColorStateList
+ }
+ tintCursor(color)
+}
+
+fun EditText.tintCursor(@ColorInt color: Int) {
+ try {
+ val fCursorDrawableRes = TextView::class.java.getDeclaredField("mCursorDrawableRes")
+ fCursorDrawableRes.isAccessible = true
+ val mCursorDrawableRes = fCursorDrawableRes.getInt(this)
+ val fEditor = TextView::class.java.getDeclaredField("mEditor")
+ fEditor.isAccessible = true
+ val editor = fEditor.get(this)
+ val clazz = editor.javaClass
+ val fCursorDrawable = clazz.getDeclaredField("mCursorDrawable")
+ fCursorDrawable.isAccessible = true
+ val drawables: Array<Drawable> = Array(2, {
+ val drawable = ContextCompat.getDrawable(context, mCursorDrawableRes)
+ drawable.setColorFilter(color, PorterDuff.Mode.SRC_IN)
+ drawable
+ })
+ fCursorDrawable.set(editor, drawables)
+ } catch (e: Exception) {
+ e.printStackTrace()
+ }
+
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/Const.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/Const.kt
new file mode 100644
index 0000000..944caa4
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/Const.kt
@@ -0,0 +1,6 @@
+package ca.allanwang.kau.utils
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+const val ANDROID_NAMESPACE = "http://schemas.android.com/apk/res/android" \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
new file mode 100644
index 0000000..d27a609
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ContextUtils.kt
@@ -0,0 +1,93 @@
+package ca.allanwang.kau.utils
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.ConnectivityManager
+import android.os.Handler
+import android.support.annotation.*
+import android.support.v4.content.ContextCompat
+import android.util.TypedValue
+import android.widget.Toast
+import ca.allanwang.kau.R
+import com.afollestad.materialdialogs.MaterialDialog
+import com.pitchedapps.frost.utils.ChangelogAdapter
+import com.pitchedapps.frost.utils.parse
+import java.util.*
+
+/**
+ * Created by Allan Wang on 2017-06-03.
+ */
+fun Activity.restart(extras: ((Intent) -> Unit)? = null) {
+ val i = Intent(this, this::class.java)
+ i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+ extras?.invoke(i)
+ startActivity(i)
+ overridePendingTransition(0, 0) //No transitions
+ finish()
+ overridePendingTransition(0, 0)
+}
+
+//Toast helpers
+fun Context.toast(@StringRes id: Int, duration: Int = Toast.LENGTH_LONG) = toast(this.string(id), duration)
+
+fun Context.toast(text: String, duration: Int = Toast.LENGTH_LONG) {
+ Toast.makeText(this, text, duration).show()
+}
+
+//Resource retrievers
+fun Context.string(@StringRes id: Int): String = getString(id)
+
+fun Context.string(@StringRes id: Int, fallback: String?): String? = if (id > 0) string(id) else fallback
+fun Context.string(holder: StringHolder?): String? = holder?.getString(this)
+fun Context.color(@ColorRes id: Int): Int = ContextCompat.getColor(this, id)
+fun Context.integer(@IntegerRes id: Int): Int = resources.getInteger(id)
+fun Context.dimen(@DimenRes id: Int): Float = resources.getDimension(id)
+fun Context.drawable(@DrawableRes id: Int): Drawable = ContextCompat.getDrawable(this, id)
+
+//Attr retrievers
+fun Context.resolveColor(@AttrRes attr: Int, fallback: Int = 0): Int {
+ val a = theme.obtainStyledAttributes(intArrayOf(attr))
+ try {
+ return a.getColor(0, fallback)
+ } finally {
+ a.recycle()
+ }
+}
+
+fun Context.resolveBoolean(@AttrRes attr: Int, fallback: Boolean = false): Boolean {
+ val a = theme.obtainStyledAttributes(intArrayOf(attr))
+ try {
+ return a.getBoolean(0, fallback)
+ } finally {
+ a.recycle()
+ }
+}
+
+fun Context.resolveString(@AttrRes attr: Int, fallback: String = ""): String {
+ val v = TypedValue()
+ return if (theme.resolveAttribute(attr, v, true)) v.string.toString() else fallback
+}
+
+fun Context.showChangelog(@XmlRes xmlRes: Int) {
+ val mHandler = Handler()
+ Thread(Runnable {
+ val items = parse(this, xmlRes)
+ mHandler.post(object : TimerTask() {
+ override fun run() {
+ MaterialDialog.Builder(this@showChangelog)
+ .title(R.string.kau_changelog)
+ .positiveText(R.string.kau_great)
+ .adapter(ChangelogAdapter(items), null)
+ .show()
+ }
+ })
+ }).start()
+}
+
+val isNetworkAvailable = fun Context.(): Boolean {
+ val connectivityManager = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
+ val activeNetworkInfo = connectivityManager.activeNetworkInfo
+ return activeNetworkInfo != null && activeNetworkInfo.isConnectedOrConnecting
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt
new file mode 100644
index 0000000..2ef1232
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/FragmentUtils.kt
@@ -0,0 +1,24 @@
+package ca.allanwang.kau.utils
+
+import android.os.Bundle
+import android.support.v4.app.Fragment
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+
+private fun Fragment.bundle(): Bundle {
+ if (this.arguments == null)
+ this.arguments = Bundle()
+ return this.arguments
+}
+
+fun <T : Fragment> T.putString(key: String, value: String): T {
+ this.bundle().putString(key, value)
+ return this
+}
+
+fun <T : Fragment> T.putInt(key: String, value: Int): T {
+ this.bundle().putInt(key, value)
+ return this
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt
new file mode 100644
index 0000000..d47d133
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/IIconUtils.kt
@@ -0,0 +1,19 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.support.annotation.ColorInt
+import com.mikepenz.iconics.IconicsDrawable
+import com.mikepenz.iconics.typeface.IIcon
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ */
+fun IIcon.toDrawable(c: Context, sizeDp: Int = 24, @ColorInt color: Int = Color.WHITE): Drawable {
+ val state = ColorStateList.valueOf(color)
+ val icon = IconicsDrawable(c).icon(this).sizeDp(sizeDp)
+ icon.setTintList(state)
+ return icon
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt
new file mode 100644
index 0000000..bd68b03
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/Kotterknife.kt
@@ -0,0 +1,137 @@
+package ca.allanwang.kau.utils
+
+/**
+ * Created by Allan Wang on 2017-05-29.
+ *
+ * Courtesy of Jake Wharton
+ *
+ * https://github.com/JakeWharton/kotterknife/blob/master/src/main/kotlin/kotterknife/ButterKnife.kt
+ */
+import android.app.Activity
+import android.app.Dialog
+import android.app.DialogFragment
+import android.app.Fragment
+import android.support.v7.widget.RecyclerView.ViewHolder
+import android.view.View
+import kotlin.properties.ReadOnlyProperty
+import kotlin.reflect.KProperty
+import android.support.v4.app.DialogFragment as SupportDialogFragment
+import android.support.v4.app.Fragment as SupportFragment
+
+public fun <V : View> View.bindView(id: Int)
+ : ReadOnlyProperty<View, V> = required(id, viewFinder)
+public fun <V : View> Activity.bindView(id: Int)
+ : ReadOnlyProperty<Activity, V> = required(id, viewFinder)
+public fun <V : View> Dialog.bindView(id: Int)
+ : ReadOnlyProperty<Dialog, V> = required(id, viewFinder)
+public fun <V : View> DialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V> = required(id, viewFinder)
+public fun <V : View> android.support.v4.app.DialogFragment.bindView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, V> = required(id, viewFinder)
+public fun <V : View> Fragment.bindView(id: Int)
+ : ReadOnlyProperty<Fragment, V> = required(id, viewFinder)
+public fun <V : View> android.support.v4.app.Fragment.bindView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, V> = required(id, viewFinder)
+public fun <V : View> ViewHolder.bindView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V> = required(id, viewFinder)
+
+public fun <V : View> View.bindOptionalView(id: Int)
+ : ReadOnlyProperty<View, V?> = optional(id, viewFinder)
+public fun <V : View> Activity.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Activity, V?> = optional(id, viewFinder)
+public fun <V : View> Dialog.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Dialog, V?> = optional(id, viewFinder)
+public fun <V : View> DialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<DialogFragment, V?> = optional(id, viewFinder)
+public fun <V : View> android.support.v4.app.DialogFragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, V?> = optional(id, viewFinder)
+public fun <V : View> Fragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<Fragment, V?> = optional(id, viewFinder)
+public fun <V : View> android.support.v4.app.Fragment.bindOptionalView(id: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, V?> = optional(id, viewFinder)
+public fun <V : View> ViewHolder.bindOptionalView(id: Int)
+ : ReadOnlyProperty<ViewHolder, V?> = optional(id, viewFinder)
+
+public fun <V : View> View.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = required(ids, viewFinder)
+public fun <V : View> Activity.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = required(ids, viewFinder)
+public fun <V : View> Dialog.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = required(ids, viewFinder)
+public fun <V : View> DialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> android.support.v4.app.DialogFragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> Fragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> android.support.v4.app.Fragment.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, List<V>> = required(ids, viewFinder)
+public fun <V : View> ViewHolder.bindViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = required(ids, viewFinder)
+
+public fun <V : View> View.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<View, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Activity.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Activity, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Dialog.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Dialog, List<V>> = optional(ids, viewFinder)
+public fun <V : View> DialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<DialogFragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> android.support.v4.app.DialogFragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.DialogFragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> Fragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<Fragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> android.support.v4.app.Fragment.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<android.support.v4.app.Fragment, List<V>> = optional(ids, viewFinder)
+public fun <V : View> ViewHolder.bindOptionalViews(vararg ids: Int)
+ : ReadOnlyProperty<ViewHolder, List<V>> = optional(ids, viewFinder)
+
+private val View.viewFinder: View.(Int) -> View?
+ get() = { findViewById(it) }
+private val Activity.viewFinder: Activity.(Int) -> View?
+ get() = { findViewById(it) }
+private val Dialog.viewFinder: Dialog.(Int) -> View?
+ get() = { findViewById(it) }
+private val DialogFragment.viewFinder: DialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val android.support.v4.app.DialogFragment.viewFinder: android.support.v4.app.DialogFragment.(Int) -> View?
+ get() = { dialog.findViewById(it) }
+private val Fragment.viewFinder: Fragment.(Int) -> View?
+ get() = { view.findViewById(it) }
+private val android.support.v4.app.Fragment.viewFinder: android.support.v4.app.Fragment.(Int) -> View?
+ get() = { view!!.findViewById(it) }
+private val ViewHolder.viewFinder: ViewHolder.(Int) -> View?
+ get() = { itemView.findViewById(it) }
+
+private fun viewNotFound(id:Int, desc: KProperty<*>): Nothing =
+ throw IllegalStateException("View ID $id for '${desc.name}' not found.")
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> t.finder(id) as V? ?: viewNotFound(id, desc) }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(id: Int, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> t.finder(id) as V? }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> required(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> ids.map { t.finder(it) as V? ?: viewNotFound(it, desc) } }
+
+@Suppress("UNCHECKED_CAST")
+private fun <T, V : View> optional(ids: IntArray, finder: T.(Int) -> View?)
+ = Lazy { t: T, desc -> ids.map { t.finder(it) as V? }.filterNotNull() }
+
+// Like Kotlin's lazy delegate but the initializer gets the target and metadata passed to it
+private class Lazy<T, V>(private val initializer: (T, KProperty<*>) -> V) : ReadOnlyProperty<T, V> {
+ private object EMPTY
+ private var value: Any? = EMPTY
+
+ override fun getValue(thisRef: T, property: KProperty<*>): V {
+ if (value == EMPTY) {
+ value = initializer(thisRef, property)
+ }
+ @Suppress("UNCHECKED_CAST")
+ return value as V
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/LazyResettable.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/LazyResettable.kt
new file mode 100644
index 0000000..6e7e43e
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/LazyResettable.kt
@@ -0,0 +1,51 @@
+package ca.allanwang.kau.utils
+
+import java.io.Serializable
+import kotlin.reflect.KProperty
+
+/**
+ * Created by Allan Wang on 2017-05-30.
+ *
+ * Lazy delegate that can be invalidated if needed
+ * https://stackoverflow.com/a/37294840/4407321
+ */
+private object UNINITIALIZED
+
+fun <T : Any> lazyResettable(initializer: () -> T): LazyResettable<T> = LazyResettable<T>(initializer)
+
+class LazyResettable<T : Any>(private val initializer: () -> T, lock: Any? = null) : Lazy<T>, Serializable {
+ @Volatile private var _value: Any = UNINITIALIZED
+ private val lock = lock ?: this
+
+ fun invalidate() {
+ _value = UNINITIALIZED
+ }
+
+ override val value: T
+ get() {
+ val _v1 = _value
+ if (_v1 !== UNINITIALIZED)
+ @Suppress("UNCHECKED_CAST")
+ return _v1 as T
+
+ return synchronized(lock) {
+ val _v2 = _value
+ if (_v2 !== UNINITIALIZED) {
+ @Suppress("UNCHECKED_CAST")
+ _v2 as T
+ } else {
+ val typedValue = initializer()
+ _value = typedValue
+ typedValue
+ }
+ }
+ }
+
+ override fun isInitialized(): Boolean = _value !== UNINITIALIZED
+
+ override fun toString(): String = if (isInitialized()) value.toString() else "Lazy value not initialized yet."
+
+ operator fun setValue(any: Any, property: KProperty<*>, t: T) {
+ _value = t
+ }
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt
new file mode 100644
index 0000000..e70a2d1
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/StringHolder.kt
@@ -0,0 +1,22 @@
+package ca.allanwang.kau.utils
+
+import android.content.Context
+import android.support.annotation.StringRes
+
+/**
+ * Created by Allan Wang on 2017-06-08.
+ */
+class StringHolder {
+ var text: String? = null
+ var textRes: Int = 0
+
+ constructor(@StringRes textRes: Int) {
+ this.textRes = textRes
+ }
+
+ constructor(text: String) {
+ this.text = text
+ }
+
+ fun getString(context: Context) = context.string(textRes, text)
+} \ No newline at end of file
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt
new file mode 100644
index 0000000..c7ff9f2
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/Utils.kt
@@ -0,0 +1,11 @@
+package ca.allanwang.kau.utils
+
+import android.content.res.Resources
+
+/**
+ * Created by Allan Wang on 2017-05-28.
+ */
+
+val dpToPx = fun Int.(): Int = (this * Resources.getSystem().displayMetrics.density).toInt()
+
+val pxToDp = fun Int.(): Int = (this / Resources.getSystem().displayMetrics.density).toInt()
diff --git a/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
new file mode 100644
index 0000000..a5204aa
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/utils/ViewUtils.kt
@@ -0,0 +1,73 @@
+package ca.allanwang.kau.utils
+
+import android.content.res.ColorStateList
+import android.graphics.Color
+import android.support.annotation.ColorInt
+import android.support.annotation.ColorRes
+import android.support.annotation.StringRes
+import android.support.design.widget.Snackbar
+import android.support.v4.content.ContextCompat
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.ProgressBar
+import android.widget.TextView
+import com.mikepenz.iconics.typeface.IIcon
+
+
+/**
+ * Created by Allan Wang on 2017-05-31.
+ */
+fun <T : View> T.visible(): T {
+ visibility = View.VISIBLE
+ return this
+}
+
+fun <T : View> T.invisible(): T {
+ visibility = View.INVISIBLE
+ return this
+}
+
+fun <T : View> T.gone(): T {
+ visibility = View.GONE
+ return this
+}
+
+fun View.isVisible(): Boolean = visibility == View.VISIBLE
+fun View.isInvisible(): Boolean = visibility == View.INVISIBLE
+fun View.isGone(): Boolean = visibility == View.GONE
+
+fun View.matchParent() {
+ with(layoutParams) {
+ height = ViewGroup.LayoutParams.MATCH_PARENT
+ width = ViewGroup.LayoutParams.MATCH_PARENT
+ }
+}
+
+fun ProgressBar.tintRes(@ColorRes id: Int) = tint(ContextCompat.getColor(context, id))
+
+fun ProgressBar.tint(@ColorInt color: Int) {
+ val sl = ColorStateList.valueOf(color)
+ progressTintList = sl
+ secondaryProgressTintList = sl
+ indeterminateTintList = sl
+}
+
+fun View.snackbar(text: String, duration: Int = Snackbar.LENGTH_LONG, builder: (Snackbar) -> Unit = {}) {
+ val snackbar = Snackbar.make(this, text, duration)
+ builder.invoke(snackbar)
+ snackbar.show()
+}
+
+fun View.snackbar(@StringRes textId: Int, duration: Int = Snackbar.LENGTH_LONG, builder: (Snackbar) -> Unit = {})
+ = snackbar(context.string(textId), duration, builder)
+
+fun TextView.setTextIfValid(@StringRes id: Int) {
+ if (id > 0) text = context.string(id)
+}
+
+fun ImageView.setIcon(icon: IIcon?, sizeDp: Int = 24, @ColorInt color: Int = Color.WHITE) {
+ if (icon == null) return
+ setImageDrawable(icon.toDrawable(context, sizeDp, color))
+}
+
diff --git a/library/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt b/library/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt
new file mode 100644
index 0000000..2c44197
--- /dev/null
+++ b/library/src/main/kotlin/ca/allanwang/kau/views/RippleCanvas.kt
@@ -0,0 +1,79 @@
+package ca.allanwang.kau.views
+
+import android.animation.ValueAnimator
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+
+/**
+ * Created by Allan Wang on 2016-11-17.
+ *
+ *
+ * Canvas drawn ripples that keep the previous color
+ * Extends to view dimensions
+ * Supports multiple ripples from varying locations
+ */
+class RippleCanvas @JvmOverloads constructor(
+ context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0
+) : View(context, attrs, defStyleAttr) {
+ private val paint: Paint = Paint()
+ private var baseColor = Color.TRANSPARENT
+ private val ripples: MutableList<Ripple> = mutableListOf()
+
+ init {
+ paint.isAntiAlias = true
+ paint.style = Paint.Style.FILL
+ }
+
+ override fun onDraw(canvas: Canvas) {
+ canvas.drawColor(baseColor)
+ val itr = ripples.iterator()
+ while (itr.hasNext()) {
+ val r = itr.next()
+ paint.color = r.color
+ canvas.drawCircle(r.x, r.y, r.radius, paint)
+ if (r.radius == r.maxRadius) {
+ itr.remove()
+ baseColor = r.color
+ }
+ }
+ }
+
+ fun ripple(color: Int, startX: Float = 0f, startY: Float = 0f, duration: Int = 1000) {
+ var x = startX
+ var y = startY
+ val w = width.toFloat()
+ val h = height.toFloat()
+ if (x == MIDDLE)
+ x = w / 2
+ else if (x > w) x = 0f
+ if (y == MIDDLE)
+ y = h / 2
+ else if (y > h) y = 0f
+ val maxRadius = Math.hypot(Math.max(x, w - x).toDouble(), Math.max(y, h - y).toDouble()).toFloat()
+ val ripple = Ripple(color, x, y, 0f, maxRadius)
+ ripples.add(ripple)
+ val animator = ValueAnimator.ofFloat(0f, maxRadius)
+ animator.duration = duration.toLong()
+ animator.addUpdateListener { animation ->
+ ripple.radius = animation.animatedValue as Float
+ invalidate()
+ }
+ animator.start()
+ }
+
+ fun set(color: Int) {
+ baseColor = color
+ ripples.clear()
+ invalidate()
+ }
+
+ internal class Ripple(val color: Int, val x: Float, val y: Float, var radius: Float, val maxRadius: Float)
+
+ companion object {
+ const val MIDDLE = -1.0f
+ }
+}