From 9c68857e6e44e1525387d5150a51fe5e798031c9 Mon Sep 17 00:00:00 2001 From: tateisu Date: Thu, 28 Oct 2021 05:58:19 +0900 Subject: [PATCH] targetSdkVersion 31 --- .idea/compiler.xml | 5 + .idea/gradle.xml | 2 +- .idea/inspectionProfiles/Project_Default.xml | 15 + _Emoji/emojiConverter/build.gradle | 18 +- app/build.gradle | 101 +- .../subwaytooter/TestMisskeyMentionAndroid.kt | 3 +- .../juggler/subwaytooter/TestTootInstance.kt | 44 +- app/src/main/AndroidManifest.xml | 153 +- .../subwaytooter/ActColumnCustomize.kt | 4 +- .../java/jp/juggler/subwaytooter/ActPost.kt | 83 +- .../java/jp/juggler/subwaytooter/ActText.kt | 2 +- .../subwaytooter/actpost/CompletionHelper.kt | 20 +- .../juggler/subwaytooter/api/TootApiResult.kt | 2 +- .../subwaytooter/api/entity/TootInstance.kt | 2 +- .../subwaytooter/api/entity/TootReaction.kt | 4 +- .../subwaytooter/column/ColumnTask_Refresh.kt | 2 +- .../subwaytooter/column/UserRelationLoader.kt | 2 +- .../columnviewholder/ColumnViewHolder.kt | 4 +- .../itemviewholder/DlgContextMenu.kt | 2 +- .../itemviewholder/ItemViewHolder.kt | 2 +- .../itemviewholder/ItemViewHolderEnquete.kt | 4 +- .../itemviewholder/StatusButtons.kt | 4 +- .../notification/PushSubscriptionHelper.kt | 2 +- .../subwaytooter/span/HighlightSpan.kt | 2 +- .../subwaytooter/span/MyClickableSpan.kt | 2 +- .../subwaytooter/util/AttachmentPicker.kt | 2 +- .../jp/juggler/subwaytooter/util/PostImpl.kt | 12 +- .../juggler/subwaytooter/view/MyEditText.kt | 82 +- .../main/java/jp/juggler/util/ColumnMeta.kt | 39 +- .../main/java/jp/juggler/util/LogCategory.kt | 6 +- build.gradle | 19 +- .../colorpicker/AlphaPatternDrawable.java | 161 +- .../colorpicker/ColorPaletteAdapter.java | 213 +- .../android/colorpicker/ColorPanelView.java | 13 +- .../colorpicker/ColorPickerDialog.java | 1605 +++++++------- .../android/colorpicker/ColorPickerView.java | 1882 ++++++++--------- .../android/colorpicker/ColorPreference.java | 336 +-- gradle/wrapper/gradle-wrapper.properties | 3 +- sample_apng/build.gradle | 5 +- sample_apng/src/main/AndroidManifest.xml | 25 +- .../java/jp/juggler/apng/sample/ActList.kt | 17 +- .../java/jp/juggler/apng/sample/ActViewer.kt | 4 +- 42 files changed, 2504 insertions(+), 2404 deletions(-) diff --git a/.idea/compiler.xml b/.idea/compiler.xml index 059436d2..e8373bc9 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -3,6 +3,11 @@ + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index 1ea28578..73b965bb 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,10 +4,10 @@ \ No newline at end of file diff --git a/_Emoji/emojiConverter/build.gradle b/_Emoji/emojiConverter/build.gradle index 92dd4d90..36cceb9f 100644 --- a/_Emoji/emojiConverter/build.gradle +++ b/_Emoji/emojiConverter/build.gradle @@ -11,17 +11,17 @@ repositories { } dependencies { - compile fileTree(include: ['*.jar'], dir: 'src/lib') + implementation fileTree(include: ['*.jar'], dir: 'src/lib') implementation "com.google.guava:guava:28.1-jre" implementation "org.jetbrains.kotlin:kotlin-stdlib" - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.6.0' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.7.0' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine' - def ktor_version="1.5.0" - implementation "io.ktor:ktor-client-core:$ktor_version" - implementation "io.ktor:ktor-client-cio:$ktor_version" - implementation "io.ktor:ktor-client-features:$ktor_version" - implementation "io.ktor:ktor-client-encoding:$ktor_version" + def ktorVersion="1.5.0" + implementation "io.ktor:ktor-client-core:$ktorVersion" + implementation "io.ktor:ktor-client-cio:$ktorVersion" + implementation "io.ktor:ktor-client-features:$ktorVersion" + implementation "io.ktor:ktor-client-encoding:$ktorVersion" // StringEscapeUtils.unescapeHtml4 implementation "org.apache.commons:commons-text:1.9" @@ -32,4 +32,8 @@ dependencies { test { useJUnitPlatform() +} +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 } \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 6233c19a..0b2dd30d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -30,11 +30,22 @@ android { } kotlinOptions { + jvmTarget = '1.8' + useIR = true freeCompilerArgs += [ "-Xopt-in=kotlin.ExperimentalStdlibApi", "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi", + "-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi", ] } + buildFeatures { + compose true + } + composeOptions { + kotlinCompilerExtensionVersion compose_version + } buildTypes { release { @@ -57,13 +68,6 @@ android { } } - dexOptions { - jumboMode = true - preDexLibraries true - maxProcessCount 10 - javaMaxHeapSize "3g" - } - // Generate Signed APK のファイル名を変更 android.applicationVariants.all { variant -> if (variant.buildType.name == "release") { @@ -73,7 +77,8 @@ android { def versionName = defaultConfig.versionName def flavor = variant.flavorName def date = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date()) - outputFileName = "../../SubwayTooter-${flavor}-${versionCode}-${versionName}-${date}.apk" + def branch = gitBranch() + outputFileName = "../../SubwayTooter-${branch}-${flavor}-${versionCode}-${versionName}-${date}.apk" } } } @@ -81,12 +86,27 @@ android { packagingOptions { // https://github.com/Kotlin/kotlinx.coroutines/issues/1064 pickFirst("META-INF/atomicfu.kotlin_module") + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } } useLibrary 'android.test.base' useLibrary 'android.test.mock' } +static def gitBranch() { + def branch = "(no branch)" + def proc = "git status".execute() + proc.in.eachLine { line -> + def matcher = line =~ /\AOn branch (\S+)/ + if (matcher) branch = matcher.group(1) + } + proc.err.eachLine { line -> println line } + proc.waitFor() + branch +} + kapt { useBuildCache = true } @@ -109,20 +129,17 @@ dependencies { implementation "androidx.appcompat:appcompat:$appcompat_version" - //noinspection KtxExtensionAvailable - implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" - // DrawerLayout implementation "androidx.drawerlayout:drawerlayout:1.1.1" // NavigationView - implementation "com.google.android.material:material:1.3.0" + implementation "com.google.android.material:material:1.4.0" // PreferenceManager implementation "androidx.preference:preference-ktx:1.1.1" - implementation "androidx.exifinterface:exifinterface:1.3.2" + implementation "androidx.exifinterface:exifinterface:1.3.3" // CustomTabs implementation "androidx.browser:browser:1.3.0" @@ -155,19 +172,17 @@ dependencies { testImplementation "junit:junit:$junit_version" // しばらくはkotlin-testとjunitを併用 - implementation 'com.squareup.okhttp3:okhttp:4.8.1' - implementation 'com.squareup.okhttp3:okhttp-urlconnection:4.8.1' - testImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1' - androidTestImplementation 'com.squareup.okhttp3:mockwebserver:3.12.1' + def okhttpVersion = "4.9.2" + implementation "com.squareup.okhttp3:okhttp:$okhttpVersion" + implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion" + testImplementation "com.squareup.okhttp3:mockwebserver:$okhttpVersion" + androidTestImplementation "'com.squareup.okhttp3:mockwebserver:$okhttpVersion" - def glideVersion = '4.11.0' + def glideVersion = '4.12.0' implementation "com.github.bumptech.glide:glide:$glideVersion" implementation "com.github.bumptech.glide:annotations:$glideVersion" - implementation( "com.github.bumptech.glide:okhttp3-integration:$glideVersion"){ + implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion") { exclude group: 'com.squareup.okhttp3', module: 'okhttp' - // 推移的な依存関係の除外 - // glide 4.9.0 は okhttp3 3.9.1を使ってる - // http://bumptech.github.io/glide/int/about.html#how-do-i-use-a-specific-version-of-okhttp-volley-or-other-third-party-library } kapt "com.github.bumptech.glide:compiler:$glideVersion" @@ -190,7 +205,7 @@ dependencies { implementation 'com.astuetz:pagerslidingtabstrip:1.0.1' - implementation 'com.google.android.exoplayer:exoplayer:2.15.0' + implementation 'com.google.android.exoplayer:exoplayer:2.15.1' /* WARNING: [Processor] Library '…\exoplayer-ui-2.12.0.aar' contains references to both AndroidX and old support library. This seems like the library is partially migrated. Jetifier will try to rewrite the library anyway. Example of androidX reference: 'androidx/core/app/NotificationCompat$Builder' @@ -199,6 +214,46 @@ dependencies { */ implementation 'com.caverock:androidsvg-aar:1.4' + + + // ViewModel + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" + // LiveData + implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" + // Lifecycles only (without ViewModel or LiveData) + implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" + + // Saved state module for ViewModel + implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" + + // if using Java8, use the following instead of lifecycle-compiler + implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" + + // optional - helpers for implementing LifecycleOwner in a Service + implementation "androidx.lifecycle:lifecycle-service:$lifecycle_version" + + // optional - ProcessLifecycleOwner provides a lifecycle for the whole application process + implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version" + + // optional - ReactiveStreams support for LiveData + implementation "androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycle_version" + + // optional - Test helpers for LiveData + testImplementation "androidx.arch.core:core-testing:$arch_version" + + implementation "com.google.accompanist:accompanist-flowlayout:0.20.0" + + + implementation "androidx.compose.ui:ui:$compose_version" + implementation "androidx.compose.material:material:$compose_version" + implementation "androidx.compose.ui:ui-tooling-preview:$compose_version" + implementation "androidx.compose.runtime:runtime-livedata:$compose_version" + implementation "androidx.compose.material:material-icons-extended:$compose_version" + androidTestImplementation "androidx.compose.ui:ui-test-junit4:$compose_version" + debugImplementation "androidx.compose.ui:ui-tooling:$compose_version" + + implementation 'androidx.activity:activity-compose:1.4.0-rc01' + } repositories { diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt index c4a07cac..1e623ebf 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestMisskeyMentionAndroid.kt @@ -33,8 +33,9 @@ class TestMisskeyMentionAndroid { // val a="""[[ ]""".toRegex() // IDEで警告が出るが、Androidは正規表現エンジンが異なるので仕方ない + @Suppress("RegExpRedundantNestedCharacterClass") assertEquals(true, """[[ ]]][ ]""".toRegex().matches(" ] ")) - + } @Test diff --git a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt index a59554ce..8ec501a0 100644 --- a/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt +++ b/app/src/androidTest/java/jp/juggler/subwaytooter/TestTootInstance.kt @@ -1,7 +1,7 @@ package jp.juggler.subwaytooter -import androidx.test.InstrumentationRegistry -import androidx.test.runner.AndroidJUnit4 +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry import jp.juggler.subwaytooter.api.TootApiCallback import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.entity.Host @@ -10,13 +10,13 @@ import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.util.SimpleHttpClientImpl import jp.juggler.util.LogCategory import jp.juggler.util.MySslSocketFactory -import junit.framework.Assert.assertNotNull -import junit.framework.Assert.assertNull import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import okhttp3.ConnectionSpec import okhttp3.OkHttpClient +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import java.util.* @@ -38,10 +38,14 @@ class TestTootInstance { .readTimeout(60.toLong(), TimeUnit.SECONDS) .writeTimeout(60.toLong(), TimeUnit.SECONDS) .pingInterval(10, TimeUnit.SECONDS) - .connectionSpecs(Collections.singletonList(ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .allEnabledCipherSuites() - .allEnabledTlsVersions() - .build())) + .connectionSpecs( + Collections.singletonList( + ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) + .allEnabledCipherSuites() + .allEnabledTlsVersions() + .build() + ) + ) .sslSocketFactory(MySslSocketFactory, MySslSocketFactory.trustManager) .build() @@ -56,11 +60,11 @@ class TestTootInstance { } } - private val appContext = InstrumentationRegistry.getTargetContext()!! + private val appContext = InstrumentationRegistry.getInstrumentation().targetContext!! val client = TootApiClient( context = appContext, - httpClient =SimpleHttpClientImpl(appContext, okHttp), + httpClient = SimpleHttpClientImpl(appContext, okHttp), callback = dummyClientCallback ) } @@ -73,12 +77,12 @@ class TestTootInstance { @Test fun testWithoutAccount() { runBlocking { - withContext(Dispatchers.IO){ - suspend fun a(host:Host){ - val (ti,ri) = TootInstance.get(client,host ) + withContext(Dispatchers.IO) { + suspend fun a(host: Host) { + val (ti, ri) = TootInstance.getEx(client, hostArg = host) assertNotNull(ti) assertNull(ri?.error) - ti!!.run{ log.d("${instanceType} ${uri} ${version}")} + ti!!.run { log.d("$instanceType $uri $version") } } a(Host.parse("mastodon.juggler.jp")) @@ -90,15 +94,15 @@ class TestTootInstance { @Test fun testWithAccount() { runBlocking { - withContext(Dispatchers.IO){ - suspend fun a(account:SavedAccount){ - val (ti,ri) = TootInstance.get(client,account = account ) + withContext(Dispatchers.IO) { + suspend fun a(account: SavedAccount) { + val (ti, ri) = TootInstance.getEx(client, account = account) assertNull(ri?.error) assertNotNull(ti) - ti!!.run{ log.d("${account.acct} ${instanceType} ${uri} ${version}")} + ti!!.run { log.d("${account.acct} $instanceType $uri $version") } } - a( SavedAccount(45,"tateisu@mastodon.juggler.jp") ) - a( SavedAccount(45,"tateisu@misskey.io",misskeyVersion=12) ) + a(SavedAccount(45, "tateisu@mastodon.juggler.jp")) + a(SavedAccount(45, "tateisu@misskey.io", misskeyVersion = 12)) } } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 08bf1c74..848de8ca 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,19 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" package="jp.juggler.subwaytooter"> - - - - - - - - - - - @@ -62,6 +50,20 @@ + + + + + + + + + + + + - - - - - - - - - - - - - - - - - @@ -108,9 +91,8 @@ + android:exported="true" + android:label="@string/app_name"> @@ -195,105 +177,122 @@ + android:windowSoftInputMode="adjustResize"> + - /> - /> + + - @@ -301,6 +300,18 @@ android:name="android.max_aspect" android:value="100.0" /> + + + + + + - + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - \ No newline at end of file + diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt index 9d954281..83ab0690 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActColumnCustomize.kt @@ -42,8 +42,6 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke internal const val COLOR_DIALOG_ID_ACCT_TEXT = 4 internal const val COLOR_DIALOG_ID_CONTENT_TEXT = 5 - internal const val REQUEST_CODE_PICK_BACKGROUND = 1 - internal const val PROGRESS_MAX = 65536 fun createIntent(activity: ActMain, idx: Int) = @@ -72,7 +70,7 @@ class ActColumnCustomize : AppCompatActivity(), View.OnClickListener, ColorPicke private var lastImageUri: String? = null private var lastImageBitmap: Bitmap? = null - val arColumnBackgroundImage = activityResultHandler { ar -> + private val arColumnBackgroundImage = activityResultHandler { ar -> val data = ar?.data if (data != null && ar.resultCode == RESULT_OK) { data.handleGetContentResult(contentResolver) diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt index da609480..7d5b3e1a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActPost.kt @@ -14,8 +14,6 @@ import android.view.* import android.view.inputmethod.EditorInfo import android.widget.* import androidx.appcompat.app.AppCompatActivity -import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.core.view.inputmethod.InputContentInfoCompat import jp.juggler.subwaytooter.action.saveWindowSize import jp.juggler.subwaytooter.actpost.* import jp.juggler.subwaytooter.api.* @@ -56,11 +54,6 @@ class ActPost : AppCompatActivity(), const val KEY_QUOTE = "quote" const val KEY_SCHEDULED_STATUS = "scheduled_status" - const val KEY_ATTACHMENT_LIST = "attachment_list" - const val KEY_IN_REPLY_TO_ID = "in_reply_to_id" - const val KEY_IN_REPLY_TO_TEXT = "in_reply_to_text" - const val KEY_IN_REPLY_TO_IMAGE = "in_reply_to_image" - const val STATE_ALL = "all" ///////////////////////////////////////////////// @@ -97,8 +90,8 @@ class ActPost : AppCompatActivity(), lateinit var btnAccount: Button lateinit var btnVisibility: ImageButton - lateinit var btnAttachment: ImageButton - lateinit var btnPost: ImageButton + private lateinit var btnAttachment: ImageButton + private lateinit var btnPost: ImageButton lateinit var llAttachment: View lateinit var ivMedia: List lateinit var cbNSFW: CheckBox @@ -121,7 +114,7 @@ class ActPost : AppCompatActivity(), lateinit var tvCharCount: TextView lateinit var handler: Handler - lateinit var formRoot: ActPostRootLinearLayout + private lateinit var formRoot: ActPostRootLinearLayout lateinit var llReply: View lateinit var tvReplyTo: TextView @@ -129,8 +122,8 @@ class ActPost : AppCompatActivity(), lateinit var scrollView: ScrollView lateinit var tvSchedule: TextView - lateinit var ibSchedule: ImageButton - lateinit var ibScheduleReset: ImageButton + private lateinit var ibSchedule: ImageButton + private lateinit var ibScheduleReset: ImageButton lateinit var pref: SharedPreferences lateinit var appState: AppState @@ -184,41 +177,6 @@ class ActPost : AppCompatActivity(), } } - val commitContentListener = - InputConnectionCompat.OnCommitContentListener { - inputContentInfo: InputContentInfoCompat, - flags: Int, - _: Bundle?, - -> - // Intercepts InputConnection#commitContent API calls. - // - inputContentInfo : content to be committed - // - flags : {@code 0} or {@link #INPUT_CONTENT_GRANT_READ_URI_PERMISSION} - // - opts : optional bundle data. This can be {@code null} - // return - // - true if this request is accepted by the application, - // no matter if the request is already handled or still being handled in background. - // - false to use the default implementation - - // read and display inputContentInfo asynchronously - - if (Build.VERSION.SDK_INT >= 25 && - flags and InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION != 0 - ) { - try { - inputContentInfo.requestPermission() - } catch (ignored: Exception) { - // return false if failed - return@OnCommitContentListener false - } - } - - addAttachment(inputContentInfo.contentUri) { - inputContentInfo.releasePermission() - } - - true - } - //////////////////////////////////////////////////////////////// override fun onCreate(savedInstanceState: Bundle?) { @@ -331,7 +289,11 @@ class ActPost : AppCompatActivity(), openBrowser(span.linkInfo.url) } - override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { + override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray + ) { attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults) } @@ -407,7 +369,12 @@ class ActPost : AppCompatActivity(), updateTextCount() } - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { + override fun onItemSelected( + parent: AdapterView<*>?, + view: View?, + position: Int, + id: Long + ) { showPoll() updateTextCount() } @@ -463,13 +430,17 @@ class ActPost : AppCompatActivity(), cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() } completionHelper = CompletionHelper(this, pref, appState.handler) - completionHelper.attachEditText(formRoot, etContent, false, object : CompletionHelper.Callback2 { - override fun onTextUpdate() { - updateTextCount() - } + completionHelper.attachEditText( + formRoot, + etContent, + false, + object : CompletionHelper.Callback2 { + override fun onTextUpdate() { + updateTextCount() + } - override fun canOpenPopup(): Boolean = true - }) + override fun canOpenPopup(): Boolean = true + }) val textWatcher: TextWatcher = object : TextWatcher { override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {} @@ -491,6 +462,6 @@ class ActPost : AppCompatActivity(), scrollView.viewTreeObserver.addOnScrollChangedListener(scrollListener) etContent.contentMineTypeArray = AttachmentUploader.acceptableMimeTypes.toTypedArray() - etContent.commitContentListener = commitContentListener + etContent.contentCallback = { addAttachment(it) } } } diff --git a/app/src/main/java/jp/juggler/subwaytooter/ActText.kt b/app/src/main/java/jp/juggler/subwaytooter/ActText.kt index c802d8e8..c5418ad6 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/ActText.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/ActText.kt @@ -98,7 +98,7 @@ class ActText : AppCompatActivity() { return true } - override fun onCreateOptionsMenu(menu: Menu?): Boolean { + override fun onCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.act_text, menu) return super.onCreateOptionsMenu(menu) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/actpost/CompletionHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/actpost/CompletionHelper.kt index 95089f59..86b1a680 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/actpost/CompletionHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/actpost/CompletionHelper.kt @@ -83,9 +83,9 @@ class CompletionHelper( val c = cp.toChar() return '0' <= c && c <= '9' || - 'A' <= c && c <= 'Z' || - 'a' <= c && c <= 'z' || - c == '_' || c == '-' || c == '.' + 'A' <= c && c <= 'Z' || + 'a' <= c && c <= 'z' || + c == '_' || c == '-' || c == '.' } // Letter | Mark | Decimal_Number | Connector_Punctuation @@ -348,15 +348,13 @@ class CompletionHelper( } }) - et.setOnSelectionChangeListener(object : MyEditText.OnSelectionChangeListener { - override fun onSelectionChanged(selStart: Int, selEnd: Int) { - if (selStart != selEnd) { - // 範囲選択されてるならポップアップは閉じる - log.d("onSelectionChanged: range selected") - closeAcctPopup() - } + // 範囲選択されてるならポップアップは閉じる + et.onSelectionChange = { selStart, selEnd -> + if (selStart != selEnd) { + log.d("onSelectionChange: range selected") + closeAcctPopup() } - }) + } // 全然動いてなさそう… // et.setCustomSelectionActionModeCallback( action_mode_callback ); diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt index 52c01ddd..c716fa5f 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/TootApiResult.kt @@ -105,7 +105,7 @@ open class TootApiResult( } // アカウント作成APIのdetailsを読むため、エラー応答のjsonオブジェクトを保持する - var errorJson: JsonObject? = null + private var errorJson: JsonObject? = null internal fun simplifyErrorHtml( sv: String, diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt index 1e15cfed..40f1df2d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootInstance.kt @@ -379,7 +379,7 @@ class TootInstance(parser: TootParser, src: JsonObject) { val result = Channel>() } - fun queuedRequest( + private fun queuedRequest( allowPixelfed: Boolean, get: suspend (cached: TootInstance?) -> Pair, ) = QueuedRequest(allowPixelfed, get) diff --git a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt index 1499524d..c9fb4d2d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/api/entity/TootReaction.kt @@ -36,7 +36,7 @@ class TootReaction( ) { companion object { - fun appendDomain(name: String, domain: String?) = + private fun appendDomain(name: String, domain: String?) = if (domain?.isNotEmpty() == true) { "$name@$domain" } else { @@ -164,7 +164,7 @@ class TootReaction( } } - fun chooseUrl() = when { + private fun chooseUrl() = when { PrefB.bpDisableEmojiAnimation(App1.pref) -> staticUrl else -> url } diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt index 80aa2332..cc9d58c7 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/ColumnTask_Refresh.kt @@ -15,7 +15,7 @@ class ColumnTask_Refresh( private val bSilent: Boolean, val bBottom: Boolean, internal val postedStatusId: EntityId? = null, - internal val refreshAfterToot: Int = -1, + private val refreshAfterToot: Int = -1, ) : ColumnTask( columnArg, if (bBottom) ColumnTaskType.REFRESH_BOTTOM else ColumnTaskType.REFRESH_TOP diff --git a/app/src/main/java/jp/juggler/subwaytooter/column/UserRelationLoader.kt b/app/src/main/java/jp/juggler/subwaytooter/column/UserRelationLoader.kt index a515f7c2..6cd477ea 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/column/UserRelationLoader.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/column/UserRelationLoader.kt @@ -18,7 +18,7 @@ class UserRelationLoader(val column: Column) { val whoSet = HashSet() val acctSet = HashSet() - val tagSet = HashSet() + private val tagSet = HashSet() fun add(whoRef: TootAccountRef?) { add(whoRef?.get()) diff --git a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolder.kt b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolder.kt index 7c597510..842d43cd 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/columnviewholder/ColumnViewHolder.kt @@ -80,7 +80,7 @@ class ColumnViewHolder( lateinit var llColumnHeader: View lateinit var tvColumnIndex: TextView lateinit var tvColumnStatus: TextView - lateinit var tvColumnContext: TextView + private lateinit var tvColumnContext: TextView lateinit var ivColumnIcon: ImageView lateinit var tvColumnName: TextView @@ -143,7 +143,7 @@ class ColumnViewHolder( lateinit var btnQuickFilterVote: ImageButton lateinit var llRefreshError: FrameLayout - lateinit var ivRefreshError: ImageView + private lateinit var ivRefreshError: ImageView lateinit var tvRefreshError: TextView lateinit var llListList: View diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/DlgContextMenu.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/DlgContextMenu.kt index fd4b545d..dc22a88a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/DlgContextMenu.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/DlgContextMenu.kt @@ -552,7 +552,7 @@ internal class DlgContextMenu( return true } - fun onClickUpdateGroup(v: View): Boolean = when (v.id) { + private fun onClickUpdateGroup(v: View): Boolean = when (v.id) { R.id.btnGroupStatusCrossAccount -> updateGroup( btnGroupStatusCrossAccount, llGroupStatusCrossAccount, 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 8c879281..de4fd852 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolder.kt @@ -775,7 +775,7 @@ class ItemViewHolder( } } - fun _LinearLayout.inflateConversationIconOne() = + private fun _LinearLayout.inflateConversationIconOne() = myNetworkImageView { scaleType = ImageView.ScaleType.CENTER_CROP }.lparams(dip(24), dip(24)) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderEnquete.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderEnquete.kt index b9692d26..0fd5a267 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderEnquete.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/ItemViewHolderEnquete.kt @@ -324,7 +324,7 @@ fun ItemViewHolder.onClickEnqueteChoice( TootPollsType.Mastodon -> client.request( "/api/v1/polls/${enquete.pollId}/votes", jsonObject { - put("choices", jp.juggler.util.jsonArray { add(idx) }) + put("choices", jsonArray { add(idx) }) }.toPostRequestBuilder() ) TootPollsType.FriendsNico -> client.request( @@ -406,7 +406,7 @@ fun ItemViewHolder.sendMultiple( client.request( "/api/v1/polls/${enquete.pollId}/votes", jsonObject { - put("choices", jp.juggler.util.jsonArray { + put("choices", jsonArray { enquete.items.forEachIndexed { index, choice -> if (choice.checked) add(index) } diff --git a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/StatusButtons.kt b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/StatusButtons.kt index 22998715..afdc13e1 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/StatusButtons.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/itemviewholder/StatusButtons.kt @@ -84,8 +84,8 @@ class StatusButtons( private val colorAccent: Int get() = activity.attrColor(R.attr.colorImageButtonAccent) - var optionalButtonFirst: View? = null - var optionalButtonCount = 0 + private var optionalButtonFirst: View? = null + private var optionalButtonCount = 0 var ti: TootInstance? = null init { diff --git a/app/src/main/java/jp/juggler/subwaytooter/notification/PushSubscriptionHelper.kt b/app/src/main/java/jp/juggler/subwaytooter/notification/PushSubscriptionHelper.kt index a288af6c..1936d207 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/notification/PushSubscriptionHelper.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/notification/PushSubscriptionHelper.kt @@ -472,7 +472,7 @@ class PushSubscriptionHelper( } } - suspend fun canSkipSubscriptionMastodon( + private suspend fun canSkipSubscriptionMastodon( client: TootApiClient, clientIdentifier: String, endpoint: String, diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/HighlightSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/HighlightSpan.kt index 3417e3ab..44f41bab 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/HighlightSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/HighlightSpan.kt @@ -3,7 +3,7 @@ package jp.juggler.subwaytooter.span import android.text.TextPaint import android.text.style.CharacterStyle -class HighlightSpan(val colorFg: Int, val colorBg: Int) : CharacterStyle() { +class HighlightSpan(private val colorFg: Int, val colorBg: Int) : CharacterStyle() { override fun updateDrawState(ds: TextPaint) { if (colorFg != 0) ds.color = colorFg diff --git a/app/src/main/java/jp/juggler/subwaytooter/span/MyClickableSpan.kt b/app/src/main/java/jp/juggler/subwaytooter/span/MyClickableSpan.kt index d093bd7b..fce3049e 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/span/MyClickableSpan.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/span/MyClickableSpan.kt @@ -31,7 +31,7 @@ class MyClickableSpan(val linkInfo: LinkInfo) : ClickableSpan() { var showLinkUnderline = true } - val colorFg: Int + private val colorFg: Int val colorBg: Int init { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt index c7b8bf1c..e4684a5a 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/AttachmentPicker.kt @@ -223,7 +223,7 @@ class AttachmentPicker( } } - fun performCapture(action: String, errorCaption: String) { + private fun performCapture(action: String, errorCaption: String) { try { arCapture.launch(Intent(action)) } catch (ex: Throwable) { diff --git a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt index 3b4fdd88..dd4c7bfb 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/util/PostImpl.kt @@ -69,10 +69,10 @@ class PostImpl( private var visibilityChecked: TootVisibility? = null - var bConfirmTag: Boolean = false + private var bConfirmTag: Boolean = false var bConfirmAccount: Boolean = false - var bConfirmRedraft: Boolean = false - var bConfirmTagCharacter: Boolean = false + private var bConfirmRedraft: Boolean = false + private var bConfirmTagCharacter: Boolean = false private val choiceMaxChars = when { account.isMisskey -> 15 @@ -229,9 +229,9 @@ class PostImpl( return true } - var resultStatus: TootStatus? = null - var resultCredentialTmp: TootAccount? = null - var resultScheduledStatusSucceeded = false + private var resultStatus: TootStatus? = null + private var resultCredentialTmp: TootAccount? = null + private var resultScheduledStatusSucceeded = false private suspend fun getCredential( client: TootApiClient, diff --git a/app/src/main/java/jp/juggler/subwaytooter/view/MyEditText.kt b/app/src/main/java/jp/juggler/subwaytooter/view/MyEditText.kt index cefb515d..4b5ff75d 100644 --- a/app/src/main/java/jp/juggler/subwaytooter/view/MyEditText.kt +++ b/app/src/main/java/jp/juggler/subwaytooter/view/MyEditText.kt @@ -2,23 +2,53 @@ package jp.juggler.subwaytooter.view import android.annotation.SuppressLint import android.content.Context -import androidx.core.view.inputmethod.EditorInfoCompat -import androidx.core.view.inputmethod.InputConnectionCompat -import androidx.appcompat.widget.AppCompatEditText +import android.net.Uri import android.util.AttributeSet import android.view.MotionEvent -import android.view.inputmethod.EditorInfo -import android.view.inputmethod.InputConnection - +import android.view.View +import androidx.appcompat.widget.AppCompatEditText +import androidx.core.view.ContentInfoCompat +import androidx.core.view.OnReceiveContentListener +import androidx.core.view.ViewCompat import jp.juggler.util.LogCategory + class MyEditText : AppCompatEditText { companion object { private val log = LogCategory("MyEditText") + val MIME_TYPES = arrayOf("image/*") } - private var mOnSelectionChangeListener: OnSelectionChangeListener? = null + // 選択範囲変更リスナ + var onSelectionChange: ((selStart: Int, selEnd: Int) -> Unit)? = null + + // キーボードやDnDから画像を挿入するリスナ + var contentCallback: ((Uri) -> Unit)? = null + + /////////////////////////////////////////////////////// + // IMEから画像を送られてくることがあるらしい + + var contentMineTypeArray: Array? = null + + private val receiveContentListener = object : OnReceiveContentListener { + override fun onReceiveContent(view: View, payload: ContentInfoCompat): ContentInfoCompat { + // 受け付けない状況では何も受け取らずに残りを返す + val contentCallback = contentCallback ?: return payload + + val pair = payload.partition { item -> item.uri != null } + val uriContent = pair.first + val remaining = pair.second + if (uriContent != null) { + val clip = uriContent.clip + for (i in 0 until clip.itemCount) { + val uri = clip.getItemAt(i).uri + contentCallback(uri) + } + } + return remaining + } + } constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) @@ -28,20 +58,16 @@ class MyEditText : AppCompatEditText { defStyleAttr ) + init { + ViewCompat.setOnReceiveContentListener(this, MIME_TYPES, receiveContentListener) + } + //////////////////////////////////////////////////// - // 選択範囲変更イベントをコールバックに渡す - - interface OnSelectionChangeListener { - fun onSelectionChanged(selStart: Int, selEnd: Int) - } - - fun setOnSelectionChangeListener(listener: OnSelectionChangeListener) { - mOnSelectionChangeListener = listener - } + // 選択範囲変更の傍受 override fun onSelectionChanged(selStart: Int, selEnd: Int) { super.onSelectionChanged(selStart, selEnd) - mOnSelectionChangeListener?.onSelectionChanged(selStart, selEnd) + onSelectionChange?.invoke(selStart, selEnd) } //////////////////////////////////////////////////// @@ -61,26 +87,4 @@ class MyEditText : AppCompatEditText { // at android.view.View.dispatchTouchEvent (View.java:9303) } } - - /////////////////////////////////////////////////////// - // IMEから画像を送られてくることがあるらしい - - var commitContentListener: InputConnectionCompat.OnCommitContentListener? = null - var contentMineTypeArray: Array? = null - - override fun onCreateInputConnection(outAttrs: EditorInfo?): InputConnection? { - - log.d("onCreateInputConnection: listener=$commitContentListener") - - val superIc = super.onCreateInputConnection(outAttrs) - - val listener = commitContentListener - val mimeArray = contentMineTypeArray - return if (listener == null || mimeArray == null || outAttrs == null) { - superIc - } else { - EditorInfoCompat.setContentMimeTypes(outAttrs, mimeArray) - superIc?.let { InputConnectionCompat.createWrapper(it, outAttrs, listener) } - } - } } diff --git a/app/src/main/java/jp/juggler/util/ColumnMeta.kt b/app/src/main/java/jp/juggler/util/ColumnMeta.kt index 809eb3cc..4346e6e7 100644 --- a/app/src/main/java/jp/juggler/util/ColumnMeta.kt +++ b/app/src/main/java/jp/juggler/util/ColumnMeta.kt @@ -3,6 +3,7 @@ package jp.juggler.util import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase +import androidx.annotation.IntRange ///////////////////////////////////////////////////////////// // SQLite にBooleanをそのまま保存することはできないのでInt型との変換が必要になる @@ -19,16 +20,21 @@ fun Int.i2b() = this != 0 // getBoolean(getColumnIndex(key)) fun Cursor.getInt(key: String) = - getInt(getColumnIndex(key)) + getColumnIndex(key).takeIf { it >= 0 }?.let { getInt(it) } + ?: error("getInt: missing column named $key") -fun Cursor.getIntOrNull(idx: Int) = - if (isNull(idx)) null else getInt(idx) +fun Cursor.getIntOrNull(@IntRange(from = 0) idx: Int) = when { + idx < 0 -> error("getIntOrNull: invalid index $idx") + isNull(idx) -> null + else -> getInt(idx) +} fun Cursor.getIntOrNull(key: String) = - getIntOrNull(getColumnIndex(key)) + getColumnIndex(key).takeIf { it >= 0 }?.let { getIntOrNull(it) } fun Cursor.getLong(key: String) = - getLong(getColumnIndex(key)) + getColumnIndex(key).takeIf { it >= 0 }?.let { getLong(it) } + ?: error("getLong: missing column named $key") //fun Cursor.getLongOrNull(idx:Int) = // if(isNull(idx)) null else getLong(idx) @@ -37,7 +43,8 @@ fun Cursor.getLong(key: String) = // getLongOrNull(getColumnIndex(key)) fun Cursor.getString(key: String): String = - getString(getColumnIndex(key)) + getColumnIndex(key).takeIf { it >= 0 }?.let { getString(it)!! } + ?: error("getString: missing column named $key") fun Cursor.getStringOrNull(keyIdx: Int) = if (isNull(keyIdx)) null else getString(keyIdx) @@ -69,7 +76,7 @@ class ColumnMeta( class List( val table: String, - val initialVersion: Int, + private val initialVersion: Int, var createExtra: () -> Array = { emptyArray() }, var deleteBeforeCreate: Boolean = false, ) : ArrayList() { @@ -143,15 +150,9 @@ fun ContentValues.put(key: ColumnMeta, v: Float?) = put(key.name, v) fun ContentValues.put(key: ColumnMeta, v: Double?) = put(key.name, v) fun ContentValues.put(key: ColumnMeta, v: ByteArray?) = put(key.name, v) -fun Cursor.getInt(key: ColumnMeta) = getInt(getColumnIndex(key.name)) - -fun Cursor.getBoolean(key: ColumnMeta) = getInt(key).i2b() -fun Cursor.getLong(key: ColumnMeta) = getLong(getColumnIndex(key.name)) - -@Suppress("unused") -fun Cursor.getIntOrNull(key: ColumnMeta) = getIntOrNull(getColumnIndex(key.name)) -fun Cursor.getString(key: ColumnMeta): String = getString(getColumnIndex(key.name)) -fun Cursor.getStringOrNull(key: ColumnMeta): String? { - val idx = key.getIndex(this) - return if (isNull(idx)) null else getString(idx) -} +fun Cursor.getInt(key: ColumnMeta) = getInt(key.name) +fun Cursor.getBoolean(key: ColumnMeta) = getInt(key.name).i2b() +fun Cursor.getLong(key: ColumnMeta) = getLong(key.name) +fun Cursor.getIntOrNull(key: ColumnMeta) = getIntOrNull(key.name) +fun Cursor.getString(key: ColumnMeta): String = getString(key.name) +fun Cursor.getStringOrNull(key: ColumnMeta): String? = getStringOrNull(key.name) diff --git a/app/src/main/java/jp/juggler/util/LogCategory.kt b/app/src/main/java/jp/juggler/util/LogCategory.kt index 7a4ec8d8..1cdbd0e9 100644 --- a/app/src/main/java/jp/juggler/util/LogCategory.kt +++ b/app/src/main/java/jp/juggler/util/LogCategory.kt @@ -23,7 +23,7 @@ class LogCategory(category: String) { /////////////////////////////// // string - fun msg(priority: Int, msg: String): Boolean { + private fun msg(priority: Int, msg: String): Boolean { Log.println(priority, tag, msg) return false } @@ -37,7 +37,7 @@ class LogCategory(category: String) { /////////////////////////////// // Resources.getString() - fun msg(priority: Int, res: Resources, @StringRes stringId: Int, args: Array) = + private fun msg(priority: Int, res: Resources, @StringRes stringId: Int, args: Array) = msg(priority, res.getString(stringId, *args)) fun e(res: Resources, @StringRes stringId: Int, vararg args: Any) = @@ -58,7 +58,7 @@ class LogCategory(category: String) { /////////////////////////////// // Throwable + string - fun msg(priority: Int, ex: Throwable, caption: String = "exception.") = + private fun msg(priority: Int, ex: Throwable, caption: String = "exception.") = msg(priority, ex.withCaption(caption)) fun e(ex: Throwable, caption: String = "exception") = msg(Log.ERROR, ex, caption) diff --git a/build.gradle b/build.gradle index b3646602..9c3899a8 100644 --- a/build.gradle +++ b/build.gradle @@ -1,19 +1,22 @@ buildscript { ext.min_sdk_version = 21 - ext.target_sdk_version = 30 - ext.compile_sdk_version = 30 + ext.target_sdk_version = 31 + ext.compile_sdk_version = 31 - ext.appcompat_version='1.3.0' - ext.lifecycle_version='2.3.1' + ext.appcompat_version='1.3.1' + ext.lifecycle_version="2.4.0-rc01" + ext.arch_version = "2.1.0" - ext.kotlin_version = '1.5.20' - ext.kotlinx_coroutines_version = '1.5.0' + ext.kotlin_version = '1.5.31' + ext.kotlinx_coroutines_version = '1.5.2' ext.anko_version='0.10.8' ext.junit_version='4.13.2' - ext.detekt_version='1.18.0' + ext.detekt_version='1.18.1' + + ext.compose_version = '1.0.4' repositories { google() @@ -21,7 +24,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:4.2.2' + classpath 'com.android.tools.build:gradle:7.0.3' classpath 'com.google.gms:google-services:4.3.10' //noinspection DifferentKotlinGradleVersion diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java index 4c076db7..6eeb6877 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/AlphaPatternDrawable.java @@ -32,82 +32,87 @@ import androidx.annotation.NonNull; * It's the pattern you will often see as a background behind a partly transparent image in many applications. */ class AlphaPatternDrawable extends Drawable { - - private int rectangleSize; - - private Paint paint = new Paint(); - private Paint paintWhite = new Paint(); - private 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; - } - } - + + 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/ColorPaletteAdapter.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java index 18894186..57dd699d 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPaletteAdapter.java @@ -19,129 +19,130 @@ package com.jrummyapps.android.colorpicker; import android.content.Context; import android.graphics.Color; import android.graphics.PorterDuff; -import androidx.core.graphics.ColorUtils; 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*/ int colorShape; + /*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 { - - View view; - ColorPanelView colorPanelView; - ImageView imageView; - 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 = (ColorPanelView) view.findViewById(R.id.cpv_color_panel_view); - imageView = (ImageView) view.findViewById(R.id.cpv_color_image_view); - originalBorderColor = colorPanelView.getBorderColor(); - view.setTag(this); + ColorPaletteAdapter(OnColorSelectedListener listener, + int[] colors, + int selectedPosition, + @ColorShape int colorShape) { + this.listener = listener; + this.colors = colors; + this.selectedPosition = selectedPosition; + this.colorShape = colorShape; } - 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); + @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 { - colorPanelView.setBorderColor(originalBorderColor); - imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + holder = (ViewHolder) convertView.getTag(); } - } else { - setColorFilter(position); - } - setOnClickListener(position); + holder.setup(position); + return convertView; } - private void setOnClickListener(final int position) { - colorPanelView.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { - if (selectedPosition != position) { - selectedPosition = position; - notifyDataSetChanged(); - } - listener.onColorSelected(colors[position]); - } - }); - colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { - @Override public boolean onLongClick(View v) { - colorPanelView.showHint(); - return true; - } - }); + void selectNone() { + selectedPosition = -1; + notifyDataSetChanged(); } - 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); - } + 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/ColorPanelView.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java index fae4014b..3567d73c 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPanelView.java @@ -141,22 +141,22 @@ public class ColorPanelView extends View { } else if (shape == ColorShape.CIRCLE) { final int outerRadius = getMeasuredWidth() / 2; if (borderWidthPx > 0) { - canvas.drawCircle(getMeasuredWidth() / 2, - getMeasuredHeight() / 2, + canvas.drawCircle(getMeasuredWidth() / 2f, + getMeasuredHeight() / 2f, outerRadius, borderPaint); } if (Color.alpha(color) < 255) { - canvas.drawCircle(getMeasuredWidth() / 2, - getMeasuredHeight() / 2, + 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() / 2, - getMeasuredHeight() / 2, + canvas.drawCircle(getMeasuredWidth() / 2f, + getMeasuredHeight() / 2f, outerRadius - borderWidthPx, colorPaint); } @@ -169,6 +169,7 @@ public class ColorPanelView extends View { int height = MeasureSpec.getSize(heightMeasureSpec); setMeasuredDimension(width, height); } else if (shape == ColorShape.CIRCLE) { + //noinspection SuspiciousNameCombination super.onMeasure(widthMeasureSpec, widthMeasureSpec); setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); } else { diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java index 7f00624f..57e0e96a 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerDialog.java @@ -19,19 +19,12 @@ package com.jrummyapps.android.colorpicker; import android.annotation.SuppressLint; import android.app.Activity; import android.app.Dialog; -import android.app.DialogFragment; 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 androidx.annotation.ColorInt; -import androidx.annotation.IntDef; -import androidx.annotation.NonNull; -import androidx.annotation.StringRes; -import androidx.core.graphics.ColorUtils; -import androidx.appcompat.app.AlertDialog; import android.text.Editable; import android.text.InputFilter; import android.text.TextWatcher; @@ -50,6 +43,7 @@ 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; @@ -57,6 +51,15 @@ 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.

* @@ -68,822 +71,836 @@ import java.util.Locale; * ColorPickerDialog.newBuilder().show(activity); * */ -public class ColorPickerDialog extends DialogFragment implements OnTouchListener, OnColorChangedListener, TextWatcher { +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"; + 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; + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_PRESETS = 1; - static final int ALPHA_THRESHOLD = 165; + 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 - }; + /** + * 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(Activity activity) { - super.onAttach(activity); - if (colorPickerDialogListener == null && activity instanceof ColorPickerDialogListener) { - colorPickerDialogListener = (ColorPickerDialogListener) activity; - } - } - - @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - dialogId = getArguments().getInt(ARG_ID); - showAlphaSlider = getArguments().getBoolean(ARG_ALPHA); - showColorShades = getArguments().getBoolean(ARG_SHOW_COLOR_SHADES); - colorShape = getArguments().getInt(ARG_COLOR_SHAPE); - if (savedInstanceState == null) { - color = getArguments().getInt(ARG_COLOR); - dialogType = getArguments().getInt(ARG_TYPE); - } else { - color = savedInstanceState.getInt(ARG_COLOR); - dialogType = savedInstanceState.getInt(ARG_TYPE); + /** + * 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(); } - rootView = new FrameLayout(getActivity()); - if (dialogType == TYPE_CUSTOM) { - rootView.addView(createPickerView()); - } else if (dialogType == TYPE_PRESETS) { - rootView.addView(createPresetsView()); + 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; + } } - AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()) - .setView(rootView) - .setPositiveButton(R.string.cpv_select, new DialogInterface.OnClickListener() { - @Override public void onClick(DialogInterface dialog, int which) { - colorPickerDialogListener.onColorSelected(dialogId, color); - } + @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(); + } }); - int dialogTitleStringRes = getArguments().getInt(ARG_DIALOG_TITLE); - if (dialogTitleStringRes != 0) { - builder.setTitle(dialogTitleStringRes); - } + contentView.setOnTouchListener(this); + colorPicker.setOnColorChangedListener(this); + hexEditText.addTextChangedListener(this); - int neutralButtonStringRes; - if (dialogType == TYPE_CUSTOM && getArguments().getBoolean(ARG_ALLOW_PRESETS)) { - neutralButtonStringRes = R.string.cpv_presets; - } else if (dialogType == TYPE_PRESETS && getArguments().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(new View.OnClickListener() { - @Override public void onClick(View 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(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() { - 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 = - getActivity().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(getArguments().getInt(ARG_COLOR)); - colorPicker.setColor(color, true); - newColorPanel.setColor(color); - setHex(color); - - if (!showAlphaSlider) { - hexEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)}); - } - - newColorPanel.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View v) { - if (newColorPanel.getColor() == color) { - colorPickerDialogListener.onColorSelected(dialogId, color); - dismiss(); - } - } - }); - - contentView.setOnTouchListener(this); - colorPicker.setOnColorChangedListener(this); - hexEditText.addTextChangedListener(this); - - hexEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { - @Override - public void onFocusChange(View v, boolean 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(new ColorPaletteAdapter.OnColorSelectedListener() { - @Override public void onColorSelected(int 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(new Runnable() { - @Override public void run() { - // The color is black when rotating the dialog. This is a dirty fix. WTF!? - colorPanelView.setColor(colorShade); - } - }); - - colorPanelView.setOnClickListener(new View.OnClickListener() { - @Override public void onClick(View 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); + hexEditText.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + InputMethodManager imm = (InputMethodManager) getActivity() + .getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) imm.showSoftInput(hexEditText, InputMethodManager.SHOW_IMPLICIT); } - cpv.setTag(cpv == v); - } - } - }); - colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { - @Override public boolean onLongClick(View v) { - colorPanelView.showHint(); - return true; - } - }); + }); + + return contentView; } - } - 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); - } + @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; + } - 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), - }; - } + @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; + } - 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) { + @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)); - 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); - } + 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; } - } - 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; } - if (!present) { - 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; } - return array; - } - private int[] pushIfNotExists(int[] array, int value) { - boolean present = false; - for (int i : array) { - if (i == value) { - present = true; - break; - } + private int getSelectedItemPosition() { + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + return i; + } + } + return -1; } - 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; + + // 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"); + } } - 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() { + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_CUSTOM, TYPE_PRESETS}) + public @interface DialogType { } - /** - * 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(Activity) - */ - 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(Activity activity) { - create().show(activity.getFragmentManager(), "color-picker-dialog"); - } - - } - - @Retention(RetentionPolicy.SOURCE) - @IntDef({TYPE_CUSTOM, TYPE_PRESETS}) - public @interface DialogType { - - } - - // endregion + // endregion } diff --git a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java index 385b439d..92865e42 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPickerView.java @@ -46,953 +46,937 @@ import android.view.View; * 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; - } - - if( h > heightAllowed ){ - //We can't fit the view in this container, set the size to whatever was allowed. - finalHeight = heightAllowed; - }else{ - finalHeight = h; - } - - 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 ); - } - - if( w > widthAllowed ){ - //we can't fit within this container, set the size to whatever was allowed. - finalWidth = widthAllowed; - }else{ - finalWidth = w; - } - - 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() ); - } - - // private int getPreferredWidth() { - // //Our preferred width and height is 200dp for the square sat / val rectangle. - // int width = DrawingUtils.dpToPx(getContext(), 200); - // - // return (width + huePanelWidthPx + panelSpacingPx); - // } - // - // private int getPreferredHeight() { - // int height = DrawingUtils.dpToPx(getContext(), 200); - // - // if (showAlphaPanel) { - // height += panelSpacingPx + alphaPanelHeightPx; - // } - // return height; - // } - - @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 class BitmapCache { - - public Canvas canvas; - public Bitmap bitmap; - public float value; - } - - public interface OnColorChangedListener { - - void onColorChanged( int newColor ); - } - + + 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/ColorPreference.java b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java index e202fd73..96d5940b 100644 --- a/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java +++ b/colorpicker/src/main/java/com/jrummyapps/android/colorpicker/ColorPreference.java @@ -16,199 +16,217 @@ package com.jrummyapps.android.colorpicker; -import android.app.Activity; import android.content.Context; import android.content.res.TypedArray; import android.graphics.Color; import android.preference.Preference; -import androidx.annotation.ColorInt; -import androidx.annotation.NonNull; 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 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; + 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; + public ColorPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); } - 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 - ); + + public ColorPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs); } - 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); - Activity activity = (Activity) getContext(); - dialog.show(activity.getFragmentManager(), getFragmentTag()); + 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 onAttachedToActivity() { - super.onAttachedToActivity(); - - if (showDialog) { - Activity activity = (Activity) getContext(); - ColorPickerDialog fragment = - (ColorPickerDialog) activity.getFragmentManager().findFragmentByTag(getFragmentTag()); - if (fragment != null) { - // re-bind preference to fragment - fragment.setColorPickerDialogListener(this); - } + @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()); + } + } } - } - @Override protected void onBindView(View view) { - super.onBindView(view); - ColorPanelView preview = (ColorPanelView) view.findViewById(R.id.cpv_preference_preview_color_panel); - if (preview != null) { - preview.setColor(color); + @Nullable + private FragmentManager getFragmentManager() { + Context context = getContext(); + if (context instanceof FragmentActivity) { + return ((FragmentActivity) context).getSupportFragmentManager(); + } + return null; } - } - @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { - if (restorePersistedValue) { - color = getPersistedInt(0xFF000000); - } else { - color = (Integer) defaultValue; - persistInt(color); + @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 Object onGetDefaultValue(TypedArray a, int index) { - return a.getInteger(index, Color.BLACK); - } + @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 public void onColorSelected(int dialogId, @ColorInt int color) { - saveValue(color); - } + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + if (restorePersistedValue) { + color = getPersistedInt(0xFF000000); + } else { + color = (Integer) defaultValue; + persistInt(color); + } + } - @Override public void onDialogDismissed(int dialogId) { - // no-op - } + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInteger(index, Color.BLACK); + } - /** - * 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); - } + @Override + public void onColorSelected(int dialogId, @ColorInt int color) { + saveValue(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; - } + @Override + public void onDialogDismissed(int dialogId) { + // no-op + } - /** - * Get the colors that will be shown in the {@link ColorPickerDialog}. - * - * @return An array of color ints - */ - public int[] getPresets() { - return presets; - } + /** + * 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); + } - /** - * 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; - } + /** + * Set the colors shown in the {@link ColorPickerDialog}. + * + * @param presets An array of color ints + */ + public void setPresets(@NonNull int[] presets) { + this.presets = presets; + } - /** - * The tag used for the {@link ColorPickerDialog}. - * - * @return The tag - */ - public String getFragmentTag() { - return "color_" + getKey(); - } + /** + * Get the colors that will be shown in the {@link ColorPickerDialog}. + * + * @return An array of color ints + */ + public int[] getPresets() { + return presets; + } - public interface OnShowDialogListener { + /** + * 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; + } - void onShowColorPickerDialog(String title, int currentColor); - } + /** + * 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/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index d26fa4d3..c295eb39 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Wed May 05 20:20:56 JST 2021 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-6.9-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME diff --git a/sample_apng/build.gradle b/sample_apng/build.gradle index e5dcc411..d05ccb3e 100644 --- a/sample_apng/build.gradle +++ b/sample_apng/build.gradle @@ -44,10 +44,9 @@ dependencies { implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version" testImplementation "junit:junit:$junit_version" - androidTestImplementation 'androidx.test:runner:1.3.0' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + androidTestImplementation 'androidx.test:runner:1.4.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinx_coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlinx_coroutines_version" } diff --git a/sample_apng/src/main/AndroidManifest.xml b/sample_apng/src/main/AndroidManifest.xml index bb484f2c..f3c8983f 100644 --- a/sample_apng/src/main/AndroidManifest.xml +++ b/sample_apng/src/main/AndroidManifest.xml @@ -1,29 +1,32 @@ - + - + + android:theme="@style/AppTheme"> - + - + - + - + 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 929147aa..b07e247e 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 @@ -78,21 +78,12 @@ class ActList : AppCompatActivity(), CoroutineScope { permissions: Array, grantResults: IntArray ) { - when (requestCode) { - PERMISSION_REQUEST_CODE_STORAGE -> { - // If request is cancelled, the result arrays are empty. - if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) { - // permission was granted, yay! Do the - // contacts-related task you need to do. - } else { - // permission denied, boo! Disable the - // functionality that depends on this permission. - } - return + if( requestCode == PERMISSION_REQUEST_CODE_STORAGE){ + if (grantResults.all{ it == PackageManager.PERMISSION_GRANTED }) { + // 特に何もしてないらしい } - // other 'case' lines to check for other - // permissions this app might request } + super.onRequestPermissionsResult(requestCode, permissions, grantResults) } private fun load() = launch { diff --git a/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt b/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt index 5d781868..237daf0a 100644 --- a/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt +++ b/sample_apng/src/main/java/jp/juggler/apng/sample/ActViewer.kt @@ -117,7 +117,7 @@ class ActViewer : AppCompatActivity() , CoroutineScope { dir.mkdirs() if(! dir.exists() ) { - Log.e(TAG, "Directory not exists: ${dir}") + Log.e(TAG, "Directory not exists: $dir") return@launch } val frames = apngFrames.frames @@ -127,7 +127,7 @@ class ActViewer : AppCompatActivity() , CoroutineScope { } var i=0 for( f in frames) { - Log.d(TAG, "${title}[${i}] timeWidth=${f.timeWidth}") + Log.d(TAG, "$title[$i] timeWidth=${f.timeWidth}") val bitmap = f.bitmap FileOutputStream( File(dir,"${title}_${i}.png")).use{ fo ->