diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d1999ac --- /dev/null +++ b/.editorconfig @@ -0,0 +1,5 @@ +root = true +[*] +# Most of the standard properties are supported +indent_size=4 +max_line_length=100 \ No newline at end of file diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1f757e6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,16 @@ +# Lines starting with '#' are comments. +# Each line is a file pattern followed by one or more owners. + +# More details are here: https://help.github.com/articles/about-codeowners/ + +# The '*' pattern is global owners. +# Not adding in this PR, but I'd like to try adding a global owner set with the entire team. +# One interpretation of their docs is that global owners are added only if not removed +# by a more local rule. + +# Order is important. The last matching pattern has the most precedence. +# The folders are ordered as follows: + +# In each subsection folders are ordered first by depth, then alphabetically. +# This should make it easy to add new rules without breaking existing ones. +* @skydoves \ No newline at end of file diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..f7db2dd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,17 @@ +### 🎯 Goal +Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request. If it fixes a bug or resolves a feature request, be sure to link to that issue. + +### 🛠 Implementation details +Describe the implementation details for this Pull Request. + +### ✍️ Explain examples +Explain examples with code for this updates. + +### Preparing a pull request for review +Ensure your change is properly formatted by running: + +```gradle +$ ./gradlew spotlessApply +``` + +Please correct any failures before requesting a review. \ No newline at end of file diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..3604cfc --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,33 @@ +name: Android CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: set up JDK + uses: actions/setup-java@v1 + with: + java-version: 11 + + - name: Cache Gradle and wrapper + uses: actions/cache@v2 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }} + restore-keys: | + ${{ runner.os }}-gradle- + - name: Make Gradle executable + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew build \ No newline at end of file diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml new file mode 100644 index 0000000..dcff50e --- /dev/null +++ b/.github/workflows/publish-snapshot.yml @@ -0,0 +1,33 @@ +name: Publish Snapshot builds + +on: + push: + branches: + - main + +jobs: + publish: + name: Snapshot build and publish + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: adopt + java-version: 11 + - name: Release build + run: ./gradlew assemble --scan + - name: Source jar and dokka + run: ./gradlew androidSourcesJar javadocJar --scan + - name: Publish to MavenCentral + run: ./gradlew publishReleasePublicationToSonatypeRepository --scan + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} + SNAPSHOT: true diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..96f97a1 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,31 @@ +name: Publish + +on: + release: + types: [released] + +jobs: + publish: + name: Release build and publish + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v2 + - name: Set up JDK 11 + uses: actions/setup-java@v2 + with: + distribution: adopt + java-version: 11 + - name: Release build + run: ./gradlew assemble --scan + - name: Source jar and dokka + run: ./gradlew androidSourcesJar javadocJar --scan + - name: Publish to MavenCentral + run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository --scan + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} + SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SIGNING_KEY: ${{ secrets.SIGNING_KEY }} + SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..893e47d --- /dev/null +++ b/.gitignore @@ -0,0 +1,60 @@ +# Built application files +*.apk +*.ap_ + +# Files for the ART/Dalvik VM +*.dex + +# Java class files +*.class + +# Generated files +bin/ +gen/ +out/ + +# Gradle files +/.idea +.gradle/ +build/ + +# Local configuration file (sdk path, etc) +local.properties + +# Proguard folder generated by Eclipse +proguard/ + +# Log Files +*.log + +# Android Studio Navigation editor temp files +.navigation/ + +# Android Studio captures folder +captures/ + +# Intellij +*.iml +.idea/workspace.xml +.idea/tasks.xml +.idea/gradle.xml +.idea/dictionaries +.idea/libraries +app/.idea/ + +# Mac +*.DS_Store + +# Keystore files +*.jks + +# External native build folder generated in Android Studio 2.2 and later +.externalNativeBuild + +# Google Services (e.g. APIs or Firebase) +google-services.json + +# Temporary API docs +docs/api + +*.gpg \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..91a3b23 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,20 @@ +## How to contribute +We'd love to accept your patches and contributions to this project. There are just a few small guidelines you need to follow. + +## Preparing a pull request for review +Ensure your change is properly formatted by running: + +```gradle +./gradlew spotlessApply +``` + +Then dump binary API of this library that is public in sense of Kotlin visibilities and ensures that the public binary API wasn't changed in a way that make this change binary incompatible. + +```gradle +./gradlew apiDump +``` + +Please correct any failures before requesting a review. + +## Code reviews +All submissions, including submissions by project members, require review. We use GitHub pull requests for this purpose. Consult [GitHub Help](https://docs.github.com/en/github/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/about-pull-requests) for more information on using pull requests. diff --git a/README.md b/README.md new file mode 100644 index 0000000..affbd4c --- /dev/null +++ b/README.md @@ -0,0 +1,324 @@ +

ColorPicker Compose


+ +

+ License + API +


+ +

+🎨 Jetpack Compose color picker library that allows you to get colors from any images like gallery pictures by tapping on the desired color. +Also, it supports brightness and alpha slider, which can adjust your ARGB factors. +


+ +## Preview +

+ + + +

+ +## Download +[![Maven Central](https://img.shields.io/maven-central/v/com.github.skydoves/colorpicker-compose.svg?label=Maven%20Central)](https://search.maven.org/search?q=g:%22com.github.skydoves%22%20AND%20a:%22colorpicker-compose%22) + +### Gradle +Add the dependency below to your **module**'s `build.gradle` file: +```gradle +dependencies { + implementation "com.github.skydoves:colorpicker-compose:1.0.0" +} +``` + +## SNAPSHOT + +
+ See how to import the snapshot + +### Including the SNAPSHOT +Snapshots of the current development version of ColorPicker-Compose are available, which track [the latest versions](https://oss.sonatype.org/content/repositories/snapshots/com/github/skydoves/colorpicker-compose/). + +To import snapshot versions on your project, add the code snippet below on your gradle file: +```Gradle +repositories { + maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } +} +``` + +Next, add the dependency below to your **module**'s `build.gradle` file: +```gradle +dependencies { + implementation "com.github.skydoves:colorpicker-compose:1.0.1-SNAPSHOT" +} +``` + +
+ +## Usage + +First, you should initialize `ColorPickerController`, which allows you to control color pickers and all subcomponents. + +```kotlin +val controller = rememberColorPickerController() +``` + +Next, you can implement a color picker with the `ImageColorPicker` composable function. + +```kotlin +ImageColorPicker( + modifier = Modifier.fillMaxSize(), + paletteImageBitmap = ImageBitmap.imageResource(R.drawable.palettebar), + controller = controller +) +``` + + + +### ImageColorPicker + +**ImageColorPicker** allows you to get colors from any images such as gallery pictures or drawble resources by tapping on the desired color. +It interacts with the `ColorPickerController` to control the color picker and other components. You can use the `ImageColorPicker` as the following example: + +```kotlin +ImageColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(450.dp) + .padding(10.dp), + controller = controller, + paletteImageBitmap = ImageBitmap.imageResource(R.drawable.palettebar), + paletteContentScale = PaletteContentScale.FIT, + onColorChanged = { colorEnvelope: ColorEnvelope -> + // do something + } +) +``` + +With the [modernstorage](https://github.com/google/modernstorage)'s [Photo Picker](https://google.github.io/modernstorage/photopicker/), you can set an desired image as the palette like the below: + +```kotlin +val context = LocalContext.current +val photoPicker = + rememberLauncherForActivityResult(PhotoPicker()) { uris -> + val uri = uris.firstOrNull() ?: return@rememberLauncherForActivityResult + + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(context.contentResolver, uri) + } + + controller.setPaletteImageBitmap(bitmap.asImageBitmap()) + } +``` + +As you can see the above, you can set the palette with the `setPaletteImageBitmap` function of the controller. + +#### PaletteContentScale + +You can adjust your palette's image scale with the `setPaletteContentScale` function of the controller as the below: + +```kotlin +controller.setPaletteContentScale(PaletteContentScale.FIT) // scale the image to fit width and height. +controller.setPaletteContentScale(PaletteContentScale.CROP) // center crop the image. +``` + + + +### HsvColorPicker + +HsvColorPicker allows you to get colors from HSV color palette by tapping on the desired color. +It interacts with the `ColorPickerController` to control the color picker and other components. You can use the `HsvColorPicker` as the following example: + +```kotlin +HsvColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(450.dp) + .padding(10.dp), + controller = controller, + onColorChanged = { colorEnvelope: ColorEnvelope -> + // do something + } +) +``` + +> **Note**: If you use `HsvColorPicker`, you can not set the palette and content scale with the `setPaletteImageBitmap` and `setPaletteContentScale` functions. + +### ColorEnvelope + +**ColorEnvelope** is a data transfer object that includes updated color factors. If you pass the **onColorChanged** lambda function to the `ImageColorPicker` or `HsvColorPicker`, the lambda receives **ColorEnvelope**. +**ColorEnvelope** includes the following properties: + +```kotlin +onColorChanged = { colorEnvelope: ColorEnvelope -> + val color: Color = colorEnvelope.color // ARGB color value. + val hexCode: String = colorEnvelope.hexCode // Color hex code, which represents color value. + val fromUser: Boolean = colorEnvelope.fromUser // Represents this event is triggered by user or not. +} +``` + +### ColorPickerController + +**ColorPickerController** interacts with color pickers and it allows you to control the all subcomponents. + +#### Custom Wheel + +You can customize the wheel with the following functions: + +```kotlin +.setWheelRadius(40.dp) // set the radius size of the wheel. +.setWheelColor(Color.Blue) // set the color of the wheel. +.setWheelAlpha(0.5f) // set the transparency of the wheel. +.setWheelImageBitmap(imageBitmap) // set the wheel image with your custom ImageBitmap. +``` + +#### Select Points + +You can select specific points with the functions below: + +```kotlin +.selectByCoordinate(x = 100f, y = 100f, fromUser = false) // select x = 100, y = 100. +.selectCenter(fromUser = false) // select center of the palette. +``` + +#### Debounce + +You can set the debounce duration, which decides to invoke the color listener from the last tapping. Debounce can be useful to reduce overhead. For example, communicating with IoT devices or relevant works that require heavy operations. + +```kotlin +.setDebounceDuration(300L) +``` + +#### Enable and Disable + +You can enable or disable your color picker with the below function: + +```kotlin +.setEnabled(false) +``` + + + +### AlphaSlider + +**AlphaSlider** allows you to adjust the alpha value of the selected color from color pickers. +**AlphaSlider** needs to be tied to the `ColorPickerController`, and the value changes will be assembled with the selected color factors. +You can implement **AlphaSlider** as the following example: + +```kotlin +AlphaSlider( + modifier = Modifier + .fillMaxWidth() + .height(35.dp) + .padding(10.dp) + controller = controller, +) +``` + +You can customize the border of the sider with the following parameters: + +```kotlin +AlphaSlider( + borderRadius = 6.dp, + borderSize = 5.dp, + borderColor = Color.LightGray, + .. +) +``` + +You can customize the wheel of the sider with the following parameters: + +```kotlin +AlphaSlider( + wheelRadius = 30.dp, + wheelColor = Color.White, + wheelPaint = Paint().apply { color = wheelColor }, + wheelImageBitmap = ImageBitmap.imageResource(R.drawable.wheel), + .. +) +``` + +Also, you can customize tiles of the background with the following parameters: + +```kotlin +AlphaSlider( + tileOddColor = Color.White, + tileEvenColor = Color.LightGray, + tileSize = 30.dp, + .. +) +``` + + + +### BrightnessSlider + +**BrightnessSlider** allows you to adjust the brightness value of the selected color from color pickers. +**BrightnessSlider** needs to be tied to the `ColorPickerController`, and the value changes will be assembled with the selected color factors. +You can implement **BrightnessSlider** as the following example: + +```kotlin +BrightnessSlider( + modifier = Modifier + .fillMaxWidth() + .height(35.dp) + .padding(10.dp) + controller = controller, +) +``` + +You can customize the wheel of the sider with the following parameters: + +```kotlin +BrightnessSlider( + wheelRadius = 30.dp, + wheelColor = Color.White, + wheelPaint = Paint().apply { color = wheelColor }, + wheelImageBitmap = ImageBitmap.imageResource(R.drawable.wheel), + .. +) +``` + +### AlphaTile + +**AlphaTile** allows you to display ARGB colors including transparency with tiles. + +```kotlin +AlphaTile( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(6.dp)) + controller = controller +) +``` + +Also, you can customize tiles of the background with the following parameters: + +```kotlin +AlphaTile( + tileOddColor = Color.White, + tileEvenColor = Color.LightGray, + tileSize = 30.dp, + .. +) +``` + +## Find this repository useful? :heart: +Support it by joining __[stargazers](https://github.com/skydoves/colorpicker-compose/stargazers)__ for this repository. :star:
+Also, __[follow me](https://github.com/skydoves)__ on GitHub for my next creations! 🤩 + +# License +```xml +Designed and developed by 2022 skydoves (Jaewoong Eum) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +``` diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..9e9749b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,62 @@ +import com.github.skydoves.colorpicker.compose.Configuration +import com.github.skydoves.colorpicker.compose.Dependencies +import com.github.skydoves.colorpicker.compose.Versions + +plugins { + id 'com.android.application' + id 'org.jetbrains.kotlin.android' +} + +android { + compileSdkVersion Configuration.compileSdk + defaultConfig { + applicationId "com.github.skydoves.colorpickercomposedemo" + minSdkVersion Configuration.minSdk + targetSdkVersion Configuration.targetSdk + versionCode Configuration.versionCode + versionName Configuration.versionName + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + composeOptions { + kotlinCompilerExtensionVersion Versions.COMPOSE + } + + buildFeatures { + compose true + } + + packagingOptions { + resources { + excludes += '/META-INF/{AL2.0,LGPL2.1}' + } + } + + lintOptions { + abortOnError false + } +} + +dependencies { + implementation project(":colorpicker-compose") + + implementation Dependencies.material + + implementation Dependencies.composeUI + implementation Dependencies.composeMaterial + implementation Dependencies.composeRuntime + implementation Dependencies.composeTooling + implementation Dependencies.composeActivity + + implementation Dependencies.photoPicker +} + +apply from: "$rootDir/spotless.gradle" \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..c5b2a0d --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainActivity.kt b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainActivity.kt new file mode 100644 index 0000000..fa4fdb5 --- /dev/null +++ b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainActivity.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpickercomposedemo + +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + MainScreen() + } + } +} diff --git a/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainScreen.kt b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainScreen.kt new file mode 100644 index 0000000..50d5e33 --- /dev/null +++ b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/MainScreen.kt @@ -0,0 +1,106 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpickercomposedemo + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.res.imageResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.github.skydoves.colorpicker.compose.AlphaSlider +import com.github.skydoves.colorpicker.compose.AlphaTile +import com.github.skydoves.colorpicker.compose.BrightnessSlider +import com.github.skydoves.colorpicker.compose.ColorEnvelope +import com.github.skydoves.colorpicker.compose.ImageColorPicker +import com.github.skydoves.colorpicker.compose.rememberColorPickerController + +@Composable +fun MainScreen() { + val controller = rememberColorPickerController() + val hexCode = remember { mutableStateOf("") } + val textColor = remember { mutableStateOf(Color.Transparent) } + + Column { + PhotoPickerIcon(controller) + + ImageColorPicker( + modifier = Modifier + .fillMaxWidth() + .height(320.dp) + .padding(10.dp), + controller = controller, + paletteImageBitmap = ImageBitmap.imageResource(R.drawable.palettebar), + onColorChanged = { colorEnvelope: ColorEnvelope -> + hexCode.value = colorEnvelope.hexCode + textColor.value = colorEnvelope.color + } + ) + + Spacer(modifier = Modifier.height(50.dp)) + + AlphaSlider( + modifier = Modifier + .fillMaxWidth() + .height(35.dp) + .padding(10.dp) + .align(Alignment.CenterHorizontally), + controller = controller, + ) + + BrightnessSlider( + modifier = Modifier + .fillMaxWidth() + .height(35.dp) + .padding(10.dp) + .align(Alignment.CenterHorizontally), + controller = controller, + ) + + Spacer(modifier = Modifier.height(30.dp)) + + Text( + text = "#${hexCode.value}", + color = textColor.value, + fontSize = 16.sp, + fontWeight = FontWeight.Bold, + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + AlphaTile( + modifier = Modifier + .size(80.dp) + .clip(RoundedCornerShape(6.dp)) + .align(Alignment.CenterHorizontally), + controller = controller + ) + } +} diff --git a/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt new file mode 100644 index 0000000..81fdfc2 --- /dev/null +++ b/app/src/main/kotlin/com/github/skydoves/colorpickercomposedemo/PhotoPickerIcon.kt @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpickercomposedemo + +import android.annotation.SuppressLint +import android.graphics.ImageDecoder +import android.os.Build +import android.provider.MediaStore +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.unit.dp +import com.github.skydoves.colorpicker.compose.ColorPickerController +import com.google.modernstorage.photopicker.PhotoPicker + +@Composable +@SuppressLint("UnsafeOptInUsageError") +fun ColumnScope.PhotoPickerIcon( + controller: ColorPickerController +) { + val context = LocalContext.current + val photoPicker = + rememberLauncherForActivityResult(PhotoPicker()) { uris -> + val uri = uris.firstOrNull() ?: return@rememberLauncherForActivityResult + + val bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + ImageDecoder.decodeBitmap(ImageDecoder.createSource(context.contentResolver, uri)) + } else { + MediaStore.Images.Media.getBitmap(context.contentResolver, uri) + } + + controller.setPaletteImageBitmap(bitmap.asImageBitmap()) + } + + Box( + modifier = Modifier + .padding(horizontal = 16.dp) + .align(Alignment.End) + ) { + Image( + modifier = Modifier + .size(42.dp) + .clickable { + // Launch the picker with only one image selectable + photoPicker.launch(PhotoPicker.Args(PhotoPicker.Type.IMAGES_ONLY, 1)) + }, + imageVector = ImageVector.vectorResource(R.drawable.ic_gallery), + contentDescription = null + ) + } +} diff --git a/app/src/main/res/drawable-hdpi/ic_circle.png b/app/src/main/res/drawable-hdpi/ic_circle.png new file mode 100644 index 0000000..df78811 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/ic_circle.png differ diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_gallery.xml b/app/src/main/res/drawable/ic_gallery.xml new file mode 100644 index 0000000..aec96b2 --- /dev/null +++ b/app/src/main/res/drawable/ic_gallery.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..07d5da9 --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/palettebar.jpg b/app/src/main/res/drawable/palettebar.jpg new file mode 100644 index 0000000..a0a1342 Binary files /dev/null and b/app/src/main/res/drawable/palettebar.jpg differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.webp b/app/src/main/res/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 0000000..c209e78 Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 0000000..b2dfe3d Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.webp b/app/src/main/res/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 0000000..4f0f1d6 Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 0000000..62b611d Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 0000000..948a307 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..1b9a695 Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 0000000..28d4b77 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9287f50 Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 0000000..aa7d642 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 0000000..9126ae3 Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml new file mode 100644 index 0000000..abbf75d --- /dev/null +++ b/app/src/main/res/values-night/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/app/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..dea6d12 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + ColorPickerComposeDemo + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml new file mode 100644 index 0000000..7124f7b --- /dev/null +++ b/app/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..62acef4 --- /dev/null +++ b/build.gradle @@ -0,0 +1,26 @@ +import com.github.skydoves.colorpicker.compose.Dependencies + +apply plugin: 'io.github.gradle-nexus.publish-plugin' +apply plugin: 'org.jetbrains.dokka' + +buildscript { + repositories { + google() + mavenCentral() + maven { url "https://plugins.gradle.org/m2/" } + } + dependencies { + classpath Dependencies.androidGradlePlugin + classpath Dependencies.kotlinGradlePlugin + classpath Dependencies.spotlessGradlePlugin + classpath Dependencies.gradleNexusPublishPlugin + classpath Dependencies.dokka + classpath Dependencies.kotlinBinaryValidator + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} + +apply from: "${rootDir}/scripts/publish-root.gradle" \ No newline at end of file diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts new file mode 100644 index 0000000..b22ed73 --- /dev/null +++ b/buildSrc/build.gradle.kts @@ -0,0 +1,7 @@ +plugins { + `kotlin-dsl` +} + +repositories { + mavenCentral() +} \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt new file mode 100644 index 0000000..67d48cc --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Configuration.kt @@ -0,0 +1,14 @@ +package com.github.skydoves.colorpicker.compose + +object Configuration { + const val compileSdk = 31 + const val targetSdk = 31 + const val minSdk = 21 + const val majorVersion = 1 + const val minorVersion = 0 + const val patchVersion = 0 + const val versionName = "$majorVersion.$minorVersion.$patchVersion" + const val versionCode = 1 + const val snapshotVersionName = "$majorVersion.$minorVersion.${patchVersion + 1}-SNAPSHOT" + const val artifactGroup = "com.github.skydoves" +} diff --git a/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Dependencies.kt b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Dependencies.kt new file mode 100644 index 0000000..9bcdbb6 --- /dev/null +++ b/buildSrc/src/main/kotlin/com/github/skydoves/colorpicker/compose/Dependencies.kt @@ -0,0 +1,48 @@ +package com.github.skydoves.colorpicker.compose + +object Versions { + internal const val ANDROID_GRADLE_PLUGIN = "7.1.0" + internal const val ANDROID_GRADLE_SPOTLESS = "6.1.0" + internal const val GRADLE_NEXUS_PUBLISH_PLUGIN = "1.1.0" + internal const val KOTLIN = "1.6.10" + internal const val KOTLIN_GRADLE_DOKKA = "1.6.10" + internal const val KOTLIN_BINARY_VALIDATOR = "0.8.0" + internal const val KOTLIN_COROUTINE = "1.5.2" + + internal const val MATERIAL = "1.5.0" + internal const val ANDROIDX_CORE_KTX = "1.5.0" + + internal const val COMPOSE = "1.1.1" + internal const val COMPOSE_ACTIVITY = "1.4.0" + internal const val COMPOSE_MATERIAL3 = "1.0.0-alpha02" + internal const val COLOR_PICKER = "1.1.3" + internal const val PHOTO_PICKER = "1.0.0-alpha06" +} + +object Dependencies { + const val androidGradlePlugin = + "com.android.tools.build:gradle:${Versions.ANDROID_GRADLE_PLUGIN}" + const val gradleNexusPublishPlugin = + "io.github.gradle-nexus:publish-plugin:${Versions.GRADLE_NEXUS_PUBLISH_PLUGIN}" + const val kotlinGradlePlugin = "org.jetbrains.kotlin:kotlin-gradle-plugin:${Versions.KOTLIN}" + const val spotlessGradlePlugin = + "com.diffplug.spotless:spotless-plugin-gradle:${Versions.ANDROID_GRADLE_SPOTLESS}" + const val dokka = "org.jetbrains.dokka:dokka-gradle-plugin:${Versions.KOTLIN_GRADLE_DOKKA}" + const val kotlinBinaryValidator = + "org.jetbrains.kotlinx:binary-compatibility-validator:${Versions.KOTLIN_BINARY_VALIDATOR}" + const val coroutines = + "org.jetbrains.kotlinx:kotlinx-coroutines-core:${Versions.KOTLIN_COROUTINE}" + + const val material = "com.google.android.material:material:${Versions.MATERIAL}" + const val androidxCoreKtx = "androidx.core:core-ktx:${Versions.ANDROIDX_CORE_KTX}" + + const val composeUI = "androidx.compose.ui:ui:${Versions.COMPOSE}" + const val composeRuntime = "androidx.compose.runtime:runtime:${Versions.COMPOSE}" + const val composeMaterial = "androidx.compose.material:material:${Versions.COMPOSE}" + const val composeFoundation = "androidx.compose.foundation:foundation:${Versions.COMPOSE}" + const val composeTooling = "androidx.compose.ui:ui-tooling:${Versions.COMPOSE}" + const val composeActivity = "androidx.activity:activity-compose:${Versions.COMPOSE_ACTIVITY}" + const val composeMaterial3 = "androidx.compose.material3:material3:${Versions.COMPOSE_MATERIAL3}" + const val colorPicker = "com.github.skydoves:orchestra-colorpicker:${Versions.COLOR_PICKER}" + const val photoPicker = "com.google.modernstorage:modernstorage-photopicker:${Versions.PHOTO_PICKER}" +} diff --git a/colorpicker-compose/.gitignore b/colorpicker-compose/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/colorpicker-compose/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/colorpicker-compose/api/colorpicker-compose.api b/colorpicker-compose/api/colorpicker-compose.api new file mode 100644 index 0000000..707c104 --- /dev/null +++ b/colorpicker-compose/api/colorpicker-compose.api @@ -0,0 +1,74 @@ +public final class com/github/skydoves/colorpicker/compose/AlphaSliderKt { + public static final fun AlphaSlider-1sSiv8M (Landroidx/compose/ui/Modifier;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;FFJLandroidx/compose/ui/graphics/ImageBitmap;FJLandroidx/compose/ui/graphics/Paint;JJFLandroidx/compose/runtime/Composer;III)V +} + +public final class com/github/skydoves/colorpicker/compose/AlphaTileDrawable : android/graphics/drawable/Drawable { + public static final field $stable I + public synthetic fun (FJJLkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun draw (Landroid/graphics/Canvas;)V + public fun getOpacity ()I + public fun setAlpha (I)V + public fun setColorFilter (Landroid/graphics/ColorFilter;)V +} + +public final class com/github/skydoves/colorpicker/compose/AlphaTileKt { + public static final fun AlphaTile-Qd0NJH0 (Landroidx/compose/ui/Modifier;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;JJJFLandroidx/compose/runtime/Composer;II)V +} + +public final class com/github/skydoves/colorpicker/compose/BrightnessSliderKt { + public static final fun BrightnessSlider-kPGd43Y (Landroidx/compose/ui/Modifier;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;FFJLandroidx/compose/ui/graphics/ImageBitmap;FJLandroidx/compose/ui/graphics/Paint;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/github/skydoves/colorpicker/compose/ColorEnvelope { + public static final field $stable I + public synthetic fun (JLjava/lang/String;ZLkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1-0d7_KjU ()J + public final fun component2 ()Ljava/lang/String; + public final fun component3 ()Z + public final fun copy-ek8zF_U (JLjava/lang/String;Z)Lcom/github/skydoves/colorpicker/compose/ColorEnvelope; + public static synthetic fun copy-ek8zF_U$default (Lcom/github/skydoves/colorpicker/compose/ColorEnvelope;JLjava/lang/String;ZILjava/lang/Object;)Lcom/github/skydoves/colorpicker/compose/ColorEnvelope; + public fun equals (Ljava/lang/Object;)Z + public final fun getColor-0d7_KjU ()J + public final fun getFromUser ()Z + public final fun getHexCode ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/github/skydoves/colorpicker/compose/ColorPickerController { + public static final field $stable I + public fun ()V + public final fun getSelectedColor ()Landroidx/compose/runtime/State; + public final fun getSelectedPoint ()Landroidx/compose/runtime/State; + public final fun selectByCoordinate (FFZ)V + public final fun selectCenter (Z)V + public final fun setDebounceDuration (J)V + public final fun setEnabled (Z)V + public final fun setPaletteContentScale (Lcom/github/skydoves/colorpicker/compose/PaletteContentScale;)V + public final fun setPaletteImageBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V + public final fun setWheelAlpha (F)V + public final fun setWheelColor-8_81llA (J)V + public final fun setWheelImageBitmap (Landroidx/compose/ui/graphics/ImageBitmap;)V + public final fun setWheelPaint (Landroidx/compose/ui/graphics/Paint;)V + public final fun setWheelRadius-0680j_4 (F)V +} + +public final class com/github/skydoves/colorpicker/compose/ColorPickerControllerKt { + public static final fun rememberColorPickerController (Landroidx/compose/runtime/Composer;I)Lcom/github/skydoves/colorpicker/compose/ColorPickerController; +} + +public final class com/github/skydoves/colorpicker/compose/HsvColorPickerKt { + public static final fun HsvColorPicker (Landroidx/compose/ui/Modifier;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;Landroidx/compose/ui/graphics/ImageBitmap;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/github/skydoves/colorpicker/compose/ImageColorPickerKt { + public static final fun ImageColorPicker (Landroidx/compose/ui/Modifier;Lcom/github/skydoves/colorpicker/compose/ColorPickerController;Landroidx/compose/ui/graphics/ImageBitmap;Landroidx/compose/ui/graphics/ImageBitmap;Lcom/github/skydoves/colorpicker/compose/PaletteContentScale;Lkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)V +} + +public final class com/github/skydoves/colorpicker/compose/PaletteContentScale : java/lang/Enum { + public static final field CROP Lcom/github/skydoves/colorpicker/compose/PaletteContentScale; + public static final field FIT Lcom/github/skydoves/colorpicker/compose/PaletteContentScale; + public static fun valueOf (Ljava/lang/String;)Lcom/github/skydoves/colorpicker/compose/PaletteContentScale; + public static fun values ()[Lcom/github/skydoves/colorpicker/compose/PaletteContentScale; +} + diff --git a/colorpicker-compose/build.gradle b/colorpicker-compose/build.gradle new file mode 100644 index 0000000..8e65bae --- /dev/null +++ b/colorpicker-compose/build.gradle @@ -0,0 +1,66 @@ +import com.github.skydoves.colorpicker.compose.Configuration +import com.github.skydoves.colorpicker.compose.Dependencies +import com.github.skydoves.colorpicker.compose.Versions + +plugins { + id 'com.android.library' + id 'org.jetbrains.kotlin.android' + id 'org.jetbrains.dokka' + id 'binary-compatibility-validator' +} + +ext { + PUBLISH_GROUP_ID = Configuration.artifactGroup + PUBLISH_ARTIFACT_ID = 'colorpicker-compose' + PUBLISH_VERSION = rootVersionName +} + +apply from: "${rootDir}/scripts/publish-module.gradle" + +android { + compileSdkVersion Configuration.compileSdk + defaultConfig { + minSdkVersion Configuration.minSdk + targetSdkVersion Configuration.targetSdk + } + + buildFeatures { + compose true + buildConfig false + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + composeOptions { + kotlinCompilerExtensionVersion Versions.COMPOSE + } + + kotlinOptions { + jvmTarget = '1.8' + } + + lintOptions { + abortOnError false + } +} + +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions.freeCompilerArgs += [ + "-Xexplicit-api=strict", + "-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi" + ] +} + +dependencies { + implementation Dependencies.androidxCoreKtx + implementation Dependencies.composeUI + implementation Dependencies.composeRuntime + implementation Dependencies.composeMaterial + implementation Dependencies.composeFoundation + implementation Dependencies.composeTooling +} + +apply from: "$rootDir/spotless.gradle" \ No newline at end of file diff --git a/colorpicker-compose/src/main/AndroidManifest.xml b/colorpicker-compose/src/main/AndroidManifest.xml new file mode 100644 index 0000000..ad34c2c --- /dev/null +++ b/colorpicker-compose/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaSlider.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaSlider.kt new file mode 100644 index 0000000..15af09a --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaSlider.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + +/** + * AlphaSlider allows you to adjust the alpha value of the selected color from color pickers. + * + * @param modifier [Modifier] to decorate the internal Canvas. + * @param controller Allows you to control and interacts with color pickers and all relevant subcomponents. + * @param borderRadius Radius of the border. + * @param borderSize [Dp] size of the border. + * @param borderColor [Color] of the border. + * @param wheelImageBitmap [ImageBitmap] to draw the wheel. + * @param wheelRadius Radius of the wheel. + * @param wheelColor [Color] of th wheel. + * @param wheelPaint [Paint] to draw the wheel. + * @param tileOddColor Color of the odd tiles. + * @param tileEvenColor Color of the even tiles. + * @param tileSize DP size of tiles. + */ +@Composable +public fun AlphaSlider( + modifier: Modifier, + controller: ColorPickerController, + borderRadius: Dp = 6.dp, + borderSize: Dp = 5.dp, + borderColor: Color = Color.LightGray, + wheelImageBitmap: ImageBitmap? = null, + wheelRadius: Dp = 30.dp, + wheelColor: Color = Color.White, + wheelPaint: Paint = Paint().apply { color = wheelColor }, + tileOddColor: Color = defaultTileOddColor, + tileEvenColor: Color = defaultTileEvenColor, + tileSize: Dp = 30.dp, +) { + var backgroundBitmap: ImageBitmap? = null + var bitmapSize = IntSize(0, 0) + val borderPaint: Paint = Paint().apply { + style = PaintingStyle.Stroke + strokeWidth = borderSize.value + color = borderColor + } + val colorPaint: Paint = Paint().apply { + color = controller.pureSelectedColor.value + } + + LaunchedEffect(controller) { + controller.isAttachedAlphaSlider = true + } + + Canvas( + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(borderRadius)) + .onSizeChanged { newSize -> + val size = + newSize.takeIf { it.width != 0 && it.height != 0 } ?: return@onSizeChanged + val drawable = AlphaTileDrawable(tileSize, tileOddColor, tileEvenColor) + backgroundBitmap + ?.asAndroidBitmap() + ?.recycle() + backgroundBitmap = + ImageBitmap(size.width, size.height, ImageBitmapConfig.Argb8888).apply { + val backgroundCanvas = Canvas(this) + drawable.setBounds( + 0, + 0, + backgroundCanvas.nativeCanvas.width, + backgroundCanvas.nativeCanvas.height + ) + drawable.draw(backgroundCanvas.nativeCanvas) + backgroundCanvas.drawRoundRect( + left = 0f, + top = 0f, + right = size.width.toFloat(), + bottom = size.height.toFloat(), + radiusX = borderRadius.value, + radiusY = borderRadius.value, + paint = borderPaint + ) + } + bitmapSize = size + } + .pointerInteropFilter { event -> + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_UP -> { + // calculate wheel position. + val wheelPoint = event.x + val position: Float = if (wheelImageBitmap == null) { + val sliderWidth = + bitmapSize.width - (wheelRadius.value + borderSize.value) * 2 + val point = wheelPoint.coerceIn( + minimumValue = wheelRadius.value + borderSize.value, + maximumValue = bitmapSize.width - wheelRadius.value - borderSize.value, + ) + point / sliderWidth + } else { + val sliderWidth = + bitmapSize.width - (borderSize.value) * 2 - wheelImageBitmap.width + val point = wheelPoint.coerceIn( + minimumValue = borderSize.value, + maximumValue = bitmapSize.width.toFloat() - wheelImageBitmap.width - borderSize.value + ) + point / sliderWidth + } + controller.setAlpha(position.coerceIn(0f, 1f), fromUser = true) + true + } + else -> false + } + } + ) { + drawIntoCanvas { canvas -> + backgroundBitmap?.let { + // draw background bitmap. + canvas.drawImage(it, Offset.Zero, Paint()) + + // draw a linear gradient color shader. + val startColor = controller.pureSelectedColor.value.copy(alpha = 0f) + val endColor = controller.pureSelectedColor.value.copy(alpha = 1f) + val shader = LinearGradientShader( + colors = listOf(startColor, endColor), + from = Offset.Zero, + to = Offset(bitmapSize.width.toFloat(), bitmapSize.height.toFloat()), + tileMode = TileMode.Clamp + ) + colorPaint.shader = shader + canvas.drawRoundRect( + left = 0f, + top = 0f, + right = bitmapSize.width.toFloat(), + bottom = bitmapSize.height.toFloat(), + radiusX = borderRadius.value, + radiusY = borderRadius.value, + paint = colorPaint + ) + + // draw wheel bitmap on the canvas. + if (wheelImageBitmap == null) { + val position = controller.alpha.value + val point = (position * bitmapSize.width).coerceIn( + minimumValue = wheelRadius.value + borderSize.value, + maximumValue = bitmapSize.width - wheelRadius.value - borderSize.value, + ) + canvas.drawCircle( + Offset(x = point, y = bitmapSize.height / 2f), + wheelRadius.value, + wheelPaint + ) + } else { + val position = controller.alpha.value + val point = (position * bitmapSize.width).coerceIn( + minimumValue = borderSize.value, + maximumValue = bitmapSize.width.toFloat() - wheelImageBitmap.width - borderSize.value + ) + canvas.drawImage( + wheelImageBitmap, + Offset(x = point, y = bitmapSize.height / 2f - wheelImageBitmap.height / 2), + Paint() + ) + } + } + } + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTile.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTile.kt new file mode 100644 index 0000000..8afc5a3 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTile.kt @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + +/** + * AlphaTile allows you to display ARGB colors including transparency with tiles. + * + * @param modifier [Modifier] to decorate the internal Canvas. + * @param controller Allows you to control and interacts with color pickers and all relevant subcomponents. + * @param selectedColor Color to be displayed over the tiles if the [controller] is not registered. + * @param tileOddColor Color of the odd tiles. + * @param tileEvenColor Color of the even tiles. + * @param tileSize DP size of tiles. + */ +@Composable +public fun AlphaTile( + modifier: Modifier, + controller: ColorPickerController? = null, + selectedColor: Color = Color.Transparent, + tileOddColor: Color = defaultTileOddColor, + tileEvenColor: Color = defaultTileEvenColor, + tileSize: Dp = 30.dp +) { + var backgroundBitmap: ImageBitmap? = null + var bitmapSize = IntSize(0, 0) + val colorPaint: Paint = Paint().apply { + this.color = controller?.selectedColor?.value ?: selectedColor + } + + Canvas( + modifier = modifier + .fillMaxSize() + .onSizeChanged { newSize -> + val size = + newSize.takeIf { it.width != 0 && it.height != 0 } ?: return@onSizeChanged + val drawable = AlphaTileDrawable(tileSize, tileOddColor, tileEvenColor) + backgroundBitmap + ?.asAndroidBitmap() + ?.recycle() + backgroundBitmap = + ImageBitmap(size.width, size.height, ImageBitmapConfig.Argb8888).apply { + val backgroundCanvas = Canvas(this) + drawable.setBounds( + 0, + 0, + backgroundCanvas.nativeCanvas.width, + backgroundCanvas.nativeCanvas.height + ) + drawable.draw(backgroundCanvas.nativeCanvas) + } + bitmapSize = size + } + ) { + drawIntoCanvas { canvas -> + backgroundBitmap?.let { + canvas.drawImage(it, Offset.Zero, Paint()) + canvas.drawRect( + 0f, + 0f, + bitmapSize.width.toFloat(), + bitmapSize.height.toFloat(), + colorPaint + ) + } + } + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTileDrawable.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTileDrawable.kt new file mode 100644 index 0000000..e8c5455 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/AlphaTileDrawable.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.BitmapShader +import android.graphics.ColorFilter +import android.graphics.PixelFormat +import android.graphics.Shader +import android.graphics.drawable.Drawable +import androidx.compose.ui.geometry.Rect +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.unit.Dp + +/** + * AlphaTileDrawable displays ARGB colors including transparency with tiles on a canvas. + * + * @param tileSize DP size of tiles. + * @param tileOddColor Color of the odd tiles. + * @param tileEvenColor Color of the even tiles. + */ +public class AlphaTileDrawable constructor( + tileSize: Dp, + tileOddColor: Color, + tileEvenColor: Color, +) : Drawable() { + + private val androidPaint: android.graphics.Paint = android.graphics.Paint( + android.graphics.Paint.ANTI_ALIAS_FLAG + ) + + init { + val sizeF = tileSize.value + val size = sizeF.toInt() + val imageBitmap = ImageBitmap(size * 2, size * 2, ImageBitmapConfig.Argb8888) + val canvas = Canvas(imageBitmap) + val rect = Rect(0f, 0f, sizeF, sizeF) + + val bitmapPaint = Paint().apply { + style = PaintingStyle.Fill + isAntiAlias = true + } + + bitmapPaint.color = tileOddColor + drawTile(canvas, rect, bitmapPaint, 0f, 0f) + drawTile(canvas, rect, bitmapPaint, sizeF, sizeF) + + bitmapPaint.color = tileEvenColor + drawTile(canvas, rect, bitmapPaint, 0f, sizeF) + drawTile(canvas, rect, bitmapPaint, sizeF, 0f) + + androidPaint.shader = BitmapShader( + imageBitmap.asAndroidBitmap(), + Shader.TileMode.REPEAT, + Shader.TileMode.REPEAT + ) + } + + private fun drawTile(canvas: Canvas, rect: Rect, paint: Paint, dx: Float, dy: Float) { + val translated = rect.translate(dx, dy) + canvas.drawRect(translated, paint) + } + + override fun draw(canvas: android.graphics.Canvas) { + canvas.drawPaint(androidPaint) + } + + override fun setAlpha(alpha: Int) { + androidPaint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + androidPaint.colorFilter = colorFilter + } + + override fun getOpacity(): Int = PixelFormat.OPAQUE +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BitmapCalculator.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BitmapCalculator.kt new file mode 100644 index 0000000..3da102a --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BitmapCalculator.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.Bitmap +import android.media.ThumbnailUtils +import androidx.compose.ui.unit.IntSize + +/** + * A bitmap calculator to scaling and cropping to a target size. + */ +internal object BitmapCalculator { + + /** + * Scale the source with maintaining the source's aspect ratio + * so that both dimensions (width and height) of the source will be equal to or less than the + * corresponding dimension of the target size. + */ + internal fun scaleBitmap(bitmap: Bitmap, targetSize: IntSize): Bitmap { + return Bitmap.createScaledBitmap( + bitmap, + targetSize.width, + targetSize.height, + false + ) + } + + /** + * Crop ths source the corresponding dimension of the target size. + * so that if the dimensions (width and height) source is bigger than the target size, + * it will be cut off from the center. + */ + internal fun cropBitmap(bitmap: Bitmap, targetSize: IntSize): Bitmap { + return ThumbnailUtils.extractThumbnail(bitmap, targetSize.width, targetSize.height) + } + + /** + * Scale the source with maintaining the source's aspect ratio + * so that if both dimensions (width and height) of the source is smaller than the target size, + * it will not be scaled. + */ + internal fun inside(bitmap: Bitmap, targetSize: IntSize): Bitmap { + return if (bitmap.width < targetSize.width && bitmap.height < targetSize.height) { + bitmap + } else { + scaleBitmap(bitmap, targetSize) + } + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BrightnessSlider.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BrightnessSlider.kt new file mode 100644 index 0000000..a2df1f6 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/BrightnessSlider.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Canvas +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.LinearGradientShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.PaintingStyle +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp + +/** + * BrightnessSlider allows you to adjust the brightness value of the selected color from color pickers. + * + * @param modifier [Modifier] to decorate the internal Canvas. + * @param controller Allows you to control and interacts with color pickers and all relevant subcomponents. + * @param borderRadius Radius of the border. + * @param borderSize [Dp] size of the border. + * @param borderColor [Color] of the border. + * @param wheelImageBitmap [ImageBitmap] to draw the wheel. + * @param wheelRadius Radius of the wheel. + * @param wheelColor [Color] of th wheel. + * @param wheelPaint [Paint] to draw the wheel. + */ +@Composable +public fun BrightnessSlider( + modifier: Modifier, + controller: ColorPickerController, + borderRadius: Dp = 6.dp, + borderSize: Dp = 5.dp, + borderColor: Color = Color.LightGray, + wheelImageBitmap: ImageBitmap? = null, + wheelRadius: Dp = 30.dp, + wheelColor: Color = Color.White, + wheelPaint: Paint = Paint().apply { color = wheelColor }, +) { + var backgroundBitmap: ImageBitmap? = null + var bitmapSize = IntSize(0, 0) + val borderPaint: Paint = Paint().apply { + style = PaintingStyle.Stroke + strokeWidth = borderSize.value + color = borderColor + } + val colorPaint: Paint = Paint().apply { + color = controller.pureSelectedColor.value + } + + LaunchedEffect(controller) { + controller.isAttachedBrightnessSlider = true + } + + Canvas( + modifier = modifier + .fillMaxSize() + .clip(RoundedCornerShape(borderRadius)) + .onSizeChanged { newSize -> + val size = + newSize.takeIf { it.width != 0 && it.height != 0 } ?: return@onSizeChanged + backgroundBitmap + ?.asAndroidBitmap() + ?.recycle() + backgroundBitmap = + ImageBitmap(size.width, size.height, ImageBitmapConfig.Argb8888).apply { + val backgroundCanvas = Canvas(this) + backgroundCanvas.drawRoundRect( + left = 0f, + top = 0f, + right = size.width.toFloat(), + bottom = size.height.toFloat(), + radiusX = borderRadius.value, + radiusY = borderRadius.value, + paint = borderPaint + ) + } + bitmapSize = size + } + .pointerInteropFilter { event -> + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_UP -> { + // calculate wheel position. + val wheelPoint = event.x + val position: Float = if (wheelImageBitmap == null) { + val sliderWidth = + bitmapSize.width - (wheelRadius.value + borderSize.value) * 2 + val point = wheelPoint.coerceIn( + minimumValue = wheelRadius.value + borderSize.value, + maximumValue = bitmapSize.width - wheelRadius.value - borderSize.value, + ) + point / sliderWidth + } else { + val sliderWidth = + bitmapSize.width - (borderSize.value) * 2 - wheelImageBitmap.width + val point = wheelPoint.coerceIn( + minimumValue = borderSize.value, + maximumValue = bitmapSize.width.toFloat() - wheelImageBitmap.width - borderSize.value + ) + point / sliderWidth + } + controller.setBrightness(position.coerceIn(0f, 1f), fromUser = true) + true + } + else -> false + } + } + ) { + drawIntoCanvas { canvas -> + backgroundBitmap?.let { + // draw background bitmap. + canvas.drawImage(it, Offset.Zero, Paint()) + + // draw a linear gradient color shader. + val shader = LinearGradientShader( + colors = listOf(Color.Black, controller.pureSelectedColor.value), + from = Offset.Zero, + to = Offset(bitmapSize.width.toFloat(), bitmapSize.height.toFloat()), + tileMode = TileMode.Clamp + ) + colorPaint.shader = shader + canvas.drawRoundRect( + left = 0f, + top = 0f, + right = bitmapSize.width.toFloat(), + bottom = bitmapSize.height.toFloat(), + radiusX = borderRadius.value, + radiusY = borderRadius.value, + paint = colorPaint + ) + + // draw wheel bitmap on the canvas. + if (wheelImageBitmap == null) { + val position = controller.brightness.value + val point = (bitmapSize.width * position).coerceIn( + minimumValue = wheelRadius.value + borderSize.value, + maximumValue = bitmapSize.width - wheelRadius.value - borderSize.value, + ) + canvas.drawCircle( + Offset(x = point, y = bitmapSize.height / 2f), + wheelRadius.value, + wheelPaint + ) + } else { + val position = controller.brightness.value + val point = (bitmapSize.width * position).coerceIn( + minimumValue = borderSize.value, + maximumValue = bitmapSize.width.toFloat() - wheelImageBitmap.width - borderSize.value + ) + canvas.drawImage( + wheelImageBitmap, + Offset(x = point, y = bitmapSize.height / 2f - wheelImageBitmap.height / 2), + Paint() + ) + } + } + } + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorEnvelope.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorEnvelope.kt new file mode 100644 index 0000000..d55362d --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorEnvelope.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import androidx.compose.ui.graphics.Color + +/** + * Data transfer object that includes updated color factors. + * + * @param color ARGB color value. + * @param hexCode Color hex code, which represents [color] value. + * @param fromUser Represents this event is triggered by user or not. + */ +public data class ColorEnvelope( + val color: Color, + val hexCode: String, + val fromUser: Boolean +) diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorExtensions.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorExtensions.kt new file mode 100644 index 0000000..b20511c --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorExtensions.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import androidx.compose.ui.graphics.Color +import java.util.Locale + +internal val Color.hexCode: String + inline get() { + val a: Int = (alpha * 255).toInt() + val r: Int = (red * 255).toInt() + val g: Int = (green * 255).toInt() + val b: Int = (blue * 255).toInt() + return java.lang.String.format(Locale.getDefault(), "%02X%02X%02X%02X", a, r, g, b) + } diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt new file mode 100644 index 0000000..af36989 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ColorPickerController.kt @@ -0,0 +1,335 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.Bitmap +import android.graphics.Matrix +import android.graphics.PointF +import android.os.Handler +import android.os.Looper +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.State +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.graphics.toArgb +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import kotlinx.coroutines.flow.MutableStateFlow +import kotlin.math.atan2 +import kotlin.math.sqrt + +/** Creates and remembers a [ColorPickerController] on the current composer. */ +@Composable +public fun rememberColorPickerController(): ColorPickerController { + return remember { ColorPickerController() } +} + +/** + * [ColorPickerController] allows you to control and interacts with the [ImageColorPicker], [HsvColorPicker], + * and all relevant subcomponents. You can create and remember [ColorPickerController] + * with the [rememberColorPickerController] extension. + */ +public class ColorPickerController { + + /** An [ImageBitmap] to be drawn on the canvas as a palette. */ + internal var paletteBitmap: ImageBitmap? = null + + /** An [ImageBitmap] to be drawn on the canvas as a wheel. */ + internal var wheelBitmap: ImageBitmap? = null + + private val _selectedPoint: MutableState = mutableStateOf(PointF(0f, 0f)) + + /** State of [PointF], which represents the currently selected coordinate. */ + public val selectedPoint: State = _selectedPoint + + private val _selectedColor: MutableState = mutableStateOf(Color.Transparent) + + /** State of [Color], which represents the currently selected color value with alpha and brightness. */ + public val selectedColor: State = _selectedColor + + /** State of [Color], which represents the currently selected color value without alpha and brightness. */ + internal var pureSelectedColor: MutableState = mutableStateOf(Color.Transparent) + + /** Alpha value to be applied with the selected color. */ + internal var alpha: MutableState = mutableStateOf(1.0f) + + /** Brightness value to be applied with the selected color. */ + internal var brightness: MutableState = mutableStateOf(1.0f) + + /** Radius to draw default wheel. */ + internal var wheelRadius: Dp = 30.dp + private set + + /** Paint to draw default wheel. */ + internal var wheelPaint: Paint = Paint().apply { color = Color.White } + private set + + /** Enable or not color selection. */ + private val enabled: MutableState = mutableStateOf(true) + + /** Decide the content scale of the palette when draws. */ + private var paletteContentScale: PaletteContentScale = PaletteContentScale.FIT + + /** Size of the measured canvas dimensions (width and height). */ + internal val canvasSize: MutableState = mutableStateOf(IntSize(0, 0)) + + /** Matrix of the [paletteBitmap], which is used to calculate pixel positions. */ + internal val imageBitmapMatrix: MutableState = mutableStateOf(Matrix()) + + /** Indicates if the color picker is HSV model. */ + internal var isHsvColorPalette: Boolean = false + + /** Indicates if the alpha slider has been attached. */ + internal var isAttachedAlphaSlider: Boolean = false + + /** Indicates if the brightness slider has been attached. */ + internal var isAttachedBrightnessSlider: Boolean = false + + internal var reviseTick = mutableStateOf(0) + + internal var colorChangedTick = MutableStateFlow(null) + + private val debounceHandler = Handler(Looper.getMainLooper()) + + private var debounceDuration: Long = 0L + + /** Set an [ImageBitmap] to draw on the canvas as a palette. */ + public fun setPaletteImageBitmap(imageBitmap: ImageBitmap) { + val targetSize = canvasSize.value.takeIf { it.width != 0 && it.height != 0 } + ?: throw IllegalAccessException("Can't set an ImageBitmap before initializing the canvas") + val copiedBitmap = + imageBitmap.asAndroidBitmap().copy(Bitmap.Config.ARGB_8888, false) + val resized = when (paletteContentScale) { + PaletteContentScale.FIT -> BitmapCalculator.scaleBitmap(copiedBitmap, targetSize) + PaletteContentScale.CROP -> BitmapCalculator.cropBitmap(copiedBitmap, targetSize) + } + paletteBitmap = resized.asImageBitmap() + copiedBitmap.recycle() + selectCenter(fromUser = false) + reviseTick.value++ + } + + /** Set a [PaletteContentScale] to the palette bitmap. */ + public fun setPaletteContentScale(paletteContentScale: PaletteContentScale) { + this.paletteContentScale = paletteContentScale + } + + /** Set an [ImageBitmap] to draw on the canvas as a wheel. */ + public fun setWheelImageBitmap(imageBitmap: ImageBitmap?) { + wheelBitmap = imageBitmap + } + + /** Set a radius to draw default wheel. */ + public fun setWheelRadius(radius: Dp) { + wheelRadius = radius + reviseTick.value++ + } + + /** Set a paint to draw default wheel. */ + public fun setWheelPaint(paint: Paint) { + wheelPaint = paint + reviseTick.value++ + } + + /** Set a color for the wheel. */ + public fun setWheelColor(color: Color) { + wheelPaint.color = color + reviseTick.value++ + } + + /** Set an alpha for the wheel. */ + public fun setWheelAlpha(alpha: Float) { + wheelPaint.alpha = alpha + reviseTick.value++ + } + + /** Enable or unable color selection. */ + public fun setEnabled(enabled: Boolean) { + this.enabled.value = enabled + } + + /** Set the debounce duration. */ + public fun setDebounceDuration(duration: Long) { + debounceDuration = duration + } + + /** + * Select a specific point by coordinates and update a selected color. + * + * @param x x-coordinate to extract a pixel color. + * @param y y-coordinate to extract a pixel color. + * @param fromUser Represents this event is triggered by user or not. + */ + public fun selectByCoordinate(x: Float, y: Float, fromUser: Boolean) { + enabled.value.takeIf { it } ?: return + val snapPoint = PointMapper.getColorPoint(this, PointF(x, y)) + val extractedColor = if (isHsvColorPalette) { + extractPixelHsvColor(snapPoint.x, snapPoint.y) + } else { + extractPixelColor(snapPoint.x, snapPoint.y) + } + if (extractedColor != Color.Transparent) { + // set the extracted color. + pureSelectedColor.value = extractedColor + _selectedPoint.value = PointF(snapPoint.x, snapPoint.y) + _selectedColor.value = applyHSVFactors(extractedColor) + + // notify color changes to the listeners. + if (fromUser && debounceDuration != 0L) { + notifyColorChangedWithDebounce(fromUser) + } else { + notifyColorChanged(fromUser) + } + } + } + + /** + * Select center point of the palette. + * + * @param fromUser Represents this event is triggered by user or not. + */ + public fun selectCenter(fromUser: Boolean) { + val size = canvasSize.value + selectByCoordinate(size.width * 0.5f, size.height * 0.5f, fromUser) + } + + /** Notify color changes to the color picker and other subcomponents. */ + private fun notifyColorChanged(fromUser: Boolean) { + val color = _selectedColor.value + colorChangedTick.value = ColorEnvelope(color, color.hexCode, fromUser) + } + + /** Notify color changes to the color picker and other subcomponents with debounce duration. */ + private fun notifyColorChangedWithDebounce(fromUser: Boolean) { + val runnable = { notifyColorChanged(fromUser) } + debounceHandler.removeCallbacksAndMessages(null) + debounceHandler.postDelayed(runnable, debounceDuration) + } + + /** Combine the alpha value to the selected pure color. */ + internal fun setAlpha(alpha: Float, fromUser: Boolean) { + this.alpha.value = alpha + _selectedColor.value = selectedColor.value.copy(alpha = alpha) + notifyColorChanged(fromUser) + } + + /** Combine the brightness value to the selected pure color. */ + internal fun setBrightness(brightness: Float, fromUser: Boolean) { + this.brightness.value = brightness + val hsv = FloatArray(3) + android.graphics.Color.colorToHSV(selectedColor.value.toArgb(), hsv) + hsv[2] = brightness + _selectedColor.value = + Color(android.graphics.Color.HSVToColor((alpha.value * 255).toInt(), hsv)) + notifyColorChanged(fromUser) + } + + /** Return a [Color] that is applied with HSV color factors to the [color]. */ + private fun applyHSVFactors(color: Color): Color { + val hsv = FloatArray(3) + android.graphics.Color.colorToHSV(color.toArgb(), hsv) + if (isAttachedBrightnessSlider) { + hsv[2] = brightness.value + } + return if (isAttachedAlphaSlider) { + Color(android.graphics.Color.HSVToColor((alpha.value * 255).toInt(), hsv)) + } else { + Color(android.graphics.Color.HSVToColor(hsv)) + } + } + + /** + * Extract a pixel color from the [paletteBitmap]. + * + * @param x x-coordinate to extract a pixel color. + * @param y y-coordinate to extract a pixel color. + * + * @return An extracted [Color] from the desired coordinates. + * if fail to extract a pixel value, it will returns [Color.Transparent]. + */ + internal fun extractPixelColor(x: Float, y: Float): Color { + val invertMatrix = Matrix() + imageBitmapMatrix.value.invert(invertMatrix) + + val mappedPoints = floatArrayOf(x, y) + invertMatrix.mapPoints(mappedPoints) + + val palette = paletteBitmap + if (palette != null && + mappedPoints[0] >= 0 && + mappedPoints[1] >= 0 && + mappedPoints[0] < palette.width && + mappedPoints[1] < palette.height + ) { + val scaleX = mappedPoints[0] / palette.width + val x1 = scaleX * palette.width + val scaleY = mappedPoints[1] / palette.height + val y1 = scaleY * palette.height + val pixelColor = palette.asAndroidBitmap().getPixel(x1.toInt(), y1.toInt()) + return Color(pixelColor) + } + return Color.Transparent + } + + private fun extractPixelHsvColor(x: Float, y: Float): Color { + val invertMatrix = Matrix() + imageBitmapMatrix.value.invert(invertMatrix) + + val mappedPoints = floatArrayOf(x, y) + invertMatrix.mapPoints(mappedPoints) + + val palette = paletteBitmap + if (palette != null && + mappedPoints[0] >= 0 && + mappedPoints[1] >= 0 && + mappedPoints[0] < palette.width && + mappedPoints[1] < palette.height + ) { + val x2 = x - palette.width * 0.5f + val y2 = y - palette.height * 0.5f + val size = canvasSize.value + val r = sqrt((x2 * x2 + y2 * y2).toDouble()) + val radius: Float = size.width.coerceAtMost(size.height) * 0.5f + val hsv = floatArrayOf(0f, 0f, 1f) + ( + ( + atan2( + y2.toDouble(), + -x2.toDouble() + ) / Math.PI * 180f + ).toFloat() + 180 + ).also { hsv[0] = it } + hsv[1] = 0f.coerceAtLeast(1f.coerceAtMost((r / radius).toFloat())) + return Color(android.graphics.Color.HSVToColor(hsv)) + } + return Color.Transparent + } + + internal fun releaseBitmap() { + paletteBitmap?.asAndroidBitmap()?.recycle() + wheelBitmap?.asAndroidBitmap()?.recycle() + paletteBitmap = null + wheelBitmap = null + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/DefaultColors.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/DefaultColors.kt new file mode 100644 index 0000000..867b271 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/DefaultColors.kt @@ -0,0 +1,22 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import androidx.compose.ui.graphics.Color + +internal val defaultTileOddColor: Color = Color(0xFFFFFFFF) +internal val defaultTileEvenColor: Color = Color(0xFFCBCBCB) diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvBitmapDrawable.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvBitmapDrawable.kt new file mode 100644 index 0000000..b4e53d1 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvBitmapDrawable.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.content.res.Resources +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.ColorFilter +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.RadialGradient +import android.graphics.Shader +import android.graphics.SweepGradient +import android.graphics.drawable.BitmapDrawable +import kotlin.math.min + +/** + * HsvBitmapDrawable draws hsv color gradient with hue and saturation on a canvas. + * + * @param resources [Resources] to initialize [BitmapDrawable]. + * @param bitmap [Bitmap] to draw on the canvas. + */ +internal class HsvBitmapDrawable constructor( + resources: Resources, + bitmap: Bitmap +) : BitmapDrawable(resources, bitmap) { + + private val huePaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + private val saturationPaint: Paint = Paint(Paint.ANTI_ALIAS_FLAG) + + override fun draw(canvas: Canvas) { + val width = bounds.width() + val height = bounds.height() + val centerX = width * 0.5f + val centerY = height * 0.5f + val radius = min(width, height) * 0.5f + + val sweepShader = SweepGradient( + centerX, + centerY, + intArrayOf( + Color.RED, + Color.MAGENTA, + Color.BLUE, + Color.CYAN, + Color.GREEN, + Color.YELLOW, + Color.RED + ), + floatArrayOf(0.000f, 0.166f, 0.333f, 0.499f, 0.666f, 0.833f, 0.999f) + ) + huePaint.shader = sweepShader + + val saturationShader = RadialGradient( + centerX, centerY, radius, Color.WHITE, 0x00FFFFFF, Shader.TileMode.CLAMP + ) + saturationPaint.shader = saturationShader + + canvas.drawCircle(centerX, centerY, radius, huePaint) + canvas.drawCircle(centerX, centerY, radius, saturationPaint) + } + + override fun setAlpha(alpha: Int) { + huePaint.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + huePaint.colorFilter = colorFilter + } + + override fun getOpacity(): Int { + return PixelFormat.OPAQUE + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvColorPicker.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvColorPicker.kt new file mode 100644 index 0000000..451bc14 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/HsvColorPicker.kt @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.Matrix +import android.graphics.RectF +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageBitmapConfig +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.graphics.nativeCanvas +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.platform.LocalContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +/** + * HsvColorPicker allows you to get colors from HSV color palette by tapping on the desired color. + * + * @param modifier [Modifier] to decorate the internal Canvas. + * @param controller Allows you to control and interacts with color pickers and all relevant subcomponents. + * @param wheelImageBitmap [ImageBitmap] to draw the wheel. + * @param onColorChanged Color changed listener. + */ +@Composable +public fun HsvColorPicker( + modifier: Modifier, + controller: ColorPickerController, + wheelImageBitmap: ImageBitmap? = null, + onColorChanged: ((colorEnvelope: ColorEnvelope) -> Unit)? = null +) { + val context = LocalContext.current + var hsvBitmapDrawable: HsvBitmapDrawable? = null + var bitmap: ImageBitmap? = null + val coroutineScope = rememberCoroutineScope() + DisposableEffect(key1 = controller) { + coroutineScope.launch(Dispatchers.Main) { + controller.isHsvColorPalette = true + bitmap?.let { controller.setPaletteImageBitmap(it) } + controller.setWheelImageBitmap(wheelImageBitmap) + controller.colorChangedTick.mapNotNull { it }.collect { + onColorChanged?.invoke(it) + } + } + + onDispose { + controller.releaseBitmap() + } + } + + Canvas( + modifier = modifier + .fillMaxSize() + .onSizeChanged { newSize -> + val size = + newSize.takeIf { it.width != 0 && it.height != 0 } ?: return@onSizeChanged + controller.canvasSize.value = size + bitmap + ?.asAndroidBitmap() + ?.recycle() + bitmap = ImageBitmap(size.width, size.height, ImageBitmapConfig.Argb8888).also { + hsvBitmapDrawable = + HsvBitmapDrawable(context.resources, it.asAndroidBitmap()).apply { + setBounds( + 0, + 0, + size.width, + size.height + ) + } + + var dx = 0f + var dy = 0f + val scale: Float + val shaderMatrix = Matrix() + val mDrawableRect = RectF(0f, 0f, size.width.toFloat(), size.height.toFloat()) + val bitmapWidth: Int = it.asAndroidBitmap().width + val bitmapHeight: Int = it.asAndroidBitmap().height + + if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) { + scale = mDrawableRect.height() / bitmapHeight.toFloat() + dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / bitmapWidth.toFloat() + dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f + } + // resize the matrix to scale by sx and sy. + shaderMatrix.setScale(scale, scale) + + // post translate the matrix with the specified translation. + shaderMatrix.postTranslate( + (dx + 0.5f) + mDrawableRect.left, + (dy + 0.5f) + mDrawableRect.top + ) + + // set the shader matrix to the controller. + controller.imageBitmapMatrix.value = shaderMatrix + } + } + .pointerInteropFilter { event -> + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_UP -> { + controller.selectByCoordinate(event.x, event.y, true) + true + } + else -> false + } + } + ) { + drawIntoCanvas { canvas -> + // draw hsv bitmap on the canvas. + hsvBitmapDrawable?.draw(canvas.nativeCanvas) + + // draw wheel bitmap on the canvas. + val point = controller.selectedPoint.value + val wheelBitmap = controller.wheelBitmap + if (wheelBitmap == null) { + canvas.drawCircle( + Offset(point.x, point.y), + controller.wheelRadius.value, + controller.wheelPaint + ) + } else { + canvas.drawImage( + wheelBitmap, + Offset(point.x - wheelBitmap.width / 2, point.y - wheelBitmap.height / 2), + Paint() + ) + } + } + controller.reviseTick.value + } +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt new file mode 100644 index 0000000..19f9827 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/ImageColorPicker.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.Matrix +import android.graphics.RectF +import android.view.MotionEvent +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.toRect +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.graphics.ImageShader +import androidx.compose.ui.graphics.Paint +import androidx.compose.ui.graphics.ShaderBrush +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.graphics.asAndroidBitmap +import androidx.compose.ui.graphics.drawscope.drawIntoCanvas +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.layout.onSizeChanged +import androidx.compose.ui.unit.toSize +import androidx.core.util.Pools +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.launch + +/** + * ImageColorPicker allows you to get colors from any images by tapping on the desired color. + * + * @param modifier [Modifier] to decorate the internal Canvas. + * @param controller Allows you to control and interacts with color pickers and all relevant subcomponents. + * @param paletteImageBitmap [ImageBitmap] to draw the palette. + * @param wheelImageBitmap [ImageBitmap] to draw the wheel. + * @param paletteContentScale Represents a rule to apply to scale a source rectangle to be inscribed into a destination. + * @param onColorChanged Color changed listener. + */ +@Composable +public fun ImageColorPicker( + modifier: Modifier, + controller: ColorPickerController, + paletteImageBitmap: ImageBitmap, + wheelImageBitmap: ImageBitmap? = null, + paletteContentScale: PaletteContentScale = PaletteContentScale.FIT, + onColorChanged: ((colorEnvelope: ColorEnvelope) -> Unit)? = null +) { + val coroutineScope = rememberCoroutineScope() + DisposableEffect(key1 = controller) { + coroutineScope.launch(Dispatchers.Main) { + with(controller) { + setPaletteContentScale(paletteContentScale) + setPaletteImageBitmap(paletteImageBitmap) + setWheelImageBitmap(wheelImageBitmap) + colorChangedTick.mapNotNull { it }.collect { + onColorChanged?.invoke(it) + } + } + } + + onDispose { + controller.releaseBitmap() + } + } + + Canvas( + modifier = modifier + .onSizeChanged { size -> + if (size.width != 0 && size.height != 0) { + controller.canvasSize.value = size + } + } + .pointerInteropFilter { event -> + when (event.action) { + MotionEvent.ACTION_DOWN, + MotionEvent.ACTION_MOVE, + MotionEvent.ACTION_UP -> { + controller.selectByCoordinate(event.x, event.y, true) + true + } + else -> false + } + } + ) { + drawIntoCanvas { canvas -> + // draw image bitmap on the canvas. + controller.paletteBitmap?.let { imageBitmap -> + var dx = 0f + var dy = 0f + val scale: Float + val shaderMatrix = Matrix() + val shader = ImageShader(imageBitmap, TileMode.Clamp) + val brush = ShaderBrush(shader) + val paint = paintPool.acquire() ?: Paint() + paint.asFrameworkPaint().apply { + isAntiAlias = true + isDither = true + isFilterBitmap = true + } + + // cache the paint in the internal stack. + canvas.saveLayer(size.toRect(), paint) + + val mDrawableRect = RectF(0f, 0f, size.width, size.height) + val bitmapWidth: Int = imageBitmap.asAndroidBitmap().width + val bitmapHeight: Int = imageBitmap.asAndroidBitmap().height + + if (bitmapWidth * mDrawableRect.height() > mDrawableRect.width() * bitmapHeight) { + scale = mDrawableRect.height() / bitmapHeight.toFloat() + dx = (mDrawableRect.width() - bitmapWidth * scale) * 0.5f + } else { + scale = mDrawableRect.width() / bitmapWidth.toFloat() + dy = (mDrawableRect.height() - bitmapHeight * scale) * 0.5f + } + + // resize the matrix to scale by sx and sy. + shaderMatrix.setScale(scale, scale) + + // post translate the matrix with the specified translation. + shaderMatrix.postTranslate( + (dx + 0.5f) + mDrawableRect.left, + (dy + 0.5f) + mDrawableRect.top + ) + // apply the scaled matrix to the shader. + shader.setLocalMatrix(shaderMatrix) + // Set the shader matrix to the controller. + controller.imageBitmapMatrix.value = shaderMatrix + // draw an image bitmap as a rect. + drawRect(brush = brush, size = controller.canvasSize.value.toSize()) + // restore canvas. + canvas.restore() + // resets the paint and release to the pool. + paint.asFrameworkPaint().reset() + paintPool.release(paint) + } + + // draw wheel bitmap on the canvas. + val point = controller.selectedPoint.value + val wheelBitmap = controller.wheelBitmap + if (wheelBitmap == null) { + canvas.drawCircle( + Offset(point.x, point.y), + controller.wheelRadius.value, + controller.wheelPaint + ) + } else { + canvas.drawImage( + wheelBitmap, + Offset(point.x - wheelBitmap.width / 2, point.y - wheelBitmap.height / 2), + Paint() + ) + } + } + controller.reviseTick.value + } +} + +/** paint pool which caching and reusing [Paint] instances. */ +private val paintPool = Pools.SimplePool(2) diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PaletteContentScale.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PaletteContentScale.kt new file mode 100644 index 0000000..74ef786 --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PaletteContentScale.kt @@ -0,0 +1,41 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +/** Represents a rule to apply to scale a source rectangle to be inscribed into a destination. */ +public enum class PaletteContentScale { + /** + * Scale the source with maintaining the source's aspect ratio + * so that both dimensions (width and height) of the source will be equal to or less than the + * corresponding dimension of the target size. + */ + FIT, + + /** + * Crop ths source the corresponding dimension of the target size. + * so that if the dimensions (width and height) source is bigger than the target size, + * it will be cut off from the center. + */ + CROP, + + /** + * Scale the source with maintaining the source's aspect ratio + * so that if both dimensions (width and height) of the source is smaller than the target size, + * it will not be scaled. + */ +// INSIDE, +} diff --git a/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PointMapper.kt b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PointMapper.kt new file mode 100644 index 0000000..2955a9d --- /dev/null +++ b/colorpicker-compose/src/main/kotlin/com/github/skydoves/colorpicker/compose/PointMapper.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.github.skydoves.colorpicker.compose + +import android.graphics.PointF +import androidx.compose.ui.graphics.Color +import kotlin.math.abs +import kotlin.math.sqrt + +/** + * PointMapper calculates correct coordinates corresponding to bitmap ratio and size. + */ +internal object PointMapper { + + internal fun getColorPoint(controller: ColorPickerController, point: PointF): PointF { + val size = controller.canvasSize.value + val center = PointF(size.width / 2f, size.height / 2f) + return if (controller.isHsvColorPalette) { + getHuePoint(controller, point) + } else { + approximatedPoint(controller, point, center) + } + } + + private fun approximatedPoint( + controller: ColorPickerController, + start: PointF, + end: PointF + ): PointF { + if (getDistance(start, end) <= 3) return end + val center: PointF = getCenterPoint(start, end) + val color: Color = controller.extractPixelColor(center.x, center.y) + return if (color == Color.Transparent) { + approximatedPoint(controller, center, end) + } else { + approximatedPoint(controller, start, center) + } + } + + private fun getHuePoint(controller: ColorPickerController, point: PointF): PointF { + val size = controller.canvasSize.value + val centerX: Float = size.width * 0.5f + val centerY: Float = size.height * 0.5f + var x = point.x - centerX + var y = point.y - centerY + val radius = centerX.coerceAtMost(centerY) + val r = sqrt((x * x + y * y).toDouble()) + if (r > radius) { + x *= (radius / r).toFloat() + y *= (radius / r).toFloat() + } + return PointF(x + centerX, y + centerY) + } + + private fun getCenterPoint(start: PointF, end: PointF): PointF { + return PointF((end.x + start.x) / 2, (end.y + start.y) / 2) + } + + private fun getDistance(start: PointF, end: PointF): Int { + return sqrt( + ( + abs(end.x - start.x) * abs(end.x - start.x) + + abs(end.y - start.y) * abs(end.y - start.y) + ).toDouble() + ).toInt() + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..cd0519b --- /dev/null +++ b/gradle.properties @@ -0,0 +1,23 @@ +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app"s APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Kotlin code style for this project: "official" or "obsolete": +kotlin.code.style=official +# Enables namespacing of each library's R class so that its R class includes only the +# resources declared in the library itself and none from the library's dependencies, +# thereby reducing the size of the R class for that library +android.nonTransitiveRClass=true \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..e708b1c Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..0a1b456 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Wed Feb 23 20:18:07 KST 2022 +distributionBase=GRADLE_USER_HOME +distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionPath=wrapper/dists +zipStorePath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew new file mode 100755 index 0000000..4f906e0 --- /dev/null +++ b/gradlew @@ -0,0 +1,185 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..ac1b06f --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,89 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/preview/preview0.gif b/preview/preview0.gif new file mode 100644 index 0000000..d75e114 Binary files /dev/null and b/preview/preview0.gif differ diff --git a/preview/preview1.gif b/preview/preview1.gif new file mode 100644 index 0000000..ad2e58d Binary files /dev/null and b/preview/preview1.gif differ diff --git a/preview/preview2.gif b/preview/preview2.gif new file mode 100644 index 0000000..40ae103 Binary files /dev/null and b/preview/preview2.gif differ diff --git a/preview/preview3.gif b/preview/preview3.gif new file mode 100644 index 0000000..6ffc659 Binary files /dev/null and b/preview/preview3.gif differ diff --git a/preview/preview4.gif b/preview/preview4.gif new file mode 100644 index 0000000..b25412f Binary files /dev/null and b/preview/preview4.gif differ diff --git a/scripts/publish-module.gradle b/scripts/publish-module.gradle new file mode 100644 index 0000000..7f7bb63 --- /dev/null +++ b/scripts/publish-module.gradle @@ -0,0 +1,85 @@ +apply plugin: 'maven-publish' +apply plugin: 'signing' +apply plugin: 'org.jetbrains.dokka' + +task androidSourcesJar(type: Jar) { + archiveClassifier.set('sources') + if (project.plugins.findPlugin("com.android.library")) { + from android.sourceSets.main.java.srcDirs + from android.sourceSets.main.kotlin.srcDirs + } else { + from sourceSets.main.java.srcDirs + from sourceSets.main.kotlin.srcDirs + } +} + +tasks.withType(dokkaHtmlPartial.getClass()).configureEach { + pluginsMapConfiguration.set( + ["org.jetbrains.dokka.base.DokkaBase": """{ "separateInheritedMembers": true}"""] + ) +} + +task javadocJar(type: Jar, dependsOn: dokkaJavadoc) { + archiveClassifier.set('javadoc') + from dokkaJavadoc.outputDirectory +} + +artifacts { + archives androidSourcesJar + archives javadocJar +} + +group = PUBLISH_GROUP_ID +version = PUBLISH_VERSION + +afterEvaluate { + publishing { + publications { + release(MavenPublication) { + groupId PUBLISH_GROUP_ID + artifactId PUBLISH_ARTIFACT_ID + version PUBLISH_VERSION + if (project.plugins.findPlugin("com.android.library")) { + from components.release + } else { + from components.java + } + + artifact androidSourcesJar + artifact javadocJar + + pom { + name = PUBLISH_ARTIFACT_ID + description = 'Jetpack Compose color picker for getting colors from any images by tapping on the desired color.' + url = 'https://github.com/skydoves/colorpicker-compose' + licenses { + license { + name = 'The Apache Software License, Version 2.0' + url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' + } + } + developers { + developer { + id = 'skydoves' + name = 'Jaewoong Eum' + } + } + scm { + connection = 'scm:git:github.com/skydoves/colorpicker-compose.git' + developerConnection = 'scm:git:ssh://github.com/skydoves/colorpicker-compose.git' + url = 'https://github.com/skydoves/colorpicker-compose/tree/main' + } + } + } + } + } +} + +signing { + useInMemoryPgpKeys( + rootProject.ext["signing.keyId"], + rootProject.ext["signing.key"], + rootProject.ext["signing.password"], + ) + sign publishing.publications +} diff --git a/scripts/publish-root.gradle b/scripts/publish-root.gradle new file mode 100644 index 0000000..27680b1 --- /dev/null +++ b/scripts/publish-root.gradle @@ -0,0 +1,45 @@ +import com.github.skydoves.colorpicker.compose.Configuration + +// Create variables with empty default values +ext["ossrhUsername"] = '' +ext["ossrhPassword"] = '' +ext["sonatypeStagingProfileId"] = '' +ext["signing.keyId"] = '' +ext["signing.password"] = '' +ext["signing.key"] = '' +ext["snapshot"] = '' + +File secretPropsFile = project.rootProject.file('local.properties') +if (secretPropsFile.exists()) { + // Read local.properties file first if it exists + Properties p = new Properties() + new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) } + p.each { name, value -> ext[name] = value } +} else { + // Use system environment variables + ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME') + ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD') + ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID') + ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID') + ext["signing.password"] = System.getenv('SIGNING_PASSWORD') + ext["signing.key"] = System.getenv('SIGNING_KEY') + ext["snapshot"] = System.getenv('SNAPSHOT') +} + +if (snapshot) { + ext["rootVersionName"] = Configuration.snapshotVersionName +} else { + ext["rootVersionName"] = Configuration.versionName +} + +// Set up Sonatype repository +nexusPublishing { + repositories { + sonatype { + stagingProfileId = sonatypeStagingProfileId + username = ossrhUsername + password = ossrhPassword + version = rootVersionName + } + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..e6077e4 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +pluginManagement { + repositories { + gradlePluginPortal() + google() + mavenCentral() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} +rootProject.name = "ColorPickerComposeDemo" +include ':app' +include ':colorpicker-compose' diff --git a/spotless.gradle b/spotless.gradle new file mode 100644 index 0000000..c6a7ee5 --- /dev/null +++ b/spotless.gradle @@ -0,0 +1,11 @@ +apply plugin: "com.diffplug.spotless" + +spotless { + kotlin { + target "**/*.kt" + ktlint().userData(['indent_size': '4', 'continuation_indent_size': '4']) + licenseHeaderFile "$rootDir/spotless.license.kt" + trimTrailingWhitespace() + endWithNewline() + } +} \ No newline at end of file diff --git a/spotless.license.kt b/spotless.license.kt new file mode 100644 index 0000000..2ab6b1e --- /dev/null +++ b/spotless.license.kt @@ -0,0 +1,16 @@ +/* + * Copyright (C) 2022 skydoves + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + \ No newline at end of file