colorpickerのkotlin化など

This commit is contained in:
tateisu 2021-11-20 21:16:56 +09:00
parent c1b631621f
commit 5ed8383f43
44 changed files with 2650 additions and 2921 deletions

4
.gitignore vendored
View File

@ -91,12 +91,8 @@ lint/tmp/
*.hprof
#####################################
*.apk
*.iml
*.log
.DS_Store
.externalNativeBuild
.gradle
/.idea/assetWizardSettings.xml

View File

@ -0,0 +1,29 @@
package jp.juggler.subwaytooter
import android.graphics.Color
import androidx.test.runner.AndroidJUnit4
import com.jrummyapps.android.colorpicker.parseColorString
import org.junit.Assert.assertEquals
import org.junit.Test
import org.junit.runner.RunWith
@RunWith(AndroidJUnit4::class)
class TestColorString {
@Test
fun testColorString() {
fun a(s: String, expect: Int) {
assertEquals(s, expect, parseColorString(s))
assertEquals("#$s", expect, parseColorString("#$s"))
}
a("", Color.BLACK)
a("8", Color.BLACK or 0x00_00_88)
a("56", Color.BLACK or 0x00_00_56)
a("123", Color.BLACK or 0x11_22_33)
a("1234", 0x11_22_33_44)
a("12345", Color.BLACK or 0x12_34_55)
a("123456", Color.BLACK or 0x12_34_56)
a("1234567", 0x12_34_56_77)
a("12345678", 0x12_34_56_78)
a("123456789", Color.WHITE)
}
}

View File

@ -6,6 +6,8 @@ import android.database.sqlite.SQLiteOpenHelper
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.runner.AndroidJUnit4
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.global.DB_VERSION
import jp.juggler.subwaytooter.global.TABLE_LIST
import org.junit.Assert.assertNull
import org.junit.Test
import org.junit.runner.RunWith
@ -36,9 +38,9 @@ class TestDatabase {
val helper = MockDbHelper(
context,
dbName,
App1.DB_VERSION,
DB_VERSION,
create = { db ->
App1.tableList.forEach {
TABLE_LIST.forEach {
val ex = try {
it.onDBCreate(db)
null
@ -49,7 +51,7 @@ class TestDatabase {
}
},
upgrade = { db, oldV, newV ->
App1.tableList.forEach {
TABLE_LIST.forEach {
val ex = try {
it.onDBUpgrade(db, oldV, newV)
null

View File

@ -366,6 +366,14 @@
</intent-filter>
</service>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<meta-data
android:name="jp.juggler.subwaytooter.global.GlobalInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>

View File

@ -430,12 +430,12 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
override fun onDialogDismissed(dialogId: Int) {
}
override fun onColorSelected(dialogId: Int, @ColorInt colorSelected: Int) {
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
val colorTarget = this.colorTarget ?: return
val ip: IntPref = colorTarget.pref.cast() ?: error("$colorTarget has no in pref")
val c = when (colorTarget.type) {
SettingType.ColorAlpha -> colorSelected.notZero() ?: 0x01000000
else -> colorSelected or Color.BLACK
SettingType.ColorAlpha -> newColor.notZero() ?: 1
else -> newColor or Color.BLACK
}
pref.edit().put(ip, c).apply()
findItemViewHolder(colorTarget)?.showColor()
@ -1216,7 +1216,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
}
}
fun setWebBrowser(appSettingItem: AppSettingItem, value: String) {
private fun setWebBrowser(appSettingItem: AppSettingItem, value: String) {
val sp: StringPref = appSettingItem.pref.cast()
?: error("${getString(appSettingItem.caption)}: not StringPref")
pref.edit().put(sp, value).apply()
@ -1224,7 +1224,7 @@ class ActAppSetting : AppCompatActivity(), ColorPickerDialogListener, View.OnCli
showWebBrowser(findItemViewHolder(appSettingItem)?.textView1, value)
}
fun showWebBrowser(tv: TextView?, prefValue: String) {
private fun showWebBrowser(tv: TextView?, prefValue: String) {
tv ?: return
val cn = prefValue.cn()
val (label, icon) = CustomShare.getInfo(this, cn)

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Color
import android.net.Uri
import android.os.Bundle
import android.text.Editable
@ -205,25 +206,22 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
}
}
// 0xFF000000 と書きたいがkotlinではこれはlong型定数になってしまう
private val colorFF000000: Int = (0xff shl 24)
override fun onColorSelected(dialogId: Int, @ColorInt colorSelected: Int) {
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
when (dialogId) {
COLOR_DIALOG_ID_HEADER_BACKGROUND -> column.headerBgColor = colorFF000000 or
colorSelected
COLOR_DIALOG_ID_HEADER_FOREGROUND -> column.headerFgColor = colorFF000000 or
colorSelected
COLOR_DIALOG_ID_COLUMN_BACKGROUND -> column.columnBgColor = colorFF000000 or
colorSelected
COLOR_DIALOG_ID_HEADER_BACKGROUND ->
column.headerBgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_ACCT_TEXT -> {
column.acctColor = colorSelected.notZero() ?: 1
}
COLOR_DIALOG_ID_HEADER_FOREGROUND ->
column.headerFgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_CONTENT_TEXT -> {
column.contentColor = colorSelected.notZero() ?: 1
}
COLOR_DIALOG_ID_COLUMN_BACKGROUND ->
column.columnBgColor = Color.BLACK or newColor
COLOR_DIALOG_ID_ACCT_TEXT ->
column.acctColor = newColor.notZero() ?: 1
COLOR_DIALOG_ID_CONTENT_TEXT ->
column.contentColor = newColor.notZero() ?: 1
}
show()
}
@ -236,7 +234,8 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
runApiTask { client ->
try {
val backgroundDir = getBackgroundImageDir(this@ActColumnCustomize)
val file = File(backgroundDir, "${column.columnId}:${System.currentTimeMillis()}")
val file =
File(backgroundDir, "${column.columnId}:${System.currentTimeMillis()}")
val fileUri = Uri.fromFile(file)
client.publishApiProgress("loading image from $uriArg")
@ -319,7 +318,8 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke
sbColumnBackgroundAlpha = findViewById(R.id.sbColumnBackgroundAlpha)
sbColumnBackgroundAlpha.max = PROGRESS_MAX
sbColumnBackgroundAlpha.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
sbColumnBackgroundAlpha.setOnSeekBarChangeListener(object :
SeekBar.OnSeekBarChangeListener {
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}

View File

@ -213,10 +213,10 @@ class ActHighlightWordEdit
override fun onDialogDismissed(dialogId: Int) {}
override fun onColorSelected(dialogId: Int, color: Int) {
override fun onColorSelected(dialogId: Int, newColor: Int) {
when (dialogId) {
COLOR_DIALOG_ID_TEXT -> item.color_fg = color or Color.BLACK
COLOR_DIALOG_ID_BACKGROUND -> item.color_bg = color.notZero() ?: 0x01000000
COLOR_DIALOG_ID_TEXT -> item.color_fg = newColor or Color.BLACK
COLOR_DIALOG_ID_BACKGROUND -> item.color_bg = newColor.notZero() ?: 0x01000000
}
showColor()
}

View File

@ -8,7 +8,10 @@ import android.content.res.Configuration
import android.graphics.Typeface
import android.os.Bundle
import android.os.Handler
import android.view.*
import android.view.KeyEvent
import android.view.View
import android.view.Window
import android.view.WindowManager
import android.widget.HorizontalScrollView
import android.widget.ImageButton
import android.widget.LinearLayout
@ -140,7 +143,7 @@ class ActMain : AppCompatActivity(),
lateinit var handler: Handler
lateinit var appState: AppState
lateinit var sideMenuAdapter: SideMenuAdapter
private lateinit var sideMenuAdapter: SideMenuAdapter
//////////////////////////////////////////////////////////////////
// 読み取り専用のプロパティ

View File

@ -240,10 +240,10 @@ class ActNickname : AppCompatActivity(), View.OnClickListener, ColorPickerDialog
}
}
override fun onColorSelected(dialogId: Int, @ColorInt color: Int) {
override fun onColorSelected(dialogId: Int, @ColorInt newColor: Int) {
when (dialogId) {
1 -> colorFg = -0x1000000 or color
2 -> colorBg = -0x1000000 or color
1 -> colorFg = -0x1000000 or newColor
2 -> colorBg = -0x1000000 or newColor
}
show()
}

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.actmain
import android.annotation.SuppressLint
import android.content.Intent
import android.content.res.ColorStateList
import android.view.View
@ -375,6 +376,7 @@ fun ActMain.scrollToLastColumn() {
)
}
@SuppressLint("NotifyDataSetChanged")
fun ActMain.resizeColumnWidth(views: ActMainTabletViews) {
var columnWMinDp = ActMain.COLUMN_WIDTH_MIN_DP

View File

@ -5,7 +5,6 @@ import android.text.Spannable
import android.text.SpannableStringBuilder
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageButton
import android.widget.ImageView
import android.widget.LinearLayout
@ -15,17 +14,17 @@ import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootReaction
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.column.Column
import jp.juggler.subwaytooter.column.getContentColor
import jp.juggler.subwaytooter.dialog.EmojiPicker
import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.*

View File

@ -3,12 +3,11 @@ package jp.juggler.subwaytooter.columnviewholder
import android.view.View
import android.widget.ImageView
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.actmain.closePopup
import jp.juggler.subwaytooter.column.*
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefI
import jp.juggler.subwaytooter.util.endPadding
import jp.juggler.subwaytooter.util.startPadding
import jp.juggler.subwaytooter.view.ListDivider

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.columnviewholder
import android.annotation.SuppressLint
import android.os.Handler
import android.os.SystemClock
import android.view.ViewGroup
@ -165,6 +166,7 @@ class ItemListAdapter(
}
}
@SuppressLint("NotifyDataSetChanged")
fun notifyChange(
reason: String,
changeList: List<AdapterChange>? = null,

View File

@ -59,11 +59,11 @@ import jp.juggler.util.LogCategory
// 2021/5/11 59=>60 SavedAccountテーブルに項目追加
// 2021/5/23 60=>61 SavedAccountテーブルに項目追加
private const val DB_VERSION = 61
private const val DB_NAME = "app_db"
const val DB_VERSION = 61
const val DB_NAME = "app_db"
private val log = LogCategory("AppDatabase")
val tables = arrayOf(
val TABLE_LIST = arrayOf(
LogData,
SavedAccount,
ClientInfo,
@ -87,13 +87,13 @@ private class DBOpenHelper(context: Context) :
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
override fun onCreate(db: SQLiteDatabase) {
for (ti in tables) {
for (ti in TABLE_LIST) {
ti.onDBCreate(db)
}
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
for (ti in tables) {
for (ti in TABLE_LIST) {
ti.onDBUpgrade(db, oldVersion, newVersion)
}
}

View File

@ -17,7 +17,6 @@ import androidx.appcompat.widget.AppCompatButton
import androidx.core.content.ContextCompat
import com.google.android.flexbox.JustifyContent
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.TimelineItem
import jp.juggler.subwaytooter.api.entity.TootAccountRef

View File

@ -3,7 +3,6 @@ package jp.juggler.subwaytooter.itemviewholder
import android.view.View
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.ActMediaViewer
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.action.*
import jp.juggler.subwaytooter.actmain.nextPosition
import jp.juggler.subwaytooter.api.entity.*

View File

@ -4,11 +4,10 @@ import android.content.Context
import android.text.Spanned
import android.text.style.RelativeSizeSpan
import jp.juggler.subwaytooter.ActMain
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.api.entity.Acct
import jp.juggler.subwaytooter.api.entity.EntityId
import jp.juggler.subwaytooter.api.entity.TootMention
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.HighlightSpan
import jp.juggler.subwaytooter.span.LinkInfo
import jp.juggler.subwaytooter.span.MyClickableSpan

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter.pref
import android.os.Build
import jp.juggler.subwaytooter.pref.impl.BooleanPref
object PrefB {

View File

@ -1,7 +1,6 @@
package jp.juggler.subwaytooter.pref
import android.graphics.Color
import jp.juggler.subwaytooter.drawable.MediaBackgroundDrawable
import jp.juggler.subwaytooter.itemviewholder.AdditionalButtonsPosition
import jp.juggler.subwaytooter.pref.impl.IntPref
@ -115,5 +114,5 @@ object PrefI {
// const val TTCS_WEEKLY = 0
// const val TTCS_DAILY = 1
val ipMediaBackground = IntPref("MediaBackground", 1 )
val ipMediaBackground = IntPref("MediaBackground", 1)
}

View File

@ -8,7 +8,6 @@ import android.text.style.ReplacementSpan
import androidx.annotation.IntRange
import com.caverock.androidsvg.SVG
import jp.juggler.subwaytooter.emoji.UnicodeEmoji
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.util.LogCategory
// 絵文字リソースの種類によって異なるスパンを作る

View File

@ -6,11 +6,10 @@ import android.text.SpannableString
import android.text.SpannableStringBuilder
import android.text.Spanned
import android.text.style.*
import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.mfm.MisskeyMarkdownDecoder
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.span.*
import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.table.HighlightWord
@ -469,7 +468,13 @@ object HTMLDecoder {
else -> false
}
fun canSkipEncode(isBlockParent: Boolean, curr: Node, parent: Node, prev: Node?, next: Node?) = when {
fun canSkipEncode(
isBlockParent: Boolean,
curr: Node,
parent: Node,
prev: Node?,
next: Node?
) = when {
!isBlockParent -> false
curr.tag != TAG_TEXT -> false
curr.text.isNotBlank() -> false
@ -504,7 +509,10 @@ object HTMLDecoder {
) {
sb.append(options.decodeEmoji(title))
return
} else if (cssClass == "emoji" && url != null && alt != null && reNotestockEmojiAlt.matches(alt)) {
} else if (cssClass == "emoji" && url != null && alt != null && reNotestockEmojiAlt.matches(
alt
)
) {
// notestock custom emoji
sb.run {
val start = length
@ -529,7 +537,15 @@ object HTMLDecoder {
sb.append(caption.notEmpty() ?: url)
if (reUrlStart.find(url) != null) {
val span =
MyClickableSpan(LinkInfo(url = url, ac = null, tag = null, caption = caption, mention = null))
MyClickableSpan(
LinkInfo(
url = url,
ac = null,
tag = null,
caption = caption,
mention = null
)
)
sb.setSpan(span, start, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
}
sb.append(" ")
@ -549,7 +565,12 @@ object HTMLDecoder {
val originalFlusher: EncodeSpanEnv.() -> Unit = {
when (tag) {
"s", "strike", "del" -> {
sb.setSpan(StrikethroughSpan(), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StrikethroughSpan(),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"em" -> {
sb.setSpan(
@ -560,7 +581,12 @@ object HTMLDecoder {
)
}
"strong" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"tr" -> {
sb.append("|")
@ -570,37 +596,122 @@ object HTMLDecoder {
// sb_tmpにレンダリングした分は読み捨てる
}
"h1" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(1.8f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(1.8f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"h2" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(1.6f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(1.6f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"h3" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(1.4f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(1.4f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"h4" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(1.2f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(1.2f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"h5" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(1.0f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(1.0f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"h6" -> {
sb.setSpan(StyleSpan(Typeface.BOLD), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(0.8f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
StyleSpan(Typeface.BOLD),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(0.8f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"pre" -> {
sb.setSpan(BackgroundColorSpan(0x40808080), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(RelativeSizeSpan(0.7f), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(fontSpan(Typeface.MONOSPACE), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
BackgroundColorSpan(0x40808080),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
RelativeSizeSpan(0.7f),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
fontSpan(Typeface.MONOSPACE),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"code" -> {
sb.setSpan(BackgroundColorSpan(0x40808080), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(fontSpan(Typeface.MONOSPACE), spanStart, sb.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
sb.setSpan(
BackgroundColorSpan(0x40808080),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
sb.setSpan(
fontSpan(Typeface.MONOSPACE),
spanStart,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
"hr" -> sb.append("----------")
}

View File

@ -29,19 +29,15 @@ class ProgressResponseBody private constructor(
internal val log = LogCategory("ProgressResponseBody")
// please append this for OkHttpClient.Builder#addInterceptor().
// ex) builder.addInterceptor( ProgressResponseBody.makeInterceptor() );
fun makeInterceptor(): Interceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val originalResponse = chain.proceed(chain.request())
fun makeInterceptor(): Interceptor = Interceptor { chain ->
val originalResponse = chain.proceed(chain.request())
val originalBody = originalResponse.body
?: error("makeInterceptor: originalResponse.body() returns null.")
val originalBody = originalResponse.body
?: error("makeInterceptor: originalResponse.body() returns null.")
return originalResponse.newBuilder()
.body(ProgressResponseBody(originalBody))
.build()
}
originalResponse.newBuilder()
.body(ProgressResponseBody(originalBody))
.build()
}
@Throws(IOException::class)

View File

@ -12,7 +12,7 @@ class JsonException : RuntimeException {
private const val CHAR0 = '\u0000'
private val reDecimal = """(?:\A\-0\z)|[.eE]""".toRegex()
private val reDecimal = """(?:\A-0\z)|[.eE]""".toRegex()
// Tests if the value should be tried as a decimal.
// It makes no test if there are actual digits.

View File

@ -92,6 +92,7 @@ class ToastCompat private constructor(
companion object {
@SuppressLint("DiscouragedPrivateApi")
private fun setContextCompat(view: View?, contextCreator: () -> Context) {
if (view != null && Build.VERSION.SDK_INT == 25) {
try {

View File

@ -13,9 +13,9 @@ android {
targetSdkVersion target_sdk_version
minSdkVersion min_sdk_version
vectorDrawables.useSupportLibrary = true
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
lintOptions {
abortOnError false
}
@ -39,8 +39,12 @@ dependencies {
implementation "androidx.appcompat:appcompat:$appcompat_version"
implementation 'com.google.android.flexbox:flexbox:3.0.0'
}
//apply plugin: 'com.getkeepsafe.dexcount'
//apply from: 'https://raw.githubusercontent.com/jaredrummler/android-artifact-push/master/artifactory/publication.gradle'
//apply from: 'https://raw.githubusercontent.com/jaredrummler/android-artifact-push/master/maven/gradle-mvn-push.gradle'
testImplementation "junit:junit:$junit_version"
androidTestImplementation "androidx.test:core:1.4.0"
androidTestImplementation('androidx.test.espresso:espresso-core:3.1.0-alpha4', {
exclude group: 'com.android.support', module: 'support-annotations'
})
}

View File

@ -1,118 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.ColorFilter;
import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
/**
* This drawable will draw a simple white and gray chessboard pattern.
* It's the pattern you will often see as a background behind a partly transparent image in many applications.
*/
class AlphaPatternDrawable extends Drawable {
private final int rectangleSize;
private final Paint paint = new Paint();
private final Paint paintWhite = new Paint();
private final Paint paintGray = new Paint();
private int numRectanglesHorizontal;
private int numRectanglesVertical;
/**
* Bitmap in which the pattern will be cached.
* This is so the pattern will not have to be recreated each time draw() gets called.
* Because recreating the pattern i rather expensive. I will only be recreated if the size changes.
*/
private Bitmap bitmap;
AlphaPatternDrawable(int rectangleSize) {
this.rectangleSize = rectangleSize;
paintWhite.setColor(0xFFFFFFFF);
paintGray.setColor(0xFFCBCBCB);
}
@Override
public void draw(@NonNull Canvas canvas) {
if (bitmap != null && !bitmap.isRecycled()) {
canvas.drawBitmap(bitmap, null, getBounds(), paint);
}
}
@Override
public int getOpacity() {
return PixelFormat.UNKNOWN;
}
@Override
public void setAlpha(int alpha) {
throw new UnsupportedOperationException("Alpha is not supported by this drawable.");
}
@Override
public void setColorFilter(ColorFilter cf) {
throw new UnsupportedOperationException("ColorFilter is not supported by this drawable.");
}
@Override
protected void onBoundsChange(Rect bounds) {
super.onBoundsChange(bounds);
int height = bounds.height();
int width = bounds.width();
numRectanglesHorizontal = (int) Math.ceil(width / (float) rectangleSize);
numRectanglesVertical = (int) Math.ceil(height / (float) rectangleSize);
generatePatternBitmap();
}
/**
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
*/
private void generatePatternBitmap() {
if (getBounds().width() <= 0 || getBounds().height() <= 0) {
return;
}
bitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888);
Canvas canvas = new Canvas(bitmap);
Rect r = new Rect();
boolean verticalStartWhite = true;
for (int i = 0; i <= numRectanglesVertical; i++) {
boolean isWhite = verticalStartWhite;
for (int j = 0; j <= numRectanglesHorizontal; j++) {
r.top = i * rectangleSize;
r.left = j * rectangleSize;
r.bottom = r.top + rectangleSize;
r.right = r.left + rectangleSize;
canvas.drawRect(r, isWhite ? paintWhite : paintGray);
isWhite = !isWhite;
}
verticalStartWhite = !verticalStartWhite;
}
}
}

View File

@ -0,0 +1,95 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.graphics.*
import android.graphics.drawable.Drawable
import kotlin.math.ceil
/**
* This drawable will draw a simple white and gray chessboard pattern.
* It's the pattern you will often see as a background behind a partly transparent image in many applications.
*/
internal class AlphaPatternDrawable(private val rectangleSize: Int) : Drawable() {
private val paint = Paint()
private val paintWhite = Paint().apply { color = Color.WHITE }
private val paintGray = Paint().apply { color = -0x343434 }
private var numRectanglesHorizontal = 0
private var numRectanglesVertical = 0
/**
* Bitmap in which the pattern will be cached.
* This is so the pattern will not have to be recreated each time draw() gets called.
* Because recreating the pattern i rather expensive. I will only be recreated if the size changes.
*/
private var bitmap: Bitmap? = null
/**
* This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on.
* We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few milliseconds
*/
private fun generatePatternBitmap() {
if (bounds.width() <= 0 || bounds.height() <= 0) {
return
}
val bitmap = Bitmap.createBitmap(bounds.width(), bounds.height(), Bitmap.Config.ARGB_8888)
.also { this.bitmap = it }
val canvas = Canvas(bitmap)
val r = Rect()
var verticalStartWhite = true
for (i in 0..numRectanglesVertical) {
var isWhite = verticalStartWhite
for (j in 0..numRectanglesHorizontal) {
r.top = i * rectangleSize
r.left = j * rectangleSize
r.bottom = r.top + rectangleSize
r.right = r.left + rectangleSize
canvas.drawRect(r, if (isWhite) paintWhite else paintGray)
isWhite = !isWhite
}
verticalStartWhite = !verticalStartWhite
}
}
override fun draw(canvas: Canvas) {
val bitmap = this.bitmap
if (bitmap != null && !bitmap.isRecycled) {
canvas.drawBitmap(bitmap, null, bounds, paint)
}
}
override fun getOpacity(): Int {
return PixelFormat.UNKNOWN
}
override fun setAlpha(alpha: Int) {
throw UnsupportedOperationException("Alpha is not supported by this drawable.")
}
override fun setColorFilter(cf: ColorFilter?) {
throw UnsupportedOperationException("ColorFilter is not supported by this drawable.")
}
override fun onBoundsChange(bounds: Rect) {
super.onBoundsChange(bounds)
val height = bounds.height()
val width = bounds.width()
numRectanglesHorizontal = ceil((width / rectangleSize.toFloat()).toDouble()).toInt()
numRectanglesVertical = ceil((height / rectangleSize.toFloat()).toDouble()).toInt()
generatePatternBitmap()
}
}

View File

@ -1,148 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.content.Context;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ImageView;
import androidx.core.graphics.ColorUtils;
class ColorPaletteAdapter extends BaseAdapter {
/*package*/ final OnColorSelectedListener listener;
/*package*/ final int[] colors;
/*package*/ int selectedPosition;
/*package*/ final int colorShape;
ColorPaletteAdapter(OnColorSelectedListener listener,
int[] colors,
int selectedPosition,
@ColorShape int colorShape) {
this.listener = listener;
this.colors = colors;
this.selectedPosition = selectedPosition;
this.colorShape = colorShape;
}
@Override
public int getCount() {
return colors.length;
}
@Override
public Object getItem(int position) {
return colors[position];
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
final ViewHolder holder;
if (convertView == null) {
holder = new ViewHolder(parent.getContext());
convertView = holder.view;
} else {
holder = (ViewHolder) convertView.getTag();
}
holder.setup(position);
return convertView;
}
void selectNone() {
selectedPosition = -1;
notifyDataSetChanged();
}
interface OnColorSelectedListener {
void onColorSelected(int color);
}
private final class ViewHolder {
final View view;
final ColorPanelView colorPanelView;
final ImageView imageView;
final int originalBorderColor;
ViewHolder(Context context) {
int layoutResId;
if (colorShape == ColorShape.SQUARE) {
layoutResId = R.layout.cpv_color_item_square;
} else {
layoutResId = R.layout.cpv_color_item_circle;
}
view = View.inflate(context, layoutResId, null);
colorPanelView = view.findViewById(R.id.cpv_color_panel_view);
imageView = view.findViewById(R.id.cpv_color_image_view);
originalBorderColor = colorPanelView.getBorderColor();
view.setTag(this);
}
void setup(int position) {
int color = colors[position];
int alpha = Color.alpha(color);
colorPanelView.setColor(color);
imageView.setImageResource(selectedPosition == position ? R.drawable.cpv_preset_checked : 0);
if (alpha != 255) {
if (alpha <= ColorPickerDialog.ALPHA_THRESHOLD) {
colorPanelView.setBorderColor(color | 0xFF000000);
imageView.setColorFilter(/*color | 0xFF000000*/Color.BLACK, PorterDuff.Mode.SRC_IN);
} else {
colorPanelView.setBorderColor(originalBorderColor);
imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
}
} else {
setColorFilter(position);
}
setOnClickListener(position);
}
private void setOnClickListener(final int position) {
colorPanelView.setOnClickListener(v -> {
if (selectedPosition != position) {
selectedPosition = position;
notifyDataSetChanged();
}
listener.onColorSelected(colors[position]);
});
colorPanelView.setOnLongClickListener(v -> {
colorPanelView.showHint();
return true;
});
}
private void setColorFilter(int position) {
if (position == selectedPosition && ColorUtils.calculateLuminance(colors[position]) >= 0.65) {
imageView.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN);
} else {
imageView.setColorFilter(null);
}
}
}
}

View File

@ -0,0 +1,109 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.graphics.Color
import android.graphics.PorterDuff
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.ImageView
import androidx.core.graphics.ColorUtils
internal class ColorPaletteAdapter(
val colors: IntArray,
var selectedPosition: Int,
@ColorShape val colorShape: Int,
val listener: (Int)->Unit
) : BaseAdapter() {
override fun getCount(): Int = colors.size
override fun getItem(position: Int): Any = colors[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup) =
(convertView?.tag as? ViewHolder ?: ViewHolder(parent))
.apply { bind(position) }
.root
fun selectNone() {
selectedPosition = -1
notifyDataSetChanged()
}
private inner class ViewHolder(parent: ViewGroup) {
val root: View = run {
LayoutInflater.from(parent.context).inflate(
if (colorShape == ColorShape.SQUARE) {
R.layout.cpv_color_item_square
} else {
R.layout.cpv_color_item_circle
},
parent,
false
).apply { tag = this@ViewHolder }
}
val colorPanelView: ColorPanelView = root.findViewById(R.id.cpv_color_panel_view)
val imageView: ImageView = root.findViewById(R.id.cpv_color_image_view)
val originalBorderColor: Int = colorPanelView.borderColor
fun bind(position: Int) {
val color = colors[position]
val alpha = Color.alpha(color)
colorPanelView.color = color
imageView.setImageResource(
if (selectedPosition == position)
R.drawable.cpv_preset_checked
else
0
)
when {
alpha == 255 -> {
if (position == selectedPosition && ColorUtils.calculateLuminance(colors[position]) >= 0.65) {
imageView.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
} else {
imageView.colorFilter = null
}
}
alpha <= ColorPickerDialog.ALPHA_THRESHOLD -> {
colorPanelView.borderColor = color or -0x1000000
imageView.setColorFilter( /*color | 0xFF000000*/Color.BLACK,
PorterDuff.Mode.SRC_IN
)
}
else -> {
colorPanelView.borderColor = originalBorderColor
imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
}
}
colorPanelView.setOnClickListener {
if (selectedPosition != position) {
selectedPosition = position
notifyDataSetChanged()
}
listener(colors[position])
}
colorPanelView.setOnLongClickListener {
colorPanelView.showHint()
true
}
}
}
}

View File

@ -1,324 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.BitmapShader;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.os.Parcelable;
import androidx.annotation.ColorInt;
import androidx.core.view.GravityCompat;
import androidx.core.view.ViewCompat;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.Gravity;
import android.view.View;
import android.widget.Toast;
import java.util.Locale;
/**
* This class draws a panel which which will be filled with a color which can be set. It can be used to show the
* currently selected color which you will get from the {@link ColorPickerView}.
*/
public class ColorPanelView extends View {
private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E;
private Drawable alphaPattern;
private Paint borderPaint;
private Paint colorPaint;
private Paint alphaPaint;
private Paint originalPaint;
private Rect drawingRect;
private Rect colorRect;
private RectF centerRect = new RectF();
private boolean showOldColor;
/* The width in pixels of the border surrounding the color panel. */
private int borderWidthPx;
private int borderColor = DEFAULT_BORDER_COLOR;
private int color = Color.BLACK;
private int shape;
public ColorPanelView(Context context) {
this(context, null);
}
public ColorPanelView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ColorPanelView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
@Override public Parcelable onSaveInstanceState() {
Bundle state = new Bundle();
state.putParcelable("instanceState", super.onSaveInstanceState());
state.putInt("color", color);
return state;
}
@Override public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
color = bundle.getInt("color");
state = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(state);
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView);
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE);
showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false);
if (showOldColor && shape != ColorShape.CIRCLE) {
throw new IllegalStateException("Color preview is only available in circle mode");
}
borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR);
a.recycle();
if (borderColor == DEFAULT_BORDER_COLOR) {
// If no specific border color has been set we take the default secondary text color as border/slider color.
// Thus it will adopt to theme changes automatically.
final TypedValue value = new TypedValue();
TypedArray typedArray = context.obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorSecondary});
borderColor = typedArray.getColor(0, borderColor);
typedArray.recycle();
}
borderWidthPx = DrawingUtils.dpToPx(context, 1);
borderPaint = new Paint();
borderPaint.setAntiAlias(true);
colorPaint = new Paint();
colorPaint.setAntiAlias(true);
if (showOldColor) {
originalPaint = new Paint();
}
if (shape == ColorShape.CIRCLE) {
Bitmap bitmap = ((BitmapDrawable) context.getResources().getDrawable(R.drawable.cpv_alpha)).getBitmap();
alphaPaint = new Paint();
alphaPaint.setAntiAlias(true);
BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT);
alphaPaint.setShader(shader);
}
}
@Override protected void onDraw(Canvas canvas) {
borderPaint.setColor(borderColor);
colorPaint.setColor(color);
if (shape == ColorShape.SQUARE) {
if (borderWidthPx > 0) {
canvas.drawRect(drawingRect, borderPaint);
}
if (alphaPattern != null) {
alphaPattern.draw(canvas);
}
canvas.drawRect(colorRect, colorPaint);
} else if (shape == ColorShape.CIRCLE) {
final int outerRadius = getMeasuredWidth() / 2;
if (borderWidthPx > 0) {
canvas.drawCircle(getMeasuredWidth() / 2f,
getMeasuredHeight() / 2f,
outerRadius,
borderPaint);
}
if (Color.alpha(color) < 255) {
canvas.drawCircle(getMeasuredWidth() / 2f,
getMeasuredHeight() / 2f,
outerRadius - borderWidthPx, alphaPaint);
}
if (showOldColor) {
canvas.drawArc(centerRect, 90, 180, true, originalPaint);
canvas.drawArc(centerRect, 270, 180, true, colorPaint);
} else {
canvas.drawCircle(getMeasuredWidth() / 2f,
getMeasuredHeight() / 2f,
outerRadius - borderWidthPx,
colorPaint);
}
}
}
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (shape == ColorShape.SQUARE) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = MeasureSpec.getSize(heightMeasureSpec);
setMeasuredDimension(width, height);
} else if (shape == ColorShape.CIRCLE) {
//noinspection SuspiciousNameCombination
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth());
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
@Override protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (shape == ColorShape.SQUARE || showOldColor) {
drawingRect = new Rect();
drawingRect.left = getPaddingLeft();
drawingRect.right = w - getPaddingRight();
drawingRect.top = getPaddingTop();
drawingRect.bottom = h - getPaddingBottom();
if (showOldColor) {
setUpCenterRect();
} else {
setUpColorRect();
}
}
}
private void setUpCenterRect() {
final Rect dRect = drawingRect;
int left = dRect.left + borderWidthPx;
int top = dRect.top + borderWidthPx;
int bottom = dRect.bottom - borderWidthPx;
int right = dRect.right - borderWidthPx;
centerRect = new RectF(left, top, right, bottom);
}
private void setUpColorRect() {
final Rect dRect = drawingRect;
int left = dRect.left + borderWidthPx;
int top = dRect.top + borderWidthPx;
int bottom = dRect.bottom - borderWidthPx;
int right = dRect.right - borderWidthPx;
colorRect = new Rect(left, top, right, bottom);
alphaPattern = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4));
alphaPattern.setBounds(Math.round(colorRect.left),
Math.round(colorRect.top),
Math.round(colorRect.right),
Math.round(colorRect.bottom));
}
/**
* Set the color that should be shown by this view.
*
* @param color
* the color value
*/
public void setColor(int color) {
this.color = color;
invalidate();
}
/**
* Get the color currently show by this view.
*
* @return the color value
*/
public int getColor() {
return color;
}
/**
* Set the original color. This is only used for previewing colors.
*
* @param color
* The original color
*/
public void setOriginalColor(@ColorInt int color) {
if (originalPaint != null) {
originalPaint.setColor(color);
}
}
/**
* Set the color of the border surrounding the panel.
*
* @param color
* the color value
*/
public void setBorderColor(int color) {
borderColor = color;
invalidate();
}
/**
* @return the color of the border surrounding the panel.
*/
public int getBorderColor() {
return borderColor;
}
/**
* Set the shape.
*
* @param shape
* Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}.
*/
public void setShape(@ColorShape int shape) {
this.shape = shape;
invalidate();
}
/**
* Get the shape
*
* @return Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}.
*/
@ColorShape public int getShape() {
return shape;
}
/**
* Show a toast message with the hex color code below the view.
*/
public void showHint() {
final int[] screenPos = new int[2];
final Rect displayFrame = new Rect();
getLocationOnScreen(screenPos);
getWindowVisibleDisplayFrame(displayFrame);
final Context context = getContext();
final int width = getWidth();
final int height = getHeight();
final int midy = screenPos[1] + height / 2;
int referenceX = screenPos[0] + width / 2;
if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) {
final int screenWidth = context.getResources().getDisplayMetrics().widthPixels;
referenceX = screenWidth - referenceX; // mirror
}
StringBuilder hint = new StringBuilder("#");
if (Color.alpha(color) != 255) {
hint.append(Integer.toHexString(color).toUpperCase(Locale.ENGLISH));
} else {
hint.append(String.format("%06X", 0xFFFFFF & color).toUpperCase(Locale.ENGLISH));
}
Toast cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT);
if (midy < displayFrame.height()) {
// Show along the top; follow action buttons
cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX,
screenPos[1] + height - displayFrame.top);
} else {
// Show along the bottom center
cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height);
}
cheatSheet.show();
}
}

View File

@ -0,0 +1,286 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.graphics.*
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.Gravity
import android.view.View
import android.widget.Toast
import androidx.annotation.ColorInt
import androidx.core.content.ContextCompat
import androidx.core.view.GravityCompat
import androidx.core.view.ViewCompat
/**
* This class draws a panel which which will be filled with a color which can be set. It can be used to show the
* currently selected color which you will get from the [ColorPickerView].
*/
class ColorPanelView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle) {
companion object {
private const val DEFAULT_BORDER_COLOR = -0x919192
}
/* The width in pixels of the border surrounding the color panel. */
private val borderWidthPx = DrawingUtils.dpToPx(context, 1f)
private val borderPaint = Paint().apply {
isAntiAlias = true
}
private val colorPaint = Paint().apply {
isAntiAlias = true
}
private val alphaPaint = Paint().apply {
val bitmap = (ContextCompat.getDrawable(context, R.drawable.cpv_alpha) as BitmapDrawable)
.bitmap
shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
isAntiAlias = true
}
private val originalPaint = Paint()
private val centerRect = RectF()
private var drawingRect = Rect()
private var colorRect = Rect()
private var alphaPattern = AlphaPatternDrawable(DrawingUtils.dpToPx(context, 4f))
private var showOldColor = false
var borderColor = DEFAULT_BORDER_COLOR
set(value) {
if (field != value) {
field = value
invalidate()
}
}
var color = Color.BLACK
set(value) {
if (field != value) {
field = value
invalidate()
}
}
@ColorShape
private var shape = 0
set(value) {
if (field != value) {
field = value
invalidate()
}
}
init {
val a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView)
shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE)
showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false)
check(!(showOldColor && shape != ColorShape.CIRCLE)) { "Color preview is only available in circle mode" }
borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR)
a.recycle()
if (borderColor == DEFAULT_BORDER_COLOR) {
// If no specific border color has been set we take the default secondary text color as border/slider color.
// Thus it will adopt to theme changes automatically.
val value = TypedValue()
val typedArray = context.obtainStyledAttributes(
value.data,
intArrayOf(android.R.attr.textColorSecondary)
)
borderColor = typedArray.getColor(0, borderColor)
typedArray.recycle()
}
}
public override fun onSaveInstanceState(): Parcelable {
val state = Bundle()
state.putParcelable("instanceState", super.onSaveInstanceState())
state.putInt("color", color)
return state
}
public override fun onRestoreInstanceState(state: Parcelable) {
if (state is Bundle) {
color = state.getInt("color")
super.onRestoreInstanceState(state.getParcelable("instanceState"))
} else {
super.onRestoreInstanceState(state)
}
}
override fun onDraw(canvas: Canvas) {
borderPaint.color = borderColor
colorPaint.color = color
if (shape == ColorShape.SQUARE) {
if (borderWidthPx > 0) {
canvas.drawRect(drawingRect, borderPaint)
}
alphaPattern.draw(canvas)
canvas.drawRect(colorRect, colorPaint)
} else if (shape == ColorShape.CIRCLE) {
val outerRadius = measuredWidth / 2
if (borderWidthPx > 0) {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f,
outerRadius.toFloat(),
borderPaint
)
}
if (Color.alpha(color) < 255) {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f, (
outerRadius - borderWidthPx).toFloat(), alphaPaint
)
}
if (showOldColor) {
canvas.drawArc(centerRect, 90f, 180f, true, originalPaint)
canvas.drawArc(centerRect, 270f, 180f, true, colorPaint)
} else {
canvas.drawCircle(
measuredWidth / 2f,
measuredHeight / 2f, (
outerRadius - borderWidthPx).toFloat(),
colorPaint
)
}
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
when (shape) {
ColorShape.SQUARE -> {
val width = MeasureSpec.getSize(widthMeasureSpec)
val height = MeasureSpec.getSize(heightMeasureSpec)
setMeasuredDimension(width, height)
}
ColorShape.CIRCLE -> {
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
setMeasuredDimension(measuredWidth, measuredWidth)
}
else -> {
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
}
}
}
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
if (shape == ColorShape.SQUARE || showOldColor) {
drawingRect.set(
paddingLeft,
paddingTop,
w - paddingRight,
h - paddingBottom
)
if (showOldColor) {
setUpCenterRect()
} else {
setUpColorRect()
}
}
}
private fun setUpCenterRect() {
val dRect = drawingRect
val left = dRect.left + borderWidthPx
val top = dRect.top + borderWidthPx
val bottom = dRect.bottom - borderWidthPx
val right = dRect.right - borderWidthPx
centerRect.set(
left.toFloat(),
top.toFloat(),
right.toFloat(),
bottom.toFloat()
)
}
private fun setUpColorRect() {
val left = drawingRect.left + borderWidthPx
val top = drawingRect.top + borderWidthPx
val bottom = drawingRect.bottom - borderWidthPx
val right = drawingRect.right - borderWidthPx
colorRect.set(left, top, right, bottom)
alphaPattern.setBounds(left, top, right, bottom)
}
/**
* Set the original color. This is only used for previewing colors.
*
* @param color
* The original color
*/
fun setOriginalColor(@ColorInt color: Int) {
originalPaint.color = color
}
/**
* Show a toast message with the hex color code below the view.
*/
fun showHint() {
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 hint = StringBuilder("#")
if (Color.alpha(color) != 255) {
hint.append(Integer.toHexString(color).uppercase())
} else {
hint.append(String.format("%06X", 0xFFFFFF and color).uppercase())
}
val cheatSheet = Toast.makeText(context, hint.toString(), 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()
}
}

View File

@ -1,906 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.content.DialogInterface;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.PorterDuff;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.TextWatcher;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.GridView;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import com.jrummyapps.android.colorpicker.ColorPickerView.OnColorChangedListener;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.Arrays;
import java.util.Locale;
import androidx.annotation.ColorInt;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.StringRes;
import androidx.appcompat.app.AlertDialog;
import androidx.core.graphics.ColorUtils;
import androidx.fragment.app.DialogFragment;
import androidx.fragment.app.FragmentActivity;
/**
* <p>A dialog to pick a color.</p>
*
* <p>The {@link Activity activity} that shows this dialog should implement {@link ColorPickerDialogListener}</p>
*
* <p>Example usage:</p>
*
* <pre>
* ColorPickerDialog.newBuilder().show(activity);
* </pre>
*/
public class ColorPickerDialog
extends DialogFragment
implements OnTouchListener, OnColorChangedListener, TextWatcher {
private static final String ARG_ID = "id";
private static final String ARG_TYPE = "dialogType";
private static final String ARG_COLOR = "color";
private static final String ARG_ALPHA = "alpha";
private static final String ARG_PRESETS = "presets";
private static final String ARG_ALLOW_PRESETS = "allowPresets";
private static final String ARG_ALLOW_CUSTOM = "allowCustom";
private static final String ARG_DIALOG_TITLE = "dialogTitle";
private static final String ARG_SHOW_COLOR_SHADES = "showColorShades";
private static final String ARG_COLOR_SHAPE = "colorShape";
public static final int TYPE_CUSTOM = 0;
public static final int TYPE_PRESETS = 1;
static final int ALPHA_THRESHOLD = 165;
/**
* Material design colors used as the default color presets
*/
public static final int[] MATERIAL_COLORS = {
0xFFF44336, // RED 500
0xFFE91E63, // PINK 500
0xFFFF2C93, // LIGHT PINK 500
0xFF9C27B0, // PURPLE 500
0xFF673AB7, // DEEP PURPLE 500
0xFF3F51B5, // INDIGO 500
0xFF2196F3, // BLUE 500
0xFF03A9F4, // LIGHT BLUE 500
0xFF00BCD4, // CYAN 500
0xFF009688, // TEAL 500
0xFF4CAF50, // GREEN 500
0xFF8BC34A, // LIGHT GREEN 500
0xFFCDDC39, // LIME 500
0xFFFFEB3B, // YELLOW 500
0xFFFFC107, // AMBER 500
0xFFFF9800, // ORANGE 500
0xFF795548, // BROWN 500
0xFF607D8B, // BLUE GREY 500
0xFF9E9E9E, // GREY 500
};
/**
* Create a new Builder for creating a {@link ColorPickerDialog} instance
*
* @return The {@link Builder builder} to create the {@link ColorPickerDialog}.
*/
public static Builder newBuilder() {
return new Builder();
}
ColorPickerDialogListener colorPickerDialogListener;
FrameLayout rootView;
int[] presets;
@ColorInt
int color;
int dialogType;
int dialogId;
boolean showColorShades;
int colorShape;
// -- PRESETS --------------------------
ColorPaletteAdapter adapter;
LinearLayout shadesLayout;
SeekBar transparencySeekBar;
TextView transparencyPercText;
// -- CUSTOM ---------------------------
ColorPickerView colorPicker;
ColorPanelView newColorPanel;
EditText hexEditText;
boolean showAlphaSlider;
private boolean fromEditText;
@Override
public void onAttach(@NonNull Context context) {
super.onAttach(context);
if (colorPickerDialogListener == null && context instanceof ColorPickerDialogListener) {
colorPickerDialogListener = (ColorPickerDialogListener) context;
}
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
if (args == null) throw new RuntimeException("onCreateDialog: args is null");
Context context = getContext();
if (context == null) throw new RuntimeException("onCreateDialog: context is null");
Activity activity = getActivity();
if (activity == null) throw new RuntimeException("onCreateDialog: activity is null");
dialogId = args.getInt(ARG_ID);
showAlphaSlider = args.getBoolean(ARG_ALPHA);
showColorShades = args.getBoolean(ARG_SHOW_COLOR_SHADES);
colorShape = args.getInt(ARG_COLOR_SHAPE);
if (savedInstanceState == null) {
color = args.getInt(ARG_COLOR);
dialogType = args.getInt(ARG_TYPE);
} else {
color = savedInstanceState.getInt(ARG_COLOR);
dialogType = savedInstanceState.getInt(ARG_TYPE);
}
rootView = new FrameLayout(activity);
if (dialogType == TYPE_CUSTOM) {
rootView.addView(createPickerView());
} else if (dialogType == TYPE_PRESETS) {
rootView.addView(createPresetsView());
}
AlertDialog.Builder builder = new AlertDialog.Builder(activity)
.setView(rootView)
.setPositiveButton(R.string.cpv_select, (dialog, which) ->
colorPickerDialogListener.onColorSelected(dialogId, color));
int dialogTitleStringRes = args.getInt(ARG_DIALOG_TITLE);
if (dialogTitleStringRes != 0) {
builder.setTitle(dialogTitleStringRes);
}
int neutralButtonStringRes;
if (dialogType == TYPE_CUSTOM && args.getBoolean(ARG_ALLOW_PRESETS)) {
neutralButtonStringRes = R.string.cpv_presets;
} else if (dialogType == TYPE_PRESETS && args.getBoolean(ARG_ALLOW_CUSTOM)) {
neutralButtonStringRes = R.string.cpv_custom;
} else {
neutralButtonStringRes = 0;
}
if (neutralButtonStringRes != 0) {
builder.setNeutralButton(neutralButtonStringRes, null);
}
return builder.create();
}
@Override
public void onStart() {
super.onStart();
AlertDialog dialog = (AlertDialog) getDialog();
// http://stackoverflow.com/a/16972670/1048340
//noinspection ConstantConditions
dialog.getWindow()
.clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE);
// Do not dismiss the dialog when clicking the neutral button.
Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL);
if (neutralButton != null) {
neutralButton.setOnClickListener(v -> {
rootView.removeAllViews();
switch (dialogType) {
case TYPE_CUSTOM:
dialogType = TYPE_PRESETS;
((Button) v).setText(R.string.cpv_custom);
rootView.addView(createPresetsView());
break;
case TYPE_PRESETS:
dialogType = TYPE_CUSTOM;
((Button) v).setText(R.string.cpv_presets);
rootView.addView(createPickerView());
}
});
}
}
@Override
public void onDismiss(@NonNull DialogInterface dialog) {
super.onDismiss(dialog);
colorPickerDialogListener.onDialogDismissed(dialogId);
}
@Override
public void onSaveInstanceState(Bundle outState) {
outState.putInt(ARG_COLOR, color);
outState.putInt(ARG_TYPE, dialogType);
super.onSaveInstanceState(outState);
}
/**
* Set the callback
*
* @param colorPickerDialogListener The callback invoked when a color is selected or the dialog is dismissed.
*/
public void setColorPickerDialogListener(ColorPickerDialogListener colorPickerDialogListener) {
this.colorPickerDialogListener = colorPickerDialogListener;
}
// region Custom Picker
View createPickerView() {
Bundle args = getArguments();
if (args == null) throw new RuntimeException("createPickerView: args is null");
FragmentActivity activity = getActivity();
if (activity == null) throw new RuntimeException("createPickerView: activity is null");
View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null);
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view);
ColorPanelView oldColorPanel = contentView.findViewById(R.id.cpv_color_panel_old);
newColorPanel = contentView.findViewById(R.id.cpv_color_panel_new);
ImageView arrowRight = contentView.findViewById(R.id.cpv_arrow_right);
hexEditText = contentView.findViewById(R.id.cpv_hex);
try {
final TypedValue value = new TypedValue();
TypedArray typedArray = activity.obtainStyledAttributes(
value.data,
new int[]{android.R.attr.textColorPrimary}
);
int arrowColor = typedArray.getColor(0, Color.BLACK);
typedArray.recycle();
arrowRight.setColorFilter(arrowColor);
} catch (Exception ignored) {
}
colorPicker.setAlphaSliderVisible(showAlphaSlider);
oldColorPanel.setColor(args.getInt(ARG_COLOR));
colorPicker.setColor(color, true);
newColorPanel.setColor(color);
setHex(color);
if (!showAlphaSlider) {
hexEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)});
}
newColorPanel.setOnClickListener(v -> {
if (newColorPanel.getColor() == color) {
colorPickerDialogListener.onColorSelected(dialogId, color);
dismiss();
}
});
contentView.setOnTouchListener(this);
colorPicker.setOnColorChangedListener(this);
hexEditText.addTextChangedListener(this);
hexEditText.setOnFocusChangeListener((v, hasFocus) -> {
if (hasFocus) {
InputMethodManager imm = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) imm.showSoftInput(hexEditText, InputMethodManager.SHOW_IMPLICIT);
}
});
return contentView;
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouch(View v, MotionEvent event) {
if (v != hexEditText && hexEditText.hasFocus()) {
hexEditText.clearFocus();
InputMethodManager imm = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0);
hexEditText.clearFocus();
return true;
}
return false;
}
@Override
public void onColorChanged(int newColor) {
color = newColor;
newColorPanel.setColor(newColor);
if (!fromEditText) {
setHex(newColor);
if (hexEditText.hasFocus()) {
InputMethodManager imm = (InputMethodManager) getActivity()
.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm != null) imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0);
hexEditText.clearFocus();
}
}
fromEditText = false;
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
if (hexEditText.isFocused()) {
try {
int color = parseColorString(s.toString());
if (color != colorPicker.getColor()) {
fromEditText = true;
colorPicker.setColor(color, true);
}
} catch (NumberFormatException ex) {
// nothing to do
}
}
}
private void setHex(int color) {
if (showAlphaSlider) {
hexEditText.setText(String.format("%08X", (color)));
} else {
hexEditText.setText(String.format("%06X", (0xFFFFFF & color)));
}
}
private int parseColorString(String colorString) throws NumberFormatException {
int a, r, g, b = 0;
if (colorString.startsWith("#")) {
colorString = colorString.substring(1);
}
if (colorString.length() == 0) {
r = 0;
a = 255;
g = 0;
} else if (colorString.length() <= 2) {
a = 255;
r = 0;
b = Integer.parseInt(colorString, 16);
g = 0;
} else if (colorString.length() == 3) {
a = 255;
r = Integer.parseInt(colorString.substring(0, 1), 16);
g = Integer.parseInt(colorString.substring(1, 2), 16);
b = Integer.parseInt(colorString.substring(2, 3), 16);
} else if (colorString.length() == 4) {
a = 255;
r = Integer.parseInt(colorString.substring(0, 2), 16);
g = r;
r = 0;
b = Integer.parseInt(colorString.substring(2, 4), 16);
} else if (colorString.length() == 5) {
a = 255;
r = Integer.parseInt(colorString.substring(0, 1), 16);
g = Integer.parseInt(colorString.substring(1, 3), 16);
b = Integer.parseInt(colorString.substring(3, 5), 16);
} else if (colorString.length() == 6) {
a = 255;
r = Integer.parseInt(colorString.substring(0, 2), 16);
g = Integer.parseInt(colorString.substring(2, 4), 16);
b = Integer.parseInt(colorString.substring(4, 6), 16);
} else if (colorString.length() == 7) {
a = Integer.parseInt(colorString.substring(0, 1), 16);
r = Integer.parseInt(colorString.substring(1, 3), 16);
g = Integer.parseInt(colorString.substring(3, 5), 16);
b = Integer.parseInt(colorString.substring(5, 7), 16);
} else if (colorString.length() == 8) {
a = Integer.parseInt(colorString.substring(0, 2), 16);
r = Integer.parseInt(colorString.substring(2, 4), 16);
g = Integer.parseInt(colorString.substring(4, 6), 16);
b = Integer.parseInt(colorString.substring(6, 8), 16);
} else {
b = -1;
g = -1;
r = -1;
a = -1;
}
return Color.argb(a, r, g, b);
}
// -- endregion --
// region Presets Picker
View createPresetsView() {
View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_presets, null);
shadesLayout = contentView.findViewById(R.id.shades_layout);
transparencySeekBar = contentView.findViewById(R.id.transparency_seekbar);
transparencyPercText = contentView.findViewById(R.id.transparency_text);
GridView gridView = contentView.findViewById(R.id.gridView);
loadPresets();
if (showColorShades) {
createColorShades(color);
} else {
shadesLayout.setVisibility(View.GONE);
contentView.findViewById(R.id.shades_divider).setVisibility(View.GONE);
}
adapter = new ColorPaletteAdapter(newColor -> {
if (color == newColor) {
colorPickerDialogListener.onColorSelected(dialogId, color);
dismiss();
return;
}
color = newColor;
if (showColorShades) {
createColorShades(color);
}
}, presets, getSelectedItemPosition(), colorShape);
gridView.setAdapter(adapter);
if (showAlphaSlider) {
setupTransparency();
} else {
contentView.findViewById(R.id.transparency_layout).setVisibility(View.GONE);
contentView.findViewById(R.id.transparency_title).setVisibility(View.GONE);
}
return contentView;
}
private void loadPresets() {
int alpha = Color.alpha(color);
presets = getArguments().getIntArray(ARG_PRESETS);
if (presets == null) presets = MATERIAL_COLORS;
boolean isMaterialColors = presets == MATERIAL_COLORS;
presets = Arrays.copyOf(presets, presets.length);
// don't update the original array when modifying alpha
if (alpha != 255) {
// add alpha to the presets
for (int i = 0; i < presets.length; i++) {
int color = presets[i];
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
presets[i] = Color.argb(alpha, red, green, blue);
}
}
presets = unshiftIfNotExists(presets, color);
if (isMaterialColors && presets.length == 19) {
// Add black to have a total of 20 colors if the current color is in the material color palette
presets = pushIfNotExists(presets, Color.argb(alpha, 0, 0, 0));
}
}
void createColorShades(@ColorInt final int color) {
final int[] colorShades = getColorShades(color);
if (shadesLayout.getChildCount() != 0) {
for (int i = 0; i < shadesLayout.getChildCount(); i++) {
FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i);
final ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view);
ImageView iv = layout.findViewById(R.id.cpv_color_image_view);
cpv.setColor(colorShades[i]);
cpv.setTag(false);
iv.setImageDrawable(null);
}
return;
}
final int horizontalPadding = getResources()
.getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding);
for (final int colorShade : colorShades) {
int layoutResId;
if (colorShape == ColorShape.SQUARE) {
layoutResId = R.layout.cpv_color_item_square;
} else {
layoutResId = R.layout.cpv_color_item_circle;
}
final View view = View.inflate(getActivity(), layoutResId, null);
final ColorPanelView colorPanelView = view.findViewById(R.id.cpv_color_panel_view);
ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) colorPanelView
.getLayoutParams();
params.leftMargin = params.rightMargin = horizontalPadding;
colorPanelView.setLayoutParams(params);
colorPanelView.setColor(colorShade);
shadesLayout.addView(view);
colorPanelView.post(() -> {
// The color is black when rotating the dialog. This is a dirty fix. WTF!?
colorPanelView.setColor(colorShade);
});
colorPanelView.setOnClickListener(v -> {
if (v.getTag() instanceof Boolean && (Boolean) v.getTag()) {
colorPickerDialogListener.onColorSelected(dialogId, ColorPickerDialog.this.color);
dismiss();
return; // already selected
}
ColorPickerDialog.this.color = colorPanelView.getColor();
adapter.selectNone();
for (int i = 0; i < shadesLayout.getChildCount(); i++) {
FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i);
ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view);
ImageView iv = layout.findViewById(R.id.cpv_color_image_view);
iv.setImageResource(cpv == v ? R.drawable.cpv_preset_checked : 0);
if (cpv == v && ColorUtils.calculateLuminance(cpv.getColor()) >= 0.65 ||
Color.alpha(cpv.getColor()) <= ALPHA_THRESHOLD) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN);
} else {
iv.setColorFilter(null);
}
cpv.setTag(cpv == v);
}
});
colorPanelView.setOnLongClickListener(v -> {
colorPanelView.showHint();
return true;
});
}
}
private int shadeColor(@ColorInt int color, double percent) {
String hex = String.format("#%06X", (0xFFFFFF & color));
long f = Long.parseLong(hex.substring(1), 16);
double t = percent < 0 ? 0 : 255;
double p = percent < 0 ? percent * -1 : percent;
long R = f >> 16;
long G = f >> 8 & 0x00FF;
long B = f & 0x0000FF;
int alpha = Color.alpha(color);
int red = (int) (Math.round((t - R) * p) + R);
int green = (int) (Math.round((t - G) * p) + G);
int blue = (int) (Math.round((t - B) * p) + B);
return Color.argb(alpha, red, green, blue);
}
private int[] getColorShades(@ColorInt int color) {
return new int[]{
shadeColor(color, 0.9),
shadeColor(color, 0.7),
shadeColor(color, 0.5),
shadeColor(color, 0.333),
shadeColor(color, 0.166),
shadeColor(color, -0.125),
shadeColor(color, -0.25),
shadeColor(color, -0.375),
shadeColor(color, -0.5),
shadeColor(color, -0.675),
shadeColor(color, -0.7),
shadeColor(color, -0.775),
};
}
private void setupTransparency() {
int progress = 255 - Color.alpha(color);
transparencySeekBar.setMax(255);
transparencySeekBar.setProgress(progress);
int percentage = (int) ((double) progress * 100 / 255);
transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage));
transparencySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
int percentage = (int) ((double) progress * 100 / 255);
transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage));
int alpha = 255 - progress;
// update items in GridView:
for (int i = 0; i < adapter.colors.length; i++) {
int color = adapter.colors[i];
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
adapter.colors[i] = Color.argb(alpha, red, green, blue);
}
adapter.notifyDataSetChanged();
// update shades:
for (int i = 0; i < shadesLayout.getChildCount(); i++) {
FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i);
ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view);
ImageView iv = layout.findViewById(R.id.cpv_color_image_view);
if (layout.getTag() == null) {
// save the original border color
layout.setTag(cpv.getBorderColor());
}
int color = cpv.getColor();
color = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color));
if (alpha <= ALPHA_THRESHOLD) {
cpv.setBorderColor(color | 0xFF000000);
} else {
cpv.setBorderColor((int) layout.getTag());
}
if (cpv.getTag() != null && (Boolean) cpv.getTag()) {
// The alpha changed on the selected shaded color. Update the checkmark color filter.
if (alpha <= ALPHA_THRESHOLD) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN);
} else {
if (ColorUtils.calculateLuminance(color) >= 0.65) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN);
} else {
iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN);
}
}
}
cpv.setColor(color);
}
// update color:
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
color = Color.argb(alpha, red, green, blue);
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
});
}
private int[] unshiftIfNotExists(int[] array, int value) {
boolean present = false;
for (int i : array) {
if (i == value) {
present = true;
break;
}
}
if (present) {
return array;
}
int[] newArray = new int[array.length + 1];
newArray[0] = value;
System.arraycopy(array, 0, newArray, 1, newArray.length - 1);
return newArray;
}
private int[] pushIfNotExists(int[] array, int value) {
boolean present = false;
for (int i : array) {
if (i == value) {
present = true;
break;
}
}
if (!present) {
int[] newArray = new int[array.length + 1];
newArray[newArray.length - 1] = value;
System.arraycopy(array, 0, newArray, 0, newArray.length - 1);
return newArray;
}
return array;
}
private int getSelectedItemPosition() {
for (int i = 0; i < presets.length; i++) {
if (presets[i] == color) {
return i;
}
}
return -1;
}
// endregion
// region Builder
@SuppressWarnings("WeakerAccess")
public static final class Builder {
@StringRes
int dialogTitle = R.string.cpv_default_title;
@DialogType
int dialogType = TYPE_PRESETS;
int[] presets = MATERIAL_COLORS;
@ColorInt
int color = Color.BLACK;
int dialogId = 0;
boolean showAlphaSlider = false;
boolean allowPresets = true;
boolean allowCustom = true;
boolean showColorShades = true;
@ColorShape
int colorShape = ColorShape.CIRCLE;
/*package*/ Builder() {
}
/**
* Set the dialog title string resource id
*
* @param dialogTitle The string resource used for the dialog title
* @return This builder object for chaining method calls
*/
public Builder setDialogTitle(@StringRes int dialogTitle) {
this.dialogTitle = dialogTitle;
return this;
}
/**
* Set which dialog view to show.
*
* @param dialogType Either {@link ColorPickerDialog#TYPE_CUSTOM} or {@link ColorPickerDialog#TYPE_PRESETS}.
* @return This builder object for chaining method calls
*/
public Builder setDialogType(@DialogType int dialogType) {
this.dialogType = dialogType;
return this;
}
/**
* Set the colors used for the presets
*
* @param presets An array of color ints.
* @return This builder object for chaining method calls
*/
public Builder setPresets(@NonNull int[] presets) {
this.presets = presets;
return this;
}
/**
* Set the original color
*
* @param color The default color for the color picker
* @return This builder object for chaining method calls
*/
public Builder setColor(int color) {
this.color = color;
return this;
}
/**
* Set the dialog id used for callbacks
*
* @param dialogId The id that is sent back to the {@link ColorPickerDialogListener}.
* @return This builder object for chaining method calls
*/
public Builder setDialogId(int dialogId) {
this.dialogId = dialogId;
return this;
}
/**
* Show the alpha slider
*
* @param showAlphaSlider {@code true} to show the alpha slider. Currently only supported with
* the {@link ColorPickerView}.
* @return This builder object for chaining method calls
*/
public Builder setShowAlphaSlider(boolean showAlphaSlider) {
this.showAlphaSlider = showAlphaSlider;
return this;
}
/**
* Show/Hide a neutral button to select preset colors.
*
* @param allowPresets {@code false} to disable showing the presets button.
* @return This builder object for chaining method calls
*/
public Builder setAllowPresets(boolean allowPresets) {
this.allowPresets = allowPresets;
return this;
}
/**
* Show/Hide the neutral button to select a custom color.
*
* @param allowCustom {@code false} to disable showing the custom button.
* @return This builder object for chaining method calls
*/
public Builder setAllowCustom(boolean allowCustom) {
this.allowCustom = allowCustom;
return this;
}
/**
* Show/Hide the color shades in the presets picker
*
* @param showColorShades {@code false} to hide the color shades.
* @return This builder object for chaining method calls
*/
public Builder setShowColorShades(boolean showColorShades) {
this.showColorShades = showColorShades;
return this;
}
/**
* Set the shape of the color panel view.
*
* @param colorShape Either {@link ColorShape#CIRCLE} or {@link ColorShape#SQUARE}.
* @return This builder object for chaining method calls
*/
public Builder setColorShape(int colorShape) {
this.colorShape = colorShape;
return this;
}
/**
* Create the {@link ColorPickerDialog} instance.
*
* @return A new {@link ColorPickerDialog}.
* @see #show(FragmentActivity)
*/
public ColorPickerDialog create() {
ColorPickerDialog dialog = new ColorPickerDialog();
Bundle args = new Bundle();
args.putInt(ARG_ID, dialogId);
args.putInt(ARG_TYPE, dialogType);
args.putInt(ARG_COLOR, color);
args.putIntArray(ARG_PRESETS, presets);
args.putBoolean(ARG_ALPHA, showAlphaSlider);
args.putBoolean(ARG_ALLOW_CUSTOM, allowCustom);
args.putBoolean(ARG_ALLOW_PRESETS, allowPresets);
args.putInt(ARG_DIALOG_TITLE, dialogTitle);
args.putBoolean(ARG_SHOW_COLOR_SHADES, showColorShades);
args.putInt(ARG_COLOR_SHAPE, colorShape);
dialog.setArguments(args);
return dialog;
}
/**
* Create and show the {@link ColorPickerDialog} created with this builder.
*
* @param activity The current activity.
*/
public void show(FragmentActivity activity) {
create().show(activity.getSupportFragmentManager(), "color-picker-dialog");
}
}
@Retention(RetentionPolicy.SOURCE)
@IntDef({TYPE_CUSTOM, TYPE_PRESETS})
public @interface DialogType {
}
// endregion
}

View File

@ -0,0 +1,760 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.annotation.SuppressLint
import android.app.Activity
import android.app.Dialog
import android.content.Context
import android.content.DialogInterface
import android.graphics.Color
import android.graphics.PorterDuff
import android.os.Bundle
import android.text.Editable
import android.text.InputFilter
import android.text.InputFilter.LengthFilter
import android.text.TextWatcher
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import android.view.View.OnTouchListener
import android.view.ViewGroup.MarginLayoutParams
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.*
import android.widget.SeekBar.OnSeekBarChangeListener
import androidx.annotation.ColorInt
import androidx.annotation.IntDef
import androidx.annotation.StringRes
import androidx.appcompat.app.AlertDialog
import androidx.core.graphics.ColorUtils
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentActivity
import com.jrummyapps.android.colorpicker.ColorPickerView.OnColorChangedListener
import java.util.*
import kotlin.math.roundToInt
/**
*
* A dialog to pick a color.
*
*
* The [activity][Activity] that shows this dialog should implement [ColorPickerDialogListener]
*
*
* Example usage:
*
* <pre>
* ColorPickerDialog.newBuilder().show(activity);
</pre> *
*/
class ColorPickerDialog :
DialogFragment(),
OnTouchListener,
OnColorChangedListener,
TextWatcher {
companion object {
private const val ARG_ID = "id"
private const val ARG_TYPE = "dialogType"
private const val ARG_COLOR = "color"
private const val ARG_ALPHA = "alpha"
private const val ARG_PRESETS = "presets"
private const val ARG_ALLOW_PRESETS = "allowPresets"
private const val ARG_ALLOW_CUSTOM = "allowCustom"
private const val ARG_DIALOG_TITLE = "dialogTitle"
private const val ARG_SHOW_COLOR_SHADES = "showColorShades"
private const val ARG_COLOR_SHAPE = "colorShape"
const val TYPE_CUSTOM = 0
const val TYPE_PRESETS = 1
const val ALPHA_THRESHOLD = 165
/**
* Material design colors used as the default color presets
*/
@JvmField
val MATERIAL_COLORS = intArrayOf(
-0xbbcca, // RED 500
-0x16e19d, // PINK 500
-0xd36d, // LIGHT PINK 500
-0x63d850, // PURPLE 500
-0x98c549, // DEEP PURPLE 500
-0xc0ae4b, // INDIGO 500
-0xde690d, // BLUE 500
-0xfc560c, // LIGHT BLUE 500
-0xff432c, // CYAN 500
-0xff6978, // TEAL 500
-0xb350b0, // GREEN 500
-0x743cb6, // LIGHT GREEN 500
-0x3223c7, // LIME 500
-0x14c5, // YELLOW 500
-0x3ef9, // AMBER 500
-0x6800, // ORANGE 500
-0x86aab8, // BROWN 500
-0x9f8275, // BLUE GREY 500
-0x616162
)
/**
* Create a new Builder for creating a [ColorPickerDialog] instance
*
* @return The [builder][Builder] to create the [ColorPickerDialog].
*/
@JvmStatic
fun newBuilder(): Builder {
return Builder()
}
fun unshiftIfNotExists(array: IntArray, value: Int): IntArray {
if (array.any { it == value }) {
return array
}
val newArray = IntArray(array.size + 1)
newArray[0] = value
System.arraycopy(array, 0, newArray, 1, newArray.size - 1)
return newArray
}
fun pushIfNotExists(array: IntArray, value: Int): IntArray {
if (array.any { it == value }) {
return array
}
val newArray = IntArray(array.size + 1)
newArray[newArray.size - 1] = value
System.arraycopy(array, 0, newArray, 0, newArray.size - 1)
return newArray
}
}
@Retention(AnnotationRetention.SOURCE)
@IntDef(TYPE_CUSTOM, TYPE_PRESETS)
annotation class DialogType
var colorPickerDialogListener: ColorPickerDialogListener? = null
var presets: IntArray = MATERIAL_COLORS
private var rootView: FrameLayout? = null
@ColorInt
var color = 0
private var dialogType = 0
var dialogId = 0
var showColorShades = false
private var colorShape = 0
// -- PRESETS --------------------------
internal var adapter: ColorPaletteAdapter? = null
private var shadesLayout: LinearLayout? = null
private var transparencySeekBar: SeekBar? = null
private var transparencyPercText: TextView? = null
// -- CUSTOM ---------------------------
var colorPicker: ColorPickerView? = null
private var newColorPanel: ColorPanelView? = null
private var hexEditText: EditText? = null
private var showAlphaSlider = false
private var fromEditText = false
private val selectedItemPosition: Int
get() = presets.indexOf(color)
override fun onAttach(context: Context) {
super.onAttach(context)
if (colorPickerDialogListener == null && context is ColorPickerDialogListener) {
colorPickerDialogListener = context
}
}
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val args = arguments ?: error("onCreateDialog: args is null")
val activity = activity ?: error("onCreateDialog: activity is null")
dialogId = args.getInt(ARG_ID)
showAlphaSlider = args.getBoolean(ARG_ALPHA)
showColorShades = args.getBoolean(ARG_SHOW_COLOR_SHADES)
colorShape = args.getInt(ARG_COLOR_SHAPE)
if (savedInstanceState == null) {
color = args.getInt(ARG_COLOR)
dialogType = args.getInt(ARG_TYPE)
} else {
color = savedInstanceState.getInt(ARG_COLOR)
dialogType = savedInstanceState.getInt(ARG_TYPE)
}
val rootView = FrameLayout(activity).also { this.rootView = it }
when (dialogType) {
TYPE_CUSTOM -> rootView.addView(createPickerView())
TYPE_PRESETS -> rootView.addView(createPresetsView())
}
val builder = AlertDialog.Builder(activity)
.setView(rootView)
.setPositiveButton(R.string.cpv_select) { _, _ ->
colorPickerDialogListener?.onColorSelected(dialogId, color)
}
val dialogTitleStringRes = args.getInt(ARG_DIALOG_TITLE)
if (dialogTitleStringRes != 0) {
builder.setTitle(dialogTitleStringRes)
}
val neutralButtonStringRes: Int
neutralButtonStringRes =
if (dialogType == TYPE_CUSTOM && args.getBoolean(ARG_ALLOW_PRESETS)) {
R.string.cpv_presets
} else if (dialogType == TYPE_PRESETS && args.getBoolean(ARG_ALLOW_CUSTOM)) {
R.string.cpv_custom
} else {
0
}
if (neutralButtonStringRes != 0) {
builder.setNeutralButton(neutralButtonStringRes, null)
}
return builder.create()
}
override fun onStart() {
super.onStart()
val dialog = dialog as AlertDialog
// http://stackoverflow.com/a/16972670/1048340
dialog.window?.clearFlags(
WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or
WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
)
dialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
// Do not dismiss the dialog when clicking the neutral button.
val neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
neutralButton?.setOnClickListener { v: View ->
rootView!!.removeAllViews()
when (dialogType) {
TYPE_CUSTOM -> {
dialogType = TYPE_PRESETS
(v as Button).setText(R.string.cpv_custom)
rootView!!.addView(createPresetsView())
}
TYPE_PRESETS -> {
dialogType = TYPE_CUSTOM
(v as Button).setText(R.string.cpv_presets)
rootView!!.addView(createPickerView())
}
}
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
colorPickerDialogListener!!.onDialogDismissed(dialogId)
}
override fun onSaveInstanceState(outState: Bundle) {
outState.putInt(ARG_COLOR, color)
outState.putInt(ARG_TYPE, dialogType)
super.onSaveInstanceState(outState)
}
// region Custom Picker
private fun createPickerView(): View {
val args = arguments ?: throw RuntimeException("createPickerView: args is null")
val activity = activity ?: throw RuntimeException("createPickerView: activity is null")
val contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null)
colorPicker = contentView.findViewById(R.id.cpv_color_picker_view)
val oldColorPanel: ColorPanelView = contentView.findViewById(R.id.cpv_color_panel_old)
newColorPanel = contentView.findViewById(R.id.cpv_color_panel_new)
val arrowRight = contentView.findViewById<ImageView>(R.id.cpv_arrow_right)
hexEditText = contentView.findViewById(R.id.cpv_hex)
try {
val value = TypedValue()
val typedArray = activity.obtainStyledAttributes(
value.data, intArrayOf(android.R.attr.textColorPrimary)
)
val arrowColor = typedArray.getColor(0, Color.BLACK)
typedArray.recycle()
arrowRight.setColorFilter(arrowColor)
} catch (ignored: Exception) {
}
oldColorPanel.color = args.getInt(ARG_COLOR)
val c = color
colorPicker?.apply {
setAlphaSliderVisible(showAlphaSlider)
setColor(c, true)
onColorChangedListener = this@ColorPickerDialog
}
newColorPanel?.apply {
color = c
setOnClickListener {
if (color == c) {
colorPickerDialogListener?.onColorSelected(dialogId, color)
dismiss()
}
}
}
hexEditText?.apply {
setHex(color)
addTextChangedListener(this@ColorPickerDialog)
setOnFocusChangeListener { _, hasFocus ->
if (hasFocus) showSoftInput(true)
}
if (!showAlphaSlider) {
filters = arrayOf<InputFilter>(LengthFilter(6))
}
}
contentView.setOnTouchListener(this)
return contentView
}
private fun View?.showSoftInput(show: Boolean) {
this ?: return
val imm = (activity?.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager)
?: return
if (show) {
imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT)
} else {
imm.hideSoftInputFromWindow(this.windowToken, 0)
}
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouch(v: View, event: MotionEvent): Boolean {
val hexEditText = this.hexEditText
if (hexEditText?.hasFocus() == true && v !== hexEditText) {
hexEditText.clearFocus()
hexEditText.showSoftInput(false)
hexEditText.clearFocus()
return true
}
return false
}
override fun onColorChanged(newColor: Int) {
color = newColor
newColorPanel!!.color = newColor
if (!fromEditText) {
setHex(newColor)
val hexEditText = this.hexEditText
if (hexEditText?.hasFocus() == true) {
hexEditText.showSoftInput(false)
hexEditText.clearFocus()
}
}
fromEditText = false
}
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(s: Editable) {
if (hexEditText!!.isFocused) {
try {
val color = parseColorString(s.toString())
if (color != colorPicker!!.color) {
fromEditText = true
colorPicker!!.setColor(color, true)
}
} catch (ex: NumberFormatException) {
// nothing to do
}
}
}
private fun setHex(color: Int) {
if (showAlphaSlider) {
hexEditText!!.setText(String.format("%08X", color))
} else {
hexEditText!!.setText(String.format("%06X", 0xFFFFFF and color))
}
}
// -- endregion --
// region Presets Picker
private fun createPresetsView(): View {
val contentView = View.inflate(activity, R.layout.cpv_dialog_presets, null)
shadesLayout = contentView.findViewById(R.id.shades_layout)
transparencySeekBar = contentView.findViewById(R.id.transparency_seekbar)
transparencyPercText = contentView.findViewById(R.id.transparency_text)
val gridView = contentView.findViewById<GridView>(R.id.gridView)
loadPresets()
if (showColorShades) {
createColorShades(color)
} else {
shadesLayout?.visibility = View.GONE
contentView.findViewById<View>(R.id.shades_divider).visibility = View.GONE
}
adapter = ColorPaletteAdapter(presets, selectedItemPosition, colorShape){
when(it){
color -> {
colorPickerDialogListener?.onColorSelected(dialogId, color)
dismiss()
}
else ->{
color = it
if (showColorShades) {
createColorShades(color)
}
}
}
}
gridView.adapter = adapter
if (showAlphaSlider) {
setupTransparency()
} else {
contentView.findViewById<View>(R.id.transparency_layout).visibility = View.GONE
contentView.findViewById<View>(R.id.transparency_title).visibility = View.GONE
}
return contentView
}
private fun loadPresets() {
presets = arguments?.getIntArray(ARG_PRESETS) ?: MATERIAL_COLORS
presets = presets.copyOf()
val isMaterialColors = presets.contentEquals(MATERIAL_COLORS)
val alpha = Color.alpha(color)
// don't update the original array when modifying alpha
if (alpha != 255) {
// add alpha to the presets
for (i in presets.indices) {
val color = presets[i]
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
presets[i] = Color.argb(alpha, red, green, blue)
}
}
presets = unshiftIfNotExists(presets, color)
if (isMaterialColors && presets.size == 19) {
// Add black to have a total of 20 colors if the current color is in the material color palette
presets = pushIfNotExists(presets, Color.argb(alpha, 0, 0, 0))
}
}
private fun createColorShades(@ColorInt color: Int) {
val colorShades = getColorShades(color)
if (shadesLayout!!.childCount != 0) {
for (i in 0 until shadesLayout!!.childCount) {
val layout = shadesLayout!!.getChildAt(i) as FrameLayout
val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
cpv.color = colorShades[i]
cpv.tag = false
iv.setImageDrawable(null)
}
return
}
val horizontalPadding = resources
.getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding)
for (colorShade in colorShades) {
var layoutResId: Int
layoutResId = if (colorShape == ColorShape.SQUARE) {
R.layout.cpv_color_item_square
} else {
R.layout.cpv_color_item_circle
}
val view = View.inflate(activity, layoutResId, null)
val colorPanelView: ColorPanelView = view.findViewById(R.id.cpv_color_panel_view)
val params = colorPanelView
.layoutParams as MarginLayoutParams
params.rightMargin = horizontalPadding
params.leftMargin = params.rightMargin
colorPanelView.layoutParams = params
colorPanelView.color = colorShade
shadesLayout!!.addView(view)
colorPanelView.post {
// The color is black when rotating the dialog. This is a dirty fix. WTF!?
colorPanelView.color = colorShade
}
colorPanelView.setOnClickListener { v: View ->
if (v.tag is Boolean && v.tag as Boolean) {
colorPickerDialogListener!!.onColorSelected(
dialogId,
this@ColorPickerDialog.color
)
dismiss()
return@setOnClickListener // already selected
}
this@ColorPickerDialog.color = colorPanelView.color
adapter!!.selectNone()
for (i in 0 until shadesLayout!!.childCount) {
val layout = shadesLayout!!.getChildAt(i) as FrameLayout
val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
iv.setImageResource(if (cpv === v) R.drawable.cpv_preset_checked else 0)
if (cpv === v && ColorUtils.calculateLuminance(cpv.color) >= 0.65 ||
Color.alpha(cpv.color) <= ALPHA_THRESHOLD
) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
} else {
iv.colorFilter = null
}
cpv.tag = cpv === v
}
}
colorPanelView.setOnLongClickListener {
colorPanelView.showHint()
true
}
}
}
private fun shadeColor(@ColorInt color: Int, percent: Double): Int {
val hex = String.format("#%06X", 0xFFFFFF and color)
val f = hex.substring(1).toLong(16)
val t = (if (percent < 0) 0 else 255).toDouble()
val p = if (percent < 0) percent * -1 else percent
val R = f shr 16
val G = f shr 8 and 0x00FF
val B = f and 0x0000FF
return Color.argb(
Color.alpha(color),
((t - R) * p).roundToInt() + R.toInt(),
((t - G) * p).roundToInt() + G.toInt(),
((t - B) * p).roundToInt() + B.toInt(),
)
}
private fun getColorShades(@ColorInt color: Int): IntArray {
return intArrayOf(
shadeColor(color, 0.9),
shadeColor(color, 0.7),
shadeColor(color, 0.5),
shadeColor(color, 0.333),
shadeColor(color, 0.166),
shadeColor(color, -0.125),
shadeColor(color, -0.25),
shadeColor(color, -0.375),
shadeColor(color, -0.5),
shadeColor(color, -0.675),
shadeColor(color, -0.7),
shadeColor(color, -0.775)
)
}
private fun setupTransparency() {
val transparency = 255 - Color.alpha(color)
val percentage = (transparency.toDouble() * 100 / 255).toInt()
transparencyPercText?.text =
String.format(Locale.ENGLISH, "%d%%", percentage)
transparencySeekBar?.apply {
max = 255
progress = transparency
this.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
handleTransparencyChanged(progress)
}
})
}
}
private fun handleTransparencyChanged(transparency: Int) {
val percentage = (transparency.toDouble() * 100 / 255).toInt()
val alpha = 255 - transparency
transparencyPercText?.text =
String.format(Locale.ENGLISH, "%d%%", percentage)
// update color:
val red = Color.red(color)
val green = Color.green(color)
val blue = Color.blue(color)
color = Color.argb(alpha, red, green, blue)
// update items in GridView:
adapter?.apply {
for (i in colors.indices) {
val color = colors[i]
colors[i] = Color.argb(
alpha,
Color.red(color),
Color.green(color),
Color.blue(color)
)
}
notifyDataSetChanged()
}
// update shades:
shadesLayout?.apply {
for (i in 0 until childCount) {
val layout = getChildAt(i) as FrameLayout
val cpv: ColorPanelView = layout.findViewById(R.id.cpv_color_panel_view)
val iv = layout.findViewById<ImageView>(R.id.cpv_color_image_view)
if (layout.tag == null) {
// save the original border color
layout.tag = cpv.borderColor
}
var color = cpv.color
color = Color.argb(
alpha,
Color.red(color),
Color.green(color),
Color.blue(color)
)
if (alpha <= ALPHA_THRESHOLD) {
cpv.borderColor = color or -0x1000000
} else {
cpv.borderColor = layout.tag as Int
}
if (cpv.tag != null && cpv.tag as Boolean) {
// The alpha changed on the selected shaded color. Update the checkmark color filter.
if (alpha <= ALPHA_THRESHOLD) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
} else {
if (ColorUtils.calculateLuminance(color) >= 0.65) {
iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN)
} else {
iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN)
}
}
}
cpv.color = color
}
}
}
@Suppress("MemberVisibilityCanBePrivate")
class Builder(
// dialog title string resource id.
@StringRes
var dialogTitle: Int = R.string.cpv_default_title,
// which dialog view to show.
// Either [ColorPickerDialog.TYPE_CUSTOM] or [ColorPickerDialog.TYPE_PRESETS].
var dialogType: Int = TYPE_PRESETS,
// the colors used for the presets.
var presets: IntArray = MATERIAL_COLORS,
// the original color.
@ColorInt
var color: Int = Color.BLACK,
// the dialog id used for callbacks.
var dialogId: Int = 0,
// the alpha slider
// true to show the alpha slider.
// Currently only supported with the ColorPickerView.
var showAlphaSlider: Boolean = false,
// Show/Hide a neutral button to select preset colors.
// false to disable showing the presets button.
var allowPresets: Boolean = true,
// Show/Hide the neutral button to select a custom color.
// false to disable showing the custom button.
var allowCustom: Boolean = true,
// Show/Hide the color shades in the presets picker
// false to hide the color shades.
var showColorShades: Boolean = true,
// the shape of the color panel view.
// Either [ColorShape.CIRCLE] or [ColorShape.SQUARE].
var colorShape: Int = ColorShape.CIRCLE,
) {
fun setDialogTitle(@StringRes dialogTitle: Int): Builder {
this.dialogTitle = dialogTitle
return this
}
fun setDialogType(@DialogType dialogType: Int): Builder {
this.dialogType = dialogType
return this
}
fun setPresets(presets: IntArray): Builder {
this.presets = presets
return this
}
fun setColor(color: Int): Builder {
this.color = color
return this
}
fun setDialogId(dialogId: Int): Builder {
this.dialogId = dialogId
return this
}
fun setShowAlphaSlider(showAlphaSlider: Boolean): Builder {
this.showAlphaSlider = showAlphaSlider
return this
}
fun setAllowPresets(allowPresets: Boolean): Builder {
this.allowPresets = allowPresets
return this
}
fun setAllowCustom(allowCustom: Boolean): Builder {
this.allowCustom = allowCustom
return this
}
fun setShowColorShades(showColorShades: Boolean): Builder {
this.showColorShades = showColorShades
return this
}
fun setColorShape(colorShape: Int): Builder {
this.colorShape = colorShape
return this
}
/**
* Create the [ColorPickerDialog] instance.
*/
fun create(): ColorPickerDialog {
val dialog = ColorPickerDialog()
val args = Bundle()
args.putInt(ARG_ID, dialogId)
args.putInt(ARG_TYPE, dialogType)
args.putInt(ARG_COLOR, color)
args.putIntArray(ARG_PRESETS, presets)
args.putBoolean(ARG_ALPHA, showAlphaSlider)
args.putBoolean(ARG_ALLOW_CUSTOM, allowCustom)
args.putBoolean(ARG_ALLOW_PRESETS, allowPresets)
args.putInt(ARG_DIALOG_TITLE, dialogTitle)
args.putBoolean(ARG_SHOW_COLOR_SHADES, showColorShades)
args.putInt(ARG_COLOR_SHAPE, colorShape)
dialog.arguments = args
return dialog
}
/**
* Create and show the [ColorPickerDialog] created with this builder.
*/
fun show(activity: FragmentActivity) {
create().show(activity.supportFragmentManager, "color-picker-dialog")
}
}
}

View File

@ -13,32 +13,29 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
package com.jrummyapps.android.colorpicker;
import androidx.annotation.ColorInt;
import androidx.annotation.ColorInt
/**
* Callback used for getting the selected color from a color picker dialog.
*/
public interface ColorPickerDialogListener {
interface ColorPickerDialogListener {
/**
* Callback that is invoked when a color is selected from the color picker dialog.
*
* @param dialogId
* The dialog id used to create the dialog instance.
* @param color
* The selected color
*/
fun onColorSelected(dialogId: Int, @ColorInt newColor: Int)
/**
* Callback that is invoked when a color is selected from the color picker dialog.
*
* @param dialogId
* The dialog id used to create the dialog instance.
* @param color
* The selected color
*/
void onColorSelected(int dialogId, @ColorInt int color);
/**
* Callback that is invoked when the color picker dialog was dismissed.
*
* @param dialogId
* The dialog id used to create the dialog instance.
*/
void onDialogDismissed(int dialogId);
/**
* Callback that is invoked when the color picker dialog was dismissed.
*
* @param dialogId
* The dialog id used to create the dialog instance.
*/
fun onDialogDismissed(dialogId: Int)
}

View File

@ -1,982 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.annotation.SuppressLint;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Bitmap.Config;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Style;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Shader.TileMode;
import android.os.Bundle;
import android.os.Parcelable;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.MotionEvent;
import android.view.View;
/**
* Displays a color picker to the user and allow them to select a color. A slider for the alpha channel is also available.
* Enable it by setting setAlphaSliderVisible(boolean) to true.
*/
public class ColorPickerView extends View {
private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E;
private final static int DEFAULT_SLIDER_COLOR = 0xFFBDBDBD;
private final static int HUE_PANEL_WDITH_DP = 30;
private final static int ALPHA_PANEL_HEIGH_DP = 20;
private final static int PANEL_SPACING_DP = 10;
private final static int CIRCLE_TRACKER_RADIUS_DP = 5;
private final static int SLIDER_TRACKER_SIZE_DP = 4;
private final static int SLIDER_TRACKER_OFFSET_DP = 2;
/**
* The width in pixels of the border
* surrounding all color panels.
*/
private final static int BORDER_WIDTH_PX = 1;
/**
* The width in px of the hue panel.
*/
private int huePanelWidthPx;
/**
* The height in px of the alpha panel
*/
private int alphaPanelHeightPx;
/**
* The distance in px between the different
* color panels.
*/
private int panelSpacingPx;
/**
* The radius in px of the color palette tracker circle.
*/
private int circleTrackerRadiusPx;
/**
* The px which the tracker of the hue or alpha panel
* will extend outside of its bounds.
*/
private int sliderTrackerOffsetPx;
/**
* Height of slider tracker on hue panel,
* width of slider on alpha panel.
*/
private int sliderTrackerSizePx;
private Paint satValPaint;
private Paint satValTrackerPaint;
private Paint alphaPaint;
private Paint alphaTextPaint;
private Paint hueAlphaTrackerPaint;
private Paint borderPaint;
private Shader valShader;
private Shader satShader;
private Shader alphaShader;
/*
* We cache a bitmap of the sat/val panel which is expensive to draw each time.
* We can reuse it when the user is sliding the circle picker as long as the hue isn't changed.
*/
private BitmapCache satValBackgroundCache;
/* We cache the hue background to since its also very expensive now. */
private BitmapCache hueBackgroundCache;
/* Current values */
private int alpha = 0xff;
private float hue = 360f;
private float sat = 0f;
private float val = 0f;
private boolean showAlphaPanel = false;
private String alphaSliderText = null;
private int sliderTrackerColor = DEFAULT_SLIDER_COLOR;
private int borderColor = DEFAULT_BORDER_COLOR;
/**
* Minimum required padding. The offset from the
* edge we must have or else the finger tracker will
* get clipped when it's drawn outside of the view.
*/
private int mRequiredPadding;
/**
* The Rect in which we are allowed to draw.
* Trackers can extend outside slightly,
* due to the required padding we have set.
*/
private Rect drawingRect;
private Rect satValRect;
private Rect hueRect;
private Rect alphaRect;
private Point startTouchPoint = null;
private AlphaPatternDrawable alphaPatternDrawable;
private OnColorChangedListener onColorChangedListener;
public ColorPickerView(Context context) {
this(context, null);
}
public ColorPickerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public ColorPickerView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(context, attrs);
}
@Override
public Parcelable onSaveInstanceState() {
Bundle state = new Bundle();
state.putParcelable("instanceState", super.onSaveInstanceState());
state.putInt("alpha", alpha);
state.putFloat("hue", hue);
state.putFloat("sat", sat);
state.putFloat("val", val);
state.putBoolean("show_alpha", showAlphaPanel);
state.putString("alpha_text", alphaSliderText);
return state;
}
@Override
public void onRestoreInstanceState(Parcelable state) {
if (state instanceof Bundle) {
Bundle bundle = (Bundle) state;
alpha = bundle.getInt("alpha");
hue = bundle.getFloat("hue");
sat = bundle.getFloat("sat");
val = bundle.getFloat("val");
showAlphaPanel = bundle.getBoolean("show_alpha");
alphaSliderText = bundle.getString("alpha_text");
state = bundle.getParcelable("instanceState");
}
super.onRestoreInstanceState(state);
}
private void init(Context context, AttributeSet attrs) {
//Load those if set in xml resource file.
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPickerView);
showAlphaPanel = a.getBoolean(R.styleable.ColorPickerView_cpv_alphaChannelVisible, false);
alphaSliderText = a.getString(R.styleable.ColorPickerView_cpv_alphaChannelText);
sliderTrackerColor = a.getColor(R.styleable.ColorPickerView_cpv_sliderColor, 0xFFBDBDBD);
borderColor = a.getColor(R.styleable.ColorPickerView_cpv_borderColor, 0xFF6E6E6E);
a.recycle();
applyThemeColors(context);
huePanelWidthPx = DrawingUtils.dpToPx(getContext(), HUE_PANEL_WDITH_DP);
alphaPanelHeightPx = DrawingUtils.dpToPx(getContext(), ALPHA_PANEL_HEIGH_DP);
panelSpacingPx = DrawingUtils.dpToPx(getContext(), PANEL_SPACING_DP);
circleTrackerRadiusPx = DrawingUtils.dpToPx(getContext(), CIRCLE_TRACKER_RADIUS_DP);
sliderTrackerSizePx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_SIZE_DP);
sliderTrackerOffsetPx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_OFFSET_DP);
mRequiredPadding = getResources().getDimensionPixelSize(R.dimen.cpv_required_padding);
initPaintTools();
//Needed for receiving trackball motion events.
setFocusable(true);
setFocusableInTouchMode(true);
}
private void applyThemeColors(Context c) {
// If no specific border/slider color has been
// set we take the default secondary text color
// as border/slider color. Thus it will adopt
// to theme changes automatically.
final TypedValue value = new TypedValue();
TypedArray a = c.obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorSecondary});
if (borderColor == DEFAULT_BORDER_COLOR) {
borderColor = a.getColor(0, DEFAULT_BORDER_COLOR);
}
if (sliderTrackerColor == DEFAULT_SLIDER_COLOR) {
sliderTrackerColor = a.getColor(0, DEFAULT_SLIDER_COLOR);
}
a.recycle();
}
private void initPaintTools() {
satValPaint = new Paint();
satValTrackerPaint = new Paint();
hueAlphaTrackerPaint = new Paint();
alphaPaint = new Paint();
alphaTextPaint = new Paint();
borderPaint = new Paint();
satValTrackerPaint.setStyle(Style.STROKE);
satValTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2));
satValTrackerPaint.setAntiAlias(true);
hueAlphaTrackerPaint.setColor(sliderTrackerColor);
hueAlphaTrackerPaint.setStyle(Style.STROKE);
hueAlphaTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2));
hueAlphaTrackerPaint.setAntiAlias(true);
alphaTextPaint.setColor(0xff1c1c1c);
alphaTextPaint.setTextSize(DrawingUtils.dpToPx(getContext(), 14));
alphaTextPaint.setAntiAlias(true);
alphaTextPaint.setTextAlign(Align.CENTER);
alphaTextPaint.setFakeBoldText(true);
}
@Override
protected void onDraw(Canvas canvas) {
if (drawingRect.width() <= 0 || drawingRect.height() <= 0) {
return;
}
drawSatValPanel(canvas);
drawHuePanel(canvas);
drawAlphaPanel(canvas);
}
private void drawSatValPanel(Canvas canvas) {
final Rect rect = satValRect;
if (BORDER_WIDTH_PX > 0) {
borderPaint.setColor(borderColor);
canvas.drawRect(drawingRect.left, drawingRect.top,
rect.right + BORDER_WIDTH_PX,
rect.bottom + BORDER_WIDTH_PX, borderPaint);
}
if (valShader == null) {
//Black gradient has either not been created or the view has been resized.
valShader = new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, 0xffffffff, 0xff000000, TileMode.CLAMP);
}
//If the hue has changed we need to recreate the cache.
if (satValBackgroundCache == null || satValBackgroundCache.value != hue) {
if (satValBackgroundCache == null) {
satValBackgroundCache = new BitmapCache();
}
//We create our bitmap in the cache if it doesn't exist.
if (satValBackgroundCache.bitmap == null) {
satValBackgroundCache.bitmap = Bitmap
.createBitmap(rect.width(), rect.height(), Config.ARGB_8888);
}
//We create the canvas once so we can draw on our bitmap and the hold on to it.
if (satValBackgroundCache.canvas == null) {
satValBackgroundCache.canvas = new Canvas(satValBackgroundCache.bitmap);
}
int rgb = Color.HSVToColor(new float[]{hue, 1f, 1f});
satShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, TileMode.CLAMP);
ComposeShader mShader = new ComposeShader(
valShader, satShader, PorterDuff.Mode.MULTIPLY);
satValPaint.setShader(mShader);
// Finally we draw on our canvas, the result will be
// stored in our bitmap which is already in the cache.
// Since this is drawn on a canvas not rendered on
// screen it will automatically not be using the
// hardware acceleration. And this was the code that
// wasn't supported by hardware acceleration which mean
// there is no need to turn it of anymore. The rest of
// the view will still be hw accelerated.
satValBackgroundCache.canvas.drawRect(0, 0,
satValBackgroundCache.bitmap.getWidth(),
satValBackgroundCache.bitmap.getHeight(),
satValPaint);
//We set the hue value in our cache to which hue it was drawn with,
//then we know that if it hasn't changed we can reuse our cached bitmap.
satValBackgroundCache.value = hue;
}
// We draw our bitmap from the cached, if the hue has changed
// then it was just recreated otherwise the old one will be used.
canvas.drawBitmap(satValBackgroundCache.bitmap, null, rect, null);
Point p = satValToPoint(sat, val);
satValTrackerPaint.setColor(0xff000000);
canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx - DrawingUtils.dpToPx(getContext(), 1), satValTrackerPaint);
satValTrackerPaint.setColor(0xffdddddd);
canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx, satValTrackerPaint);
}
private void drawHuePanel(Canvas canvas) {
final Rect rect = hueRect;
if (BORDER_WIDTH_PX > 0) {
borderPaint.setColor(borderColor);
canvas.drawRect(rect.left - BORDER_WIDTH_PX,
rect.top - BORDER_WIDTH_PX,
rect.right + BORDER_WIDTH_PX,
rect.bottom + BORDER_WIDTH_PX,
borderPaint);
}
if (hueBackgroundCache == null) {
hueBackgroundCache = new BitmapCache();
hueBackgroundCache.bitmap = Bitmap.createBitmap(rect.width(), rect.height(), Config.ARGB_8888);
hueBackgroundCache.canvas = new Canvas(hueBackgroundCache.bitmap);
int[] hueColors = new int[(int) (rect.height() + 0.5f)];
// Generate array of all colors, will be drawn as individual lines.
float h = 360f;
for (int i = 0; i < hueColors.length; i++) {
hueColors[i] = Color.HSVToColor(new float[]{h, 1f, 1f});
h -= 360f / hueColors.length;
}
// Time to draw the hue color gradient,
// its drawn as individual lines which
// will be quite many when the resolution is high
// and/or the panel is large.
Paint linePaint = new Paint();
linePaint.setStrokeWidth(0);
for (int i = 0; i < hueColors.length; i++) {
linePaint.setColor(hueColors[i]);
hueBackgroundCache.canvas.drawLine(0, i, hueBackgroundCache.bitmap.getWidth(), i, linePaint);
}
}
canvas.drawBitmap(hueBackgroundCache.bitmap, null, rect, null);
Point p = hueToPoint(hue);
RectF r = new RectF();
r.left = rect.left - sliderTrackerOffsetPx;
r.right = rect.right + sliderTrackerOffsetPx;
r.top = p.y - (sliderTrackerSizePx / 2);
r.bottom = p.y + (sliderTrackerSizePx / 2);
canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint);
}
private void drawAlphaPanel(Canvas canvas) {
/*
* Will be drawn with hw acceleration, very fast.
* Also the AlphaPatternDrawable is backed by a bitmap
* generated only once if the size does not change.
*/
if (!showAlphaPanel || alphaRect == null || alphaPatternDrawable == null) return;
final Rect rect = alphaRect;
if (BORDER_WIDTH_PX > 0) {
borderPaint.setColor(borderColor);
canvas.drawRect(rect.left - BORDER_WIDTH_PX,
rect.top - BORDER_WIDTH_PX,
rect.right + BORDER_WIDTH_PX,
rect.bottom + BORDER_WIDTH_PX,
borderPaint);
}
alphaPatternDrawable.draw(canvas);
float[] hsv = new float[]{hue, sat, val};
int color = Color.HSVToColor(hsv);
int acolor = Color.HSVToColor(0, hsv);
alphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top,
color, acolor, TileMode.CLAMP);
alphaPaint.setShader(alphaShader);
canvas.drawRect(rect, alphaPaint);
if (alphaSliderText != null && !alphaSliderText.equals("")) {
canvas.drawText(alphaSliderText, rect.centerX(), rect.centerY() + DrawingUtils.dpToPx(getContext(), 4), alphaTextPaint);
}
Point p = alphaToPoint(alpha);
RectF r = new RectF();
r.left = p.x - (sliderTrackerSizePx / 2);
r.right = p.x + (sliderTrackerSizePx / 2);
r.top = rect.top - sliderTrackerOffsetPx;
r.bottom = rect.bottom + sliderTrackerOffsetPx;
canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint);
}
private Point hueToPoint(float hue) {
final Rect rect = hueRect;
final float height = rect.height();
Point p = new Point();
p.y = (int) (height - (hue * height / 360f) + rect.top);
p.x = rect.left;
return p;
}
private Point satValToPoint(float sat, float val) {
final Rect rect = satValRect;
final float height = rect.height();
final float width = rect.width();
Point p = new Point();
p.x = (int) (sat * width + rect.left);
p.y = (int) ((1f - val) * height + rect.top);
return p;
}
private Point alphaToPoint(int alpha) {
final Rect rect = alphaRect;
final float width = rect.width();
Point p = new Point();
p.x = (int) (width - (alpha * width / 0xff) + rect.left);
p.y = rect.top;
return p;
}
private float[] pointToSatVal(float x, float y) {
final Rect rect = satValRect;
float[] result = new float[2];
float width = rect.width();
float height = rect.height();
if (x < rect.left) {
x = 0f;
} else if (x > rect.right) {
x = width;
} else {
x = x - rect.left;
}
if (y < rect.top) {
y = 0f;
} else if (y > rect.bottom) {
y = height;
} else {
y = y - rect.top;
}
result[0] = 1.f / width * x;
result[1] = 1.f - (1.f / height * y);
return result;
}
private float pointToHue(float y) {
final Rect rect = hueRect;
float height = rect.height();
if (y < rect.top) {
y = 0f;
} else if (y > rect.bottom) {
y = height;
} else {
y = y - rect.top;
}
return 360f - (y * 360f / height);
}
private int pointToAlpha(int x) {
final Rect rect = alphaRect;
final int width = rect.width();
if (x < rect.left) {
x = 0;
} else if (x > rect.right) {
x = width;
} else {
x = x - rect.left;
}
return 0xff - (x * 0xff / width);
}
@SuppressLint("ClickableViewAccessibility")
@Override
public boolean onTouchEvent(MotionEvent event) {
try {
this.getParent().requestDisallowInterceptTouchEvent(true);
} catch (Throwable ignored) {
}
boolean update = false;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
startTouchPoint = new Point((int) event.getX(), (int) event.getY());
update = moveTrackersIfNeeded(event);
break;
case MotionEvent.ACTION_MOVE:
update = moveTrackersIfNeeded(event);
break;
case MotionEvent.ACTION_UP:
startTouchPoint = null;
update = moveTrackersIfNeeded(event);
break;
}
if (update) {
if (onColorChangedListener != null) {
onColorChangedListener.onColorChanged(Color.HSVToColor(alpha, new float[]{hue, sat, val}));
}
invalidate();
return true;
}
return super.onTouchEvent(event);
}
private boolean moveTrackersIfNeeded(MotionEvent event) {
if (startTouchPoint == null) {
return false;
}
boolean update = false;
int startX = startTouchPoint.x;
int startY = startTouchPoint.y;
if (hueRect.contains(startX, startY)) {
hue = pointToHue(event.getY());
update = true;
} else if (satValRect.contains(startX, startY)) {
float[] result = pointToSatVal(event.getX(), event.getY());
sat = result[0];
val = result[1];
update = true;
} else if (alphaRect != null && alphaRect.contains(startX, startY)) {
alpha = pointToAlpha((int) event.getX());
update = true;
}
return update;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int finalWidth;
int finalHeight;
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthAllowed = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int heightAllowed =
MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop();
if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) {
//A exact value has been set in either direction, we need to stay within this size.
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
//The with has been specified exactly, we need to adopt the height to fit.
int h = (widthAllowed - panelSpacingPx - huePanelWidthPx);
if (showAlphaPanel) {
h += panelSpacingPx + alphaPanelHeightPx;
}
//We can't fit the view in this container, set the size to whatever was allowed.
finalHeight = Math.min(h, heightAllowed);
finalWidth = widthAllowed;
} else if (widthMode != MeasureSpec.EXACTLY) {
//The height has been specified exactly, we need to stay within this height and adopt the width.
int w = (heightAllowed + panelSpacingPx + huePanelWidthPx);
if (showAlphaPanel) {
w -= (panelSpacingPx + alphaPanelHeightPx);
}
//we can't fit within this container, set the size to whatever was allowed.
finalWidth = Math.min(w, widthAllowed);
finalHeight = heightAllowed;
} else {
//If we get here the dev has set the width and height to exact sizes. For example match_parent or 300dp.
//This will mean that the sat/val panel will not be square but it doesn't matter. It will work anyway.
//In all other senarios our goal is to make that panel square.
//We set the sizes to exactly what we were told.
finalWidth = widthAllowed;
finalHeight = heightAllowed;
}
} else {
//If no exact size has been set we try to make our view as big as possible
//within the allowed space.
//Calculate the needed width to layout using max allowed height.
int widthNeeded = (heightAllowed + panelSpacingPx + huePanelWidthPx);
//Calculate the needed height to layout using max allowed width.
int heightNeeded = (widthAllowed - panelSpacingPx - huePanelWidthPx);
if (showAlphaPanel) {
widthNeeded -= (panelSpacingPx + alphaPanelHeightPx);
heightNeeded += panelSpacingPx + alphaPanelHeightPx;
}
boolean widthOk = false;
boolean heightOk = false;
if (widthNeeded <= widthAllowed) {
widthOk = true;
}
if (heightNeeded <= heightAllowed) {
heightOk = true;
}
if (widthOk && heightOk) {
finalWidth = widthAllowed;
finalHeight = heightNeeded;
} else if (widthOk) {
finalHeight = heightAllowed;
finalWidth = widthNeeded;
} else if (heightOk) {
finalHeight = heightNeeded;
finalWidth = widthAllowed;
} else {
finalHeight = heightAllowed;
finalWidth = widthAllowed;
}
}
setMeasuredDimension(finalWidth + getPaddingLeft() + getPaddingRight(),
finalHeight + getPaddingTop() + getPaddingBottom());
}
@Override
public int getPaddingTop() {
return Math.max(super.getPaddingTop(), mRequiredPadding);
}
@Override
public int getPaddingBottom() {
return Math.max(super.getPaddingBottom(), mRequiredPadding);
}
@Override
public int getPaddingLeft() {
return Math.max(super.getPaddingLeft(), mRequiredPadding);
}
@Override
public int getPaddingRight() {
return Math.max(super.getPaddingRight(), mRequiredPadding);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
drawingRect = new Rect();
drawingRect.left = getPaddingLeft();
drawingRect.right = w - getPaddingRight();
drawingRect.top = getPaddingTop();
drawingRect.bottom = h - getPaddingBottom();
//The need to be recreated because they depend on the size of the view.
valShader = null;
satShader = null;
alphaShader = null;
// Clear those bitmap caches since the size may have changed.
satValBackgroundCache = null;
hueBackgroundCache = null;
setUpSatValRect();
setUpHueRect();
setUpAlphaRect();
}
private void setUpSatValRect() {
//Calculate the size for the big color rectangle.
final Rect dRect = drawingRect;
int left = dRect.left + BORDER_WIDTH_PX;
int top = dRect.top + BORDER_WIDTH_PX;
int bottom = dRect.bottom - BORDER_WIDTH_PX;
int right = dRect.right - BORDER_WIDTH_PX - panelSpacingPx - huePanelWidthPx;
if (showAlphaPanel) {
bottom -= (alphaPanelHeightPx + panelSpacingPx);
}
satValRect = new Rect(left, top, right, bottom);
}
private void setUpHueRect() {
//Calculate the size for the hue slider on the left.
final Rect dRect = drawingRect;
int left = dRect.right - huePanelWidthPx + BORDER_WIDTH_PX;
int top = dRect.top + BORDER_WIDTH_PX;
int bottom = dRect.bottom - BORDER_WIDTH_PX -
(showAlphaPanel ? (panelSpacingPx + alphaPanelHeightPx) : 0);
int right = dRect.right - BORDER_WIDTH_PX;
hueRect = new Rect(left, top, right, bottom);
}
private void setUpAlphaRect() {
if (!showAlphaPanel) return;
final Rect dRect = drawingRect;
int left = dRect.left + BORDER_WIDTH_PX;
int top = dRect.bottom - alphaPanelHeightPx + BORDER_WIDTH_PX;
int bottom = dRect.bottom - BORDER_WIDTH_PX;
int right = dRect.right - BORDER_WIDTH_PX;
alphaRect = new Rect(left, top, right, bottom);
alphaPatternDrawable = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4));
alphaPatternDrawable.setBounds(Math.round(alphaRect.left), Math
.round(alphaRect.top), Math.round(alphaRect.right), Math
.round(alphaRect.bottom));
}
/**
* Set a OnColorChangedListener to get notified when the color
* selected by the user has changed.
*
* @param listener the listener
*/
public void setOnColorChangedListener(OnColorChangedListener listener) {
onColorChangedListener = listener;
}
/**
* Get the current color this view is showing.
*
* @return the current color.
*/
public int getColor() {
return Color.HSVToColor(alpha, new float[]{hue, sat, val});
}
/**
* Set the color the view should show.
*
* @param color The color that should be selected. #argb
*/
public void setColor(int color) {
setColor(color, false);
}
/**
* Set the color this view should show.
*
* @param color The color that should be selected. #argb
* @param callback If you want to get a callback to your OnColorChangedListener.
*/
public void setColor(int color, boolean callback) {
int alpha = Color.alpha(color);
int red = Color.red(color);
int blue = Color.blue(color);
int green = Color.green(color);
float[] hsv = new float[3];
Color.RGBToHSV(red, green, blue, hsv);
this.alpha = alpha;
hue = hsv[0];
sat = hsv[1];
val = hsv[2];
if (callback && onColorChangedListener != null) {
onColorChangedListener
.onColorChanged(Color.HSVToColor(this.alpha, new float[]{hue, sat, val}));
}
invalidate();
}
/**
* Set if the user is allowed to adjust the alpha panel. Default is false.
* If it is set to false no alpha will be set.
*
* @param visible {@code true} to show the alpha slider
*/
public void setAlphaSliderVisible(boolean visible) {
if (showAlphaPanel != visible) {
showAlphaPanel = visible;
/*
* Force recreation.
*/
valShader = null;
satShader = null;
alphaShader = null;
hueBackgroundCache = null;
satValBackgroundCache = null;
requestLayout();
}
}
/**
* Set the color of the tracker slider on the hue and alpha panel.
*
* @param color a color value
*/
@SuppressWarnings("unused")
public void setSliderTrackerColor(int color) {
sliderTrackerColor = color;
hueAlphaTrackerPaint.setColor(sliderTrackerColor);
invalidate();
}
/**
* Get color of the tracker slider on the hue and alpha panel.
*
* @return the color value
*/
@SuppressWarnings("unused")
public int getSliderTrackerColor() {
return sliderTrackerColor;
}
/**
* Set the color of the border surrounding all panels.
*
* @param color a color value
*/
@SuppressWarnings("unused")
public void setBorderColor(int color) {
borderColor = color;
invalidate();
}
/**
* Get the color of the border surrounding all panels.
*/
@SuppressWarnings("unused")
public int getBorderColor() {
return borderColor;
}
/**
* Set the text that should be shown in the
* alpha slider. Set to null to disable text.
*
* @param res string resource id.
*/
@SuppressWarnings("unused")
public void setAlphaSliderText(int res) {
String text = getContext().getString(res);
setAlphaSliderText(text);
}
/**
* Set the text that should be shown in the
* alpha slider. Set to null to disable text.
*
* @param text Text that should be shown.
*/
public void setAlphaSliderText(String text) {
alphaSliderText = text;
invalidate();
}
/**
* Get the current value of the text
* that will be shown in the alpha
* slider.
*
* @return the slider text
*/
@SuppressWarnings("unused")
public String getAlphaSliderText() {
return alphaSliderText;
}
private static class BitmapCache {
public Canvas canvas;
public Bitmap bitmap;
public float value;
}
public interface OnColorChangedListener {
void onColorChanged(int newColor);
}
}

View File

@ -0,0 +1,840 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.*
import android.graphics.Paint.Align
import android.graphics.Shader.TileMode
import android.os.Bundle
import android.os.Parcelable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.MotionEvent
import android.view.View
import androidx.annotation.ColorInt
import kotlin.math.max
import kotlin.math.min
/**
* Displays a color picker to the user and allow them to select a color. A slider for the alpha channel is also available.
* Enable it by setting setAlphaSliderVisible(boolean) to true.
*/
class ColorPickerView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = 0
) : View(context, attrs, defStyle) {
companion object {
private const val DEFAULT_BORDER_COLOR = -0x919192
private const val DEFAULT_SLIDER_COLOR = -0x424243
private const val HUE_PANEL_WDITH_DP = 30
private const val ALPHA_PANEL_HEIGH_DP = 20
private const val PANEL_SPACING_DP = 10
private const val CIRCLE_TRACKER_RADIUS_DP = 5
private const val SLIDER_TRACKER_SIZE_DP = 4
private const val SLIDER_TRACKER_OFFSET_DP = 2
/**
* The width in pixels of the border
* surrounding all color panels.
*/
private const val BORDER_WIDTH_PX = 1
}
interface OnColorChangedListener {
fun onColorChanged(newColor: Int)
}
private class BitmapCache(
var canvas: Canvas? = null,
var bitmap: Bitmap? = null,
var value: Float = 0f,
)
/**
* The width in px of the hue panel.
*/
private val huePanelWidthPx =
DrawingUtils.dpToPx(context, HUE_PANEL_WDITH_DP.toFloat())
/**
* The height in px of the alpha panel
*/
private val alphaPanelHeightPx =
DrawingUtils.dpToPx(context, ALPHA_PANEL_HEIGH_DP.toFloat())
/**
* The distance in px between the different
* color panels.
*/
private val panelSpacingPx =
DrawingUtils.dpToPx(context, PANEL_SPACING_DP.toFloat())
/**
* The radius in px of the color palette tracker circle.
*/
private val circleTrackerRadiusPx =
DrawingUtils.dpToPx(context, CIRCLE_TRACKER_RADIUS_DP.toFloat())
/**
* The px which the tracker of the hue or alpha panel
* will extend outside of its bounds.
*/
private val sliderTrackerOffsetPx =
DrawingUtils.dpToPx(context, SLIDER_TRACKER_OFFSET_DP.toFloat())
/**
* Height of slider tracker on hue panel,
* width of slider on alpha panel.
*/
private val sliderTrackerSizePx =
DrawingUtils.dpToPx(context, SLIDER_TRACKER_SIZE_DP.toFloat())
/**
* the current value of the text that will be shown in the alpha slider.
* null to disable text.
*/
@Suppress("MemberVisibilityCanBePrivate")
var alphaSliderText: String? = null
set(value) {
if (field != value) {
field = value
invalidate()
}
}
// the color the view should show.
var color: Int
get() = Color.HSVToColor(alpha, floatArrayOf(hue, sat, bri))
set(color) {
setColor(color, false)
}
@ColorInt
var sliderTrackerColor = DEFAULT_SLIDER_COLOR
set(value) {
field = value
hueAlphaTrackerPaint.color = value
invalidate()
}
@ColorInt
var borderColor = DEFAULT_BORDER_COLOR
set(value) {
if (field != value) {
field = value
invalidate()
}
}
private val satValPaint = Paint()
private val satValTrackerPaint = Paint().apply {
style = Paint.Style.STROKE
strokeWidth = DrawingUtils.dpToPx(context, 2f).toFloat()
isAntiAlias = true
}
private val alphaPaint = Paint()
private val alphaTextPaint = Paint().apply {
color = -0xe3e3e4
textSize = DrawingUtils.dpToPx(context, 14f).toFloat()
isAntiAlias = true
textAlign = Align.CENTER
isFakeBoldText = true
}
private val hueAlphaTrackerPaint = Paint().apply {
color = sliderTrackerColor
style = Paint.Style.STROKE
strokeWidth = DrawingUtils.dpToPx(context, 2f).toFloat()
isAntiAlias = true
}
private val borderPaint = Paint()
private var valShader: Shader? = null
private var satShader: Shader? = null
private var alphaShader: Shader? = null
/*
* We cache a bitmap of the sat/val panel which is expensive to draw each time.
* We can reuse it when the user is sliding the circle picker as long as the hue isn't changed.
*/
private var satValBackgroundCache: BitmapCache? = null
/* We cache the hue background to since its also very expensive now. */
private var hueBackgroundCache: BitmapCache? = null
/* Current values */
private var alpha = 0xff
private var hue = 360f
private var sat = 0f
private var bri = 0f
private var showAlphaPanel = false
/**
* Minimum required padding. The offset from the
* edge we must have or else the finger tracker will
* get clipped when it's drawn outside of the view.
*/
private val mRequiredPadding =
context.resources.getDimensionPixelSize(R.dimen.cpv_required_padding)
/**
* The Rect in which we are allowed to draw.
* Trackers can extend outside slightly,
* due to the required padding we have set.
*/
private var drawingRect: Rect? = null
private var satValRect: Rect? = null
private var hueRect: Rect? = null
private var alphaRect: Rect? = null
private var startTouchPoint: Point? = null
private var alphaPatternDrawable: AlphaPatternDrawable? = null
/**
* OnColorChangedListener to get notified when the color selected by the user has changed.
*/
var onColorChangedListener: OnColorChangedListener? = null
public override fun onSaveInstanceState(): Parcelable {
val state = Bundle()
state.putParcelable("instanceState", super.onSaveInstanceState())
state.putInt("alpha", alpha)
state.putFloat("hue", hue)
state.putFloat("sat", sat)
state.putFloat("val", bri)
state.putBoolean("show_alpha", showAlphaPanel)
state.putString("alpha_text", alphaSliderText)
return state
}
public override fun onRestoreInstanceState(stateArg: Parcelable) {
var state: Parcelable? = stateArg
if (state is Bundle) {
val bundle = state
alpha = bundle.getInt("alpha")
hue = bundle.getFloat("hue")
sat = bundle.getFloat("sat")
bri = bundle.getFloat("val")
showAlphaPanel = bundle.getBoolean("show_alpha")
alphaSliderText = bundle.getString("alpha_text")
state = bundle.getParcelable("instanceState")
}
super.onRestoreInstanceState(state)
}
init {
//Load those if set in xml resource file.
var a = context.obtainStyledAttributes(attrs, R.styleable.ColorPickerView)
showAlphaPanel = a.getBoolean(R.styleable.ColorPickerView_cpv_alphaChannelVisible, false)
alphaSliderText = a.getString(R.styleable.ColorPickerView_cpv_alphaChannelText)
sliderTrackerColor = a.getColor(R.styleable.ColorPickerView_cpv_sliderColor, -0x424243)
borderColor = a.getColor(R.styleable.ColorPickerView_cpv_borderColor, -0x919192)
a.recycle()
// If no specific border/slider color has been
// set we take the default secondary text color
// as border/slider color. Thus it will adopt
// to theme changes automatically.
val value = TypedValue()
a = context.obtainStyledAttributes(
value.data,
intArrayOf(android.R.attr.textColorSecondary)
)
if (borderColor == DEFAULT_BORDER_COLOR) {
borderColor = a.getColor(0, DEFAULT_BORDER_COLOR)
}
if (sliderTrackerColor == DEFAULT_SLIDER_COLOR) {
sliderTrackerColor = a.getColor(0, DEFAULT_SLIDER_COLOR)
}
a.recycle()
//Needed for receiving trackball motion events.
isFocusable = true
isFocusableInTouchMode = true
}
override fun onDraw(canvas: Canvas) {
val drawingRect = this.drawingRect
if (drawingRect == null || drawingRect.width() <= 0 || drawingRect.height() <= 0) {
return
}
drawSatValPanel(canvas)
drawHuePanel(canvas)
drawAlphaPanel(canvas)
}
private fun drawSatValPanel(canvas: Canvas) {
val rect = this.satValRect ?: return
val drawingRect = this.drawingRect ?: return
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
drawingRect.left.toFloat(),
drawingRect.top.toFloat(),
(rect.right + BORDER_WIDTH_PX).toFloat(),
(rect.bottom + BORDER_WIDTH_PX).toFloat(), borderPaint
)
}
if (valShader == null) {
//Black gradient has either not been created or the view has been resized.
valShader = LinearGradient(
rect.left.toFloat(),
rect.top.toFloat(),
rect.left.toFloat(),
rect.bottom.toFloat(),
-0x1,
-0x1000000,
TileMode.CLAMP
)
}
//If the hue has changed we need to recreate the cache.
if (satValBackgroundCache == null || satValBackgroundCache!!.value != hue) {
if (satValBackgroundCache == null) {
satValBackgroundCache = BitmapCache()
}
//We create our bitmap in the cache if it doesn't exist.
if (satValBackgroundCache!!.bitmap == null) {
satValBackgroundCache!!.bitmap = Bitmap
.createBitmap(rect.width(), rect.height(), Bitmap.Config.ARGB_8888)
}
//We create the canvas once so we can draw on our bitmap and the hold on to it.
if (satValBackgroundCache!!.canvas == null) {
satValBackgroundCache!!.canvas = Canvas(satValBackgroundCache!!.bitmap!!)
}
val rgb = Color.HSVToColor(floatArrayOf(hue, 1f, 1f))
satShader = LinearGradient(
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.top.toFloat(),
-0x1,
rgb,
TileMode.CLAMP
)
satValPaint.shader = ComposeShader(
valShader!!,
satShader!!,
PorterDuff.Mode.MULTIPLY
)
// Finally we draw on our canvas, the result will be
// stored in our bitmap which is already in the cache.
// Since this is drawn on a canvas not rendered on
// screen it will automatically not be using the
// hardware acceleration. And this was the code that
// wasn't supported by hardware acceleration which mean
// there is no need to turn it of anymore. The rest of
// the view will still be hw accelerated.
satValBackgroundCache!!.canvas!!.drawRect(
0f, 0f,
satValBackgroundCache!!.bitmap!!.width.toFloat(),
satValBackgroundCache!!.bitmap!!.height.toFloat(),
satValPaint
)
//We set the hue value in our cache to which hue it was drawn with,
//then we know that if it hasn't changed we can reuse our cached bitmap.
satValBackgroundCache!!.value = hue
}
// We draw our bitmap from the cached, if the hue has changed
// then it was just recreated otherwise the old one will be used.
canvas.drawBitmap(satValBackgroundCache!!.bitmap!!, null, rect, null)
val p = satValToPoint(sat, bri)
satValTrackerPaint.color = -0x1000000
canvas.drawCircle(
p.x.toFloat(), p.y.toFloat(), (circleTrackerRadiusPx - DrawingUtils.dpToPx(
context, 1f
)).toFloat(), satValTrackerPaint
)
satValTrackerPaint.color = -0x222223
canvas.drawCircle(
p.x.toFloat(),
p.y.toFloat(),
circleTrackerRadiusPx.toFloat(),
satValTrackerPaint
)
}
private fun drawHuePanel(canvas: Canvas) {
val rect = hueRect
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
(rect!!.left - BORDER_WIDTH_PX).toFloat(), (
rect.top - BORDER_WIDTH_PX).toFloat(), (
rect.right + BORDER_WIDTH_PX).toFloat(), (
rect.bottom + BORDER_WIDTH_PX).toFloat(),
borderPaint
)
}
if (hueBackgroundCache == null) {
val hueBackgroundCache = BitmapCache()
.also { this.hueBackgroundCache = it }
Bitmap.createBitmap(rect!!.width(), rect.height(), Bitmap.Config.ARGB_8888)
.also {
hueBackgroundCache.bitmap = it
hueBackgroundCache.canvas = Canvas(it)
}
val hueColors = IntArray((rect.height() + 0.5f).toInt())
// Generate array of all colors, will be drawn as individual lines.
var h = 360f
for (i in hueColors.indices) {
hueColors[i] = Color.HSVToColor(floatArrayOf(h, 1f, 1f))
h -= 360f / hueColors.size
}
// Time to draw the hue color gradient,
// its drawn as individual lines which
// will be quite many when the resolution is high
// and/or the panel is large.
val linePaint = Paint()
linePaint.strokeWidth = 0f
for (i in hueColors.indices) {
linePaint.color = hueColors[i]
hueBackgroundCache.canvas?.drawLine(
0f,
i.toFloat(),
hueBackgroundCache.bitmap?.width?.toFloat() ?: 0f,
i.toFloat(),
linePaint
)
}
}
canvas.drawBitmap(hueBackgroundCache!!.bitmap!!, null, rect!!, null)
val p = hueToPoint(hue)
val r = RectF()
r.left = (rect.left - sliderTrackerOffsetPx).toFloat()
r.right = (rect.right + sliderTrackerOffsetPx).toFloat()
r.top = p.y - sliderTrackerSizePx / 2f
r.bottom = p.y + sliderTrackerSizePx / 2f
canvas.drawRoundRect(r, 2f, 2f, hueAlphaTrackerPaint)
}
private fun drawAlphaPanel(canvas: Canvas) {
if (!showAlphaPanel) return
val rect = this.alphaRect ?: return
val alphaPatternDrawable = this.alphaPatternDrawable ?: return
/*
* Will be drawn with hw acceleration, very fast.
* Also the AlphaPatternDrawable is backed by a bitmap
* generated only once if the size does not change.
*/
if (BORDER_WIDTH_PX > 0) {
borderPaint.color = borderColor
canvas.drawRect(
(rect.left - BORDER_WIDTH_PX).toFloat(), (
rect.top - BORDER_WIDTH_PX).toFloat(), (
rect.right + BORDER_WIDTH_PX).toFloat(), (
rect.bottom + BORDER_WIDTH_PX).toFloat(),
borderPaint
)
}
alphaPatternDrawable.draw(canvas)
val hsv = floatArrayOf(hue, sat, bri)
val color = Color.HSVToColor(hsv)
val acolor = Color.HSVToColor(0, hsv)
alphaShader = LinearGradient(
rect.left.toFloat(),
rect.top.toFloat(),
rect.right.toFloat(),
rect.top.toFloat(),
color, acolor, TileMode.CLAMP
)
alphaPaint.shader = alphaShader
canvas.drawRect(rect, alphaPaint)
alphaSliderText
?.takeIf { it.isNotEmpty() }
?.let {
canvas.drawText(
it,
rect.centerX().toFloat(),
(rect.centerY() + DrawingUtils.dpToPx(
context, 4f
)).toFloat(),
alphaTextPaint
)
}
val p = alphaToPoint(alpha)
val r = RectF()
r.left = p.x - sliderTrackerSizePx / 2f
r.right = p.x + sliderTrackerSizePx / 2f
r.top = (rect.top - sliderTrackerOffsetPx).toFloat()
r.bottom = (rect.bottom + sliderTrackerOffsetPx).toFloat()
canvas.drawRoundRect(r, 2f, 2f, hueAlphaTrackerPaint)
}
private fun hueToPoint(hue: Float): Point {
val rect = hueRect
val height = rect!!.height().toFloat()
val p = Point()
p.y = (height - hue * height / 360f + rect.top).toInt()
p.x = rect.left
return p
}
private fun satValToPoint(sat: Float, `val`: Float): Point {
val rect = satValRect
val height = rect!!.height().toFloat()
val width = rect.width().toFloat()
val p = Point()
p.x = (sat * width + rect.left).toInt()
p.y = ((1f - `val`) * height + rect.top).toInt()
return p
}
private fun alphaToPoint(alpha: Int): Point {
val rect = alphaRect
val width = rect!!.width().toFloat()
val p = Point()
p.x = (width - alpha * width / 0xff + rect.left).toInt()
p.y = rect.top
return p
}
private fun pointToSatVal(xArg: Float, yArg: Float): FloatArray {
var x = xArg
var y = yArg
val rect = satValRect
val result = FloatArray(2)
val width = rect!!.width().toFloat()
val height = rect.height().toFloat()
x = when {
x < rect.left -> 0f
x > rect.right -> width
else -> x - rect.left
}
y = when {
y < rect.top -> 0f
y > rect.bottom -> height
else -> y - rect.top
}
result[0] = 1f / width * x
result[1] = 1f - 1f / height * y
return result
}
private fun pointToHue(yArg: Float): Float {
var y = yArg
val rect = hueRect
val height = rect!!.height().toFloat()
y = when {
y < rect.top -> 0f
y > rect.bottom -> height
else -> y - rect.top
}
return 360f - y * 360f / height
}
private fun pointToAlpha(xArg: Int): Int {
var x = xArg
val rect = alphaRect
val width = rect!!.width()
x = when {
x < rect.left -> 0
x > rect.right -> width
else -> x - rect.left
}
return 0xff - x * 0xff / width
}
@SuppressLint("ClickableViewAccessibility")
override fun onTouchEvent(event: MotionEvent): Boolean {
try {
this.parent.requestDisallowInterceptTouchEvent(true)
} catch (ignored: Throwable) {
}
var update = false
when (event.action) {
MotionEvent.ACTION_DOWN -> {
startTouchPoint = Point(event.x.toInt(), event.y.toInt())
update = moveTrackersIfNeeded(event)
}
MotionEvent.ACTION_MOVE -> update = moveTrackersIfNeeded(event)
MotionEvent.ACTION_UP -> {
startTouchPoint = null
update = moveTrackersIfNeeded(event)
}
}
if (update) {
onColorChangedListener?.onColorChanged(
Color.HSVToColor(
alpha,
floatArrayOf(hue, sat, bri)
)
)
invalidate()
return true
}
return super.onTouchEvent(event)
}
private fun moveTrackersIfNeeded(event: MotionEvent): Boolean {
val startTouchPoint = this.startTouchPoint ?: return false
val startX = startTouchPoint.x
val startY = startTouchPoint.y
return when {
hueRect?.contains(startX, startY) == true -> {
hue = pointToHue(event.y)
true
}
satValRect?.contains(startX, startY) == true -> {
val result = pointToSatVal(event.x, event.y)
sat = result[0]
bri = result[1]
true
}
alphaRect?.contains(startX, startY) == true -> {
alpha = pointToAlpha(event.x.toInt())
true
}
else -> false
}
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val finalWidth: Int
val finalHeight: Int
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
val widthAllowed = MeasureSpec.getSize(widthMeasureSpec) - paddingLeft - paddingRight
val heightAllowed = MeasureSpec.getSize(heightMeasureSpec) - paddingBottom - paddingTop
if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) {
//A exact value has been set in either direction, we need to stay within this size.
if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) {
//The with has been specified exactly, we need to adopt the height to fit.
var h = widthAllowed - panelSpacingPx - huePanelWidthPx
if (showAlphaPanel) {
h += panelSpacingPx + alphaPanelHeightPx
}
//We can't fit the view in this container, set the size to whatever was allowed.
finalHeight = min(h, heightAllowed)
finalWidth = widthAllowed
} else if (widthMode != MeasureSpec.EXACTLY) {
//The height has been specified exactly, we need to stay within this height and adopt the width.
var w = heightAllowed + panelSpacingPx + huePanelWidthPx
if (showAlphaPanel) {
w -= panelSpacingPx + alphaPanelHeightPx
}
//we can't fit within this container, set the size to whatever was allowed.
finalWidth = min(w, widthAllowed)
finalHeight = heightAllowed
} else {
//If we get here the dev has set the width and height to exact sizes. For example match_parent or 300dp.
//This will mean that the sat/val panel will not be square but it doesn't matter. It will work anyway.
//In all other senarios our goal is to make that panel square.
//We set the sizes to exactly what we were told.
finalWidth = widthAllowed
finalHeight = heightAllowed
}
} else {
//If no exact size has been set we try to make our view as big as possible
//within the allowed space.
//Calculate the needed width to layout using max allowed height.
var widthNeeded = heightAllowed + panelSpacingPx + huePanelWidthPx
//Calculate the needed height to layout using max allowed width.
var heightNeeded = widthAllowed - panelSpacingPx - huePanelWidthPx
if (showAlphaPanel) {
widthNeeded -= panelSpacingPx + alphaPanelHeightPx
heightNeeded += panelSpacingPx + alphaPanelHeightPx
}
val widthOk = widthNeeded <= widthAllowed
val heightOk = heightNeeded <= heightAllowed
when {
widthOk && heightOk -> {
finalWidth = widthAllowed
finalHeight = heightNeeded
}
widthOk -> {
finalHeight = heightAllowed
finalWidth = widthNeeded
}
heightOk -> {
finalHeight = heightNeeded
finalWidth = widthAllowed
}
else -> {
finalHeight = heightAllowed
finalWidth = widthAllowed
}
}
}
setMeasuredDimension(
finalWidth + paddingLeft + paddingRight,
finalHeight + paddingTop + paddingBottom
)
}
override fun getPaddingTop(): Int =
max(super.getPaddingTop(), mRequiredPadding)
override fun getPaddingBottom(): Int =
max(super.getPaddingBottom(), mRequiredPadding)
override fun getPaddingLeft(): Int =
max(super.getPaddingLeft(), mRequiredPadding)
override fun getPaddingRight(): Int =
max(super.getPaddingRight(), mRequiredPadding)
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
super.onSizeChanged(w, h, oldw, oldh)
val drawingRect = Rect().also { this.drawingRect = it }
drawingRect.left = paddingLeft
drawingRect.right = w - paddingRight
drawingRect.top = paddingTop
drawingRect.bottom = h - paddingBottom
//The need to be recreated because they depend on the size of the view.
valShader = null
satShader = null
alphaShader = null
// Clear those bitmap caches since the size may have changed.
satValBackgroundCache = null
hueBackgroundCache = null
setUpSatValRect()
setUpHueRect()
setUpAlphaRect()
}
private fun setUpSatValRect() {
//Calculate the size for the big color rectangle.
val dRect = drawingRect!!
val left = dRect.left + BORDER_WIDTH_PX
val top = dRect.top + BORDER_WIDTH_PX
var bottom = dRect.bottom - BORDER_WIDTH_PX
val right = dRect.right - BORDER_WIDTH_PX - panelSpacingPx - huePanelWidthPx
if (showAlphaPanel) {
bottom -= alphaPanelHeightPx + panelSpacingPx
}
satValRect = Rect(left, top, right, bottom)
}
private fun setUpHueRect() {
//Calculate the size for the hue slider on the left.
val dRect = drawingRect!!
val left = dRect.right - huePanelWidthPx + BORDER_WIDTH_PX
val top = dRect.top + BORDER_WIDTH_PX
val bottom = dRect.bottom - BORDER_WIDTH_PX -
if (showAlphaPanel) panelSpacingPx + alphaPanelHeightPx else 0
val right = dRect.right - BORDER_WIDTH_PX
hueRect = Rect(left, top, right, bottom)
}
private fun setUpAlphaRect() {
if (!showAlphaPanel) return
val dRect = drawingRect ?: return
val left = dRect.left + BORDER_WIDTH_PX
val top = dRect.bottom - alphaPanelHeightPx + BORDER_WIDTH_PX
val bottom = dRect.bottom - BORDER_WIDTH_PX
val right = dRect.right - BORDER_WIDTH_PX
val alphaRect = Rect(left, top, right, bottom)
.also { this.alphaRect = it }
alphaPatternDrawable = AlphaPatternDrawable(DrawingUtils.dpToPx(context, 4f))
.apply {
setBounds(
alphaRect.left,
alphaRect.top,
alphaRect.right,
alphaRect.bottom
)
}
}
/**
* Set the color this view should show.
*
* @param color The color that should be selected. #argb
* @param callback If you want to get a callback to your OnColorChangedListener.
*/
fun setColor(color: Int, callback: Boolean) {
val alpha = Color.alpha(color)
val red = Color.red(color)
val blue = Color.blue(color)
val green = Color.green(color)
val hsv = FloatArray(3)
Color.RGBToHSV(red, green, blue, hsv)
this.alpha = alpha
hue = hsv[0]
sat = hsv[1]
bri = hsv[2]
if (callback) {
onColorChangedListener
?.onColorChanged(Color.HSVToColor(this.alpha, floatArrayOf(hue, sat, bri)))
}
invalidate()
}
/**
* Set if the user is allowed to adjust the alpha panel. Default is false.
* If it is set to false no alpha will be set.
*
* @param visible `true` to show the alpha slider
*/
fun setAlphaSliderVisible(visible: Boolean) {
if (showAlphaPanel != visible) {
showAlphaPanel = visible
/*
* Force recreation.
*/
valShader = null
satShader = null
alphaShader = null
hueBackgroundCache = null
satValBackgroundCache = null
requestLayout()
}
}
/**
* Set the text that should be shown in the
* alpha slider. Set to null to disable text.
*
* @param res string resource id.
*/
fun setAlphaSliderText(res: Int) {
alphaSliderText = context.getString(res)
}
}

View File

@ -1,232 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.preference.Preference;
import android.util.AttributeSet;
import android.view.View;
import com.jrummyapps.android.colorpicker.ColorPickerDialog.DialogType;
import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.fragment.app.FragmentActivity;
import androidx.fragment.app.FragmentManager;
/**
* A Preference to select a color
*/
public class ColorPreference extends Preference implements ColorPickerDialogListener {
private static final int SIZE_NORMAL = 0;
private static final int SIZE_LARGE = 1;
private OnShowDialogListener onShowDialogListener;
private int color = Color.BLACK;
private boolean showDialog;
@DialogType
private int dialogType;
private int colorShape;
private boolean allowPresets;
private boolean allowCustom;
private boolean showAlphaSlider;
private boolean showColorShades;
private int previewSize;
private int[] presets;
private int dialogTitle;
public ColorPreference(Context context, AttributeSet attrs) {
super(context, attrs);
init(attrs);
}
public ColorPreference(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
init(attrs);
}
private void init(AttributeSet attrs) {
setPersistent(true);
TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPreference);
showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true);
//noinspection WrongConstant
dialogType = a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS);
colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE);
allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true);
allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true);
showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false);
showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true);
previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL);
final int presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0);
dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title);
if (presetsResId != 0) {
presets = getContext().getResources().getIntArray(presetsResId);
} else {
presets = ColorPickerDialog.MATERIAL_COLORS;
}
if (colorShape == ColorShape.CIRCLE) {
setWidgetLayoutResource(
previewSize == SIZE_LARGE ? R.layout.cpv_preference_circle_large : R.layout.cpv_preference_circle);
} else {
setWidgetLayoutResource(
previewSize == SIZE_LARGE ? R.layout.cpv_preference_square_large : R.layout.cpv_preference_square
);
}
a.recycle();
}
@Override
protected void onClick() {
super.onClick();
if (onShowDialogListener != null) {
onShowDialogListener.onShowColorPickerDialog((String) getTitle(), color);
} else if (showDialog) {
ColorPickerDialog dialog = ColorPickerDialog.newBuilder()
.setDialogType(dialogType)
.setDialogTitle(dialogTitle)
.setColorShape(colorShape)
.setPresets(presets)
.setAllowPresets(allowPresets)
.setAllowCustom(allowCustom)
.setShowAlphaSlider(showAlphaSlider)
.setShowColorShades(showColorShades)
.setColor(color)
.create();
dialog.setColorPickerDialogListener(ColorPreference.this);
FragmentManager fm = getFragmentManager();
if (fm != null) {
dialog.show(fm, getFragmentTag());
}
}
}
@Nullable
private FragmentManager getFragmentManager() {
Context context = getContext();
if (context instanceof FragmentActivity) {
return ((FragmentActivity) context).getSupportFragmentManager();
}
return null;
}
@Override
protected void onAttachedToActivity() {
super.onAttachedToActivity();
FragmentManager fm = getFragmentManager();
if (showDialog && fm != null) {
ColorPickerDialog fragment = (ColorPickerDialog) fm.findFragmentByTag(getFragmentTag());
if (fragment != null) {
// re-bind preference to fragment
fragment.setColorPickerDialogListener(this);
}
}
}
@Override
protected void onBindView(View view) {
super.onBindView(view);
ColorPanelView preview = view.findViewById(R.id.cpv_preference_preview_color_panel);
if (preview != null) {
preview.setColor(color);
}
}
@Override
protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) {
if (restorePersistedValue) {
color = getPersistedInt(0xFF000000);
} else {
color = (Integer) defaultValue;
persistInt(color);
}
}
@Override
protected Object onGetDefaultValue(TypedArray a, int index) {
return a.getInteger(index, Color.BLACK);
}
@Override
public void onColorSelected(int dialogId, @ColorInt int color) {
saveValue(color);
}
@Override
public void onDialogDismissed(int dialogId) {
// no-op
}
/**
* Set the new color
*
* @param color The newly selected color
*/
public void saveValue(@ColorInt int color) {
this.color = color;
persistInt(this.color);
notifyChanged();
callChangeListener(color);
}
/**
* Set the colors shown in the {@link ColorPickerDialog}.
*
* @param presets An array of color ints
*/
public void setPresets(@NonNull int[] presets) {
this.presets = presets;
}
/**
* Get the colors that will be shown in the {@link ColorPickerDialog}.
*
* @return An array of color ints
*/
public int[] getPresets() {
return presets;
}
/**
* The listener used for showing the {@link ColorPickerDialog}.
* Call {@link #saveValue(int)} after the user chooses a color.
* If this is set then it is up to you to show the dialog.
*
* @param listener The listener to show the dialog
*/
public void setOnShowDialogListener(OnShowDialogListener listener) {
onShowDialogListener = listener;
}
/**
* The tag used for the {@link ColorPickerDialog}.
*
* @return The tag
*/
public String getFragmentTag() {
return "color_" + getKey();
}
public interface OnShowDialogListener {
void onShowColorPickerDialog(String title, int currentColor);
}
}

View File

@ -0,0 +1,201 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
@file:Suppress("DEPRECATION")
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.content.res.TypedArray
import android.graphics.Color
import android.preference.Preference
import android.util.AttributeSet
import android.view.View
import androidx.annotation.ColorInt
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import com.jrummyapps.android.colorpicker.ColorPickerDialog.Companion.newBuilder
/**
* A Preference to select a color
*/
class ColorPreference : Preference, ColorPickerDialogListener {
companion object {
private const val SIZE_NORMAL = 0
private const val SIZE_LARGE = 1
}
interface OnShowDialogListener {
fun onShowColorPickerDialog(title: String?, currentColor: Int)
}
private var onShowDialogListener: OnShowDialogListener? = null
private var color = Color.BLACK
private var showDialog = false
@ColorPickerDialog.DialogType
private var dialogType = 0
private var colorShape = 0
private var allowPresets = false
private var allowCustom = false
private var showAlphaSlider = false
private var showColorShades = false
private var previewSize = 0
// An array of color ints
var presets: IntArray = ColorPickerDialog.MATERIAL_COLORS
private var dialogTitle = 0
/**
* The tag used for the [ColorPickerDialog].
*/
private val fragmentTag: String
get() = "color_$key"
constructor(context: Context?, attrs: AttributeSet) : super(context, attrs) {
init(attrs)
}
constructor(context: Context?, attrs: AttributeSet, defStyle: Int) : super(
context,
attrs,
defStyle
) {
init(attrs)
}
private fun init(attrs: AttributeSet) {
isPersistent = true
val a = context.obtainStyledAttributes(attrs, R.styleable.ColorPreference)
showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true)
dialogType =
a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS)
colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE)
allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true)
allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true)
showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false)
showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true)
previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL)
val presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0)
dialogTitle =
a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title)
presets = if (presetsResId != 0) {
context.resources.getIntArray(presetsResId)
} else {
ColorPickerDialog.MATERIAL_COLORS
}
widgetLayoutResource = if (colorShape == ColorShape.CIRCLE) {
if (previewSize == SIZE_LARGE) R.layout.cpv_preference_circle_large else R.layout.cpv_preference_circle
} else {
if (previewSize == SIZE_LARGE) R.layout.cpv_preference_square_large else R.layout.cpv_preference_square
}
a.recycle()
}
override fun onClick() {
super.onClick()
if (onShowDialogListener != null) {
onShowDialogListener!!.onShowColorPickerDialog(title as String, color)
} else if (showDialog) {
val dialog = newBuilder()
.setDialogType(dialogType)
.setDialogTitle(dialogTitle)
.setColorShape(colorShape)
.setPresets(presets)
.setAllowPresets(allowPresets)
.setAllowCustom(allowCustom)
.setShowAlphaSlider(showAlphaSlider)
.setShowColorShades(showColorShades)
.setColor(color)
.create()
dialog.colorPickerDialogListener = this@ColorPreference
val fm = fragmentManager
if (fm != null) {
dialog.show(fm, fragmentTag)
}
}
}
private val fragmentManager: FragmentManager?
get() {
val context = context
return if (context is FragmentActivity) {
context.supportFragmentManager
} else null
}
override fun onAttachedToActivity() {
super.onAttachedToActivity()
val fm = fragmentManager
if (showDialog && fm != null) {
val fragment = fm.findFragmentByTag(fragmentTag) as ColorPickerDialog?
if (fragment != null) {
// re-bind preference to fragment
fragment.colorPickerDialogListener = this
}
}
}
override fun onBindView(view: View) {
super.onBindView(view)
val preview: ColorPanelView = view.findViewById(R.id.cpv_preference_preview_color_panel)
preview.color = color
}
override fun onSetInitialValue(restorePersistedValue: Boolean, defaultValue: Any) {
if (restorePersistedValue) {
color = getPersistedInt(-0x1000000)
} else {
color = defaultValue as Int
persistInt(color)
}
}
override fun onGetDefaultValue(a: TypedArray, index: Int): Any {
return a.getInteger(index, Color.BLACK)
}
override fun onColorSelected(dialogId: Int, @ColorInt color: Int) {
saveValue(color)
}
override fun onDialogDismissed(dialogId: Int) {
// no-op
}
/**
* Set the new color
*
* @param color The newly selected color
*/
private fun saveValue(@ColorInt color: Int) {
this.color = color
persistInt(this.color)
notifyChanged()
callChangeListener(color)
}
/**
* The listener used for showing the [ColorPickerDialog].
* Call [.saveValue] after the user chooses a color.
* If this is set then it is up to you to show the dialog.
*
* @param listener The listener to show the dialog
*/
fun setOnShowDialogListener(listener: OnShowDialogListener?) {
onShowDialogListener = listener
}
}

View File

@ -13,23 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
package com.jrummyapps.android.colorpicker;
import androidx.annotation.IntDef;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import androidx.annotation.IntDef
/**
* The shape of the color preview
*/
@Retention(RetentionPolicy.SOURCE)
@IntDef({ColorShape.SQUARE, ColorShape.CIRCLE})
public @interface ColorShape {
int SQUARE = 0;
int CIRCLE = 1;
@Retention(AnnotationRetention.SOURCE)
@IntDef(ColorShape.SQUARE, ColorShape.CIRCLE)
annotation class ColorShape {
companion object {
const val SQUARE = 0
const val CIRCLE = 1
}
}

View File

@ -0,0 +1,27 @@
package com.jrummyapps.android.colorpicker
import android.graphics.Color
@Throws(NumberFormatException::class)
fun parseColorString(src: String): Int {
val start = if (src.startsWith("#")) 1 else 0
fun c1(offset: Int) =
src.substring(start + offset, start + offset + 1).toInt(16) * 0x11
fun c2(offset: Int) =
src.substring(start + offset, start + offset + 2).toInt(16)
return when (src.length - start) {
0 -> Color.BLACK
1 -> Color.argb(255, 0, 0, c1(0))
2 -> Color.argb(255, 0, 0, c2(0))
3 -> Color.argb(255, c1(0), c1(1), c1(2))
4 -> Color.argb(c1(0), c1(1), c1(2), c1(3))
5 -> Color.argb(255, c2(0), c2(2), c1(4))
6 -> Color.argb(255, c2(0), c2(2), c2(4))
7 -> Color.argb(c2(0), c2(2), c2(4), c1(6))
8 -> Color.argb(c2(0), c2(2), c2(4), c2(6))
else -> Color.WHITE
}
}

View File

@ -13,21 +13,17 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.jrummyapps.android.colorpicker
package com.jrummyapps.android.colorpicker;
import android.content.Context;
import android.util.DisplayMetrics;
import android.util.TypedValue;
final class DrawingUtils {
static int dpToPx(Context c, float dipValue) {
DisplayMetrics metrics = c.getResources().getDisplayMetrics();
float val = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics);
int res = (int) (val + 0.5); // Round
// Ensure at least 1 pixel if val was > 0
return res == 0 && val > 0 ? 1 : res;
}
import android.content.Context
import android.util.TypedValue
internal object DrawingUtils {
fun dpToPx(c: Context, dipValue: Float): Int {
val metrics = c.resources.displayMetrics
val v = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics)
val res = (v + 0.5).toInt() // Round
// Ensure at least 1 pixel if val was > 0
return if (res == 0 && v > 0) 1 else res
}
}

View File

@ -1,44 +0,0 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker;
import android.content.Context;
import androidx.annotation.RestrictTo;
import android.util.AttributeSet;
import android.widget.GridView;
@RestrictTo(RestrictTo.Scope.LIBRARY)
public class NestedGridView extends GridView {
public NestedGridView(Context context) {
super(context);
}
public NestedGridView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public NestedGridView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
@Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, expandSpec);
}
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2017 JRummy Apps Inc.
*
* 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.
*/
package com.jrummyapps.android.colorpicker
import android.content.Context
import android.util.AttributeSet
import android.widget.GridView
class NestedGridView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyle: Int = android.R.attr.gridViewStyle,
defStyleRes: Int = 0,
) : GridView(context, attrs, defStyle, defStyleRes) {
public override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
val expandSpec = MeasureSpec.makeMeasureSpec(Int.MAX_VALUE shr 2, MeasureSpec.AT_MOST)
super.onMeasure(widthMeasureSpec, expandSpec)
}
}

View File

@ -2,7 +2,6 @@ package jp.juggler.apng.sample
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.SystemClock
import android.util.Log
@ -50,21 +49,20 @@ class ActList : AppCompatActivity(), CoroutineScope {
listView.onItemClickListener = listAdapter
timeAnimationStart = SystemClock.elapsedRealtime()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
// Assume thisActivity is the current activity
if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
// Assume thisActivity is the current activity
if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(
this,
Manifest.permission.WRITE_EXTERNAL_STORAGE
)
) {
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_CODE_STORAGE
)
}
PERMISSION_REQUEST_CODE_STORAGE
)
}
load()
}
@ -78,8 +76,8 @@ class ActList : AppCompatActivity(), CoroutineScope {
permissions: Array<String>,
grantResults: IntArray
) {
if( requestCode == PERMISSION_REQUEST_CODE_STORAGE){
if (grantResults.all{ it == PackageManager.PERMISSION_GRANTED }) {
if (requestCode == PERMISSION_REQUEST_CODE_STORAGE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
// 特に何もしてないらしい
}
}