From 5ed8383f433bdf7e04aa565f96f5cd60d1e93adb Mon Sep 17 00:00:00 2001 From: tateisu Date: Sat, 20 Nov 2021 21:16:56 +0900 Subject: [PATCH] =?UTF-8?q?colorpicker=E3=81=AEkotlin=E5=8C=96=E3=81=AA?= =?UTF-8?q?=E3=81=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 4 - .../juggler/subwaytooter/TestColorString.kt | 29 + .../subwaytooter/database/TestDatabase.kt | 8 +- app/src/main/AndroidManifest.xml | 10 +- .../jp/juggler/subwaytooter/ActAppSetting.kt | 10 +- .../subwaytooter/ActColumnCustomize.kt | 36 +- .../subwaytooter/ActHighlightWordEdit.kt | 6 +- .../java/jp/juggler/subwaytooter/ActMain.kt | 7 +- .../jp/juggler/subwaytooter/ActNickname.kt | 6 +- .../subwaytooter/actmain/ActMainColumns.kt | 2 + .../ColumnViewHolderAnnouncements.kt | 7 +- .../ColumnViewHolderLifecycle.kt | 5 +- .../columnviewholder/ItemListAdapter.kt | 2 + .../subwaytooter/global/AppDatabaseHolder.kt | 10 +- .../itemviewholder/ItemViewHolder.kt | 1 - .../itemviewholder/ItemViewHolderActions.kt | 1 - .../juggler/subwaytooter/mfm/SpanOutputEnv.kt | 3 +- .../jp/juggler/subwaytooter/pref/PrefB.kt | 1 - .../jp/juggler/subwaytooter/pref/PrefI.kt | 3 +- .../juggler/subwaytooter/span/SvgEmojiSpan.kt | 1 - .../juggler/subwaytooter/util/HTMLDecoder.kt | 159 ++- .../subwaytooter/util/ProgressResponseBody.kt | 18 +- app/src/main/java/jp/juggler/util/Json.kt | 2 +- .../me/drakeet/support/toast/ToastCompat.kt | 1 + colorpicker/build.gradle | 14 +- .../colorpicker/AlphaPatternDrawable.java | 118 --- .../colorpicker/AlphaPatternDrawable.kt | 95 ++ .../colorpicker/ColorPaletteAdapter.java | 148 --- .../colorpicker/ColorPaletteAdapter.kt | 109 ++ .../android/colorpicker/ColorPanelView.java | 324 ------ .../android/colorpicker/ColorPanelView.kt | 286 +++++ .../colorpicker/ColorPickerDialog.java | 906 ---------------- .../android/colorpicker/ColorPickerDialog.kt | 760 ++++++++++++++ ...ener.java => ColorPickerDialogListener.kt} | 41 +- .../android/colorpicker/ColorPickerView.java | 982 ------------------ .../android/colorpicker/ColorPickerView.kt | 840 +++++++++++++++ .../android/colorpicker/ColorPreference.java | 232 ----- .../android/colorpicker/ColorPreference.kt | 201 ++++ .../{ColorShape.java => ColorShape.kt} | 23 +- .../android/colorpicker/ColorString.kt | 27 + .../{DrawingUtils.java => DrawingUtils.kt} | 26 +- .../android/colorpicker/NestedGridView.java | 44 - .../android/colorpicker/NestedGridView.kt | 33 + .../java/jp/juggler/apng/sample/ActList.kt | 30 +- 44 files changed, 2650 insertions(+), 2921 deletions(-) create mode 100644 app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt rename colorpicker/src/main/java/com/jrummyapps/android/colorpicker/{ColorPickerDialogListener.java => ColorPickerDialogListener.kt} (52%) delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.kt rename colorpicker/src/main/java/com/jrummyapps/android/colorpicker/{ColorShape.java => ColorShape.kt} (67%) create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorString.kt rename colorpicker/src/main/java/com/jrummyapps/android/colorpicker/{DrawingUtils.java => DrawingUtils.kt} (54%) delete mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.java create mode 100644 colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.kt diff --git a/.gitignore b/.gitignore index fd5167b8..d8bec687 100644 --- a/.gitignore +++ b/.gitignore @@ -91,12 +91,8 @@ lint/tmp/ *.hprof ##################################### -*.apk -*.iml -*.log .DS_Store -.externalNativeBuild .gradle /.idea/assetWizardSettings.xml diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt new file mode 100644 index 00000000..00f20f07 --- /dev/null +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestColorString.kt @@ -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) + } +} diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt index 4cc41007..118cff9b 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/database/TestDatabase.kt @@ -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 diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 848de8ca..5baccd61 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -366,6 +366,14 @@ + + + - diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt index 7dde45dd..c2fe91b3 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActAppSetting.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt index 83ab0690..e940b431 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt @@ -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) {} diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt index 045f356f..44d7435d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActHighlightWordEdit.kt @@ -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() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt index 55dc0235..2814e99a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActMain.kt @@ -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 ////////////////////////////////////////////////////////////////// // 読み取り専用のプロパティ diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt b/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt index bc77bf73..d5bd0e90 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActNickname.kt @@ -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() } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt index 20d8ea9f..5c6e80db 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actmain/ActMainColumns.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderAnnouncements.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderAnnouncements.kt index faa487bb..cffb0b87 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderAnnouncements.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderAnnouncements.kt @@ -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.* diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt index 2f1f978d..eec48446 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolderLifecycle.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ItemListAdapter.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ItemListAdapter.kt index aad12477..d7cfa96c 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ItemListAdapter.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ItemListAdapter.kt @@ -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? = null, diff --git a/app/src/main/java/jp/juggler/subwaytooter/global/AppDatabaseHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/global/AppDatabaseHolder.kt index 9f8a58e6..c0987d92 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/global/AppDatabaseHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/global/AppDatabaseHolder.kt @@ -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) } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt index 24846729..65807cb8 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderActions.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderActions.kt index 3afa6940..7e985388 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderActions.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderActions.kt @@ -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.* diff --git a/app/src/main/java/jp/juggler/subwaytooter/mfm/SpanOutputEnv.kt b/app/src/main/java/jp/juggler/subwaytooter/mfm/SpanOutputEnv.kt index a72ccd9d..4e7d0867 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/mfm/SpanOutputEnv.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/mfm/SpanOutputEnv.kt @@ -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 diff --git a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt index 987c8032..fa8378de 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefB.kt @@ -1,6 +1,5 @@ package jp.juggler.subwaytooter.pref -import android.os.Build import jp.juggler.subwaytooter.pref.impl.BooleanPref object PrefB { diff --git a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefI.kt b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefI.kt index acf78de0..1654ff8f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/pref/PrefI.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/pref/PrefI.kt @@ -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) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt index 14fcb527..385aaf85 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/SvgEmojiSpan.kt @@ -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 // 絵文字リソースの種類によって異なるスパンを作る diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt index 69ec2c44..ac054579 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/HTMLDecoder.kt @@ -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("----------") } diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt b/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt index d94b6cdd..79c0003f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/ProgressResponseBody.kt @@ -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) diff --git a/app/src/main/java/jp/juggler/util/Json.kt b/app/src/main/java/jp/juggler/util/Json.kt index c4a21452..71f8b2a9 100644 --- a/app/src/main/java/jp/juggler/util/Json.kt +++ b/app/src/main/java/jp/juggler/util/Json.kt @@ -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. diff --git a/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt b/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt index dc8321f2..a58925c6 100644 --- a/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt +++ b/app/src/main/java/me/drakeet/support/toast/ToastCompat.kt @@ -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 { diff --git a/colorpicker/build.gradle b/colorpicker/build.gradle index 7b8f3126..564db643 100644 --- a/colorpicker/build.gradle +++ b/colorpicker/build.gradle @@ -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' \ No newline at end of file + 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' + }) +} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java deleted file mode 100644 index 6eeb6877..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java +++ /dev/null @@ -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; - } - } - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt new file mode 100644 index 00000000..437bb8bf --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.kt @@ -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() + } +} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java deleted file mode 100644 index 57dd699d..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java +++ /dev/null @@ -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); - } - } - - } - -} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt new file mode 100644 index 00000000..4e9889ed --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.kt @@ -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 + } + } + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java deleted file mode 100644 index 3567d73c..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java +++ /dev/null @@ -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(); - } - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt new file mode 100644 index 00000000..5e5d3081 --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.kt @@ -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() + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java deleted file mode 100644 index 57e0e96a..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java +++ /dev/null @@ -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; - -/** - *

A dialog to pick a color.

- * - *

The {@link Activity activity} that shows this dialog should implement {@link ColorPickerDialogListener}

- * - *

Example usage:

- * - *
- *   ColorPickerDialog.newBuilder().show(activity);
- * 
- */ -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 - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt new file mode 100644 index 00000000..590021bb --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.kt @@ -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: + * + *
+ * ColorPickerDialog.newBuilder().show(activity);
+
* + */ +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(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(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(R.id.gridView) + loadPresets() + if (showColorShades) { + createColorShades(color) + } else { + shadesLayout?.visibility = View.GONE + contentView.findViewById(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(R.id.transparency_layout).visibility = View.GONE + contentView.findViewById(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(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(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(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") + } + } +} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.kt similarity index 52% rename from colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.java rename to colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.kt index 1bf059d1..addab4f6 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialogListener.kt @@ -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) } diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java deleted file mode 100644 index 92865e42..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java +++ /dev/null @@ -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); - } - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt new file mode 100644 index 00000000..97f1e523 --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.kt @@ -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) + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java deleted file mode 100644 index 96d5940b..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java +++ /dev/null @@ -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); - } - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.kt new file mode 100644 index 00000000..2612565f --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.kt @@ -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 + } +} \ No newline at end of file diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.kt similarity index 67% rename from colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.java rename to colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.kt index e40342bd..3f68b60d 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorShape.kt @@ -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 + } } diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorString.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorString.kt new file mode 100644 index 00000000..d50cbed6 --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorString.kt @@ -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 + } +} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.kt similarity index 54% rename from colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.java rename to colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.kt index 1c9ce013..69e19e91 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/DrawingUtils.kt @@ -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 + } } diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.java deleted file mode 100644 index 9023b779..00000000 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.java +++ /dev/null @@ -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); - } - -} diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.kt b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.kt new file mode 100644 index 00000000..baf0d094 --- /dev/null +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/NestedGridView.kt @@ -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) + } +} diff --git a/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt b/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt index b07e247e..48574e5b 100644 --- a/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt +++ b/sample_apng/src/main/java/jp/juggler/apng/sample/ActList.kt @@ -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, 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 }) { // 特に何もしてないらしい } }