Skip to content

Commit 8eafa53

Browse files
committed
ColorPicker
1 parent fd15fc0 commit 8eafa53

File tree

12 files changed

+349
-6
lines changed

12 files changed

+349
-6
lines changed

.idea/gradle.xml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

app/build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,6 @@ dependencies {
4747
implementation(libs.androidx.activity)
4848
implementation(libs.androidx.constraintlayout)
4949

50-
implementation(project(":colorpicker"))
50+
implementation(project(":colorpick"))
5151

5252
}

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22
plugins {
33
alias(libs.plugins.android.application) apply false
44
alias(libs.plugins.jetbrains.kotlin.android) apply false
5-
alias(libs.plugins.androidLibrary) apply false
5+
alias(libs.plugins.android.library) apply false
66
}

colorpick/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

colorpick/build.gradle.kts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
plugins {
2+
alias(libs.plugins.android.library)
3+
alias(libs.plugins.jetbrains.kotlin.android)
4+
id("maven-publish")
5+
}
6+
7+
android {
8+
namespace = "com.hyeprsoft.picker"
9+
compileSdk = 34
10+
11+
defaultConfig {
12+
minSdk = 24
13+
14+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
15+
consumerProguardFiles("consumer-rules.pro")
16+
}
17+
18+
buildTypes {
19+
release {
20+
isMinifyEnabled = false
21+
proguardFiles(
22+
getDefaultProguardFile("proguard-android-optimize.txt"),
23+
"proguard-rules.pro"
24+
)
25+
}
26+
}
27+
compileOptions {
28+
sourceCompatibility = JavaVersion.VERSION_17
29+
targetCompatibility = JavaVersion.VERSION_17
30+
}
31+
kotlinOptions {
32+
jvmTarget = "17"
33+
}
34+
}
35+
36+
dependencies {
37+
implementation(libs.androidx.core.ktx)
38+
implementation(libs.androidx.appcompat)
39+
implementation(libs.material)
40+
}
41+
42+
43+
publishing {
44+
publications {
45+
create<MavenPublication>("release") {
46+
groupId = "com.hyeprsoft.picker"
47+
artifactId = "colorpicker"
48+
version = "2.3.7"
49+
50+
afterEvaluate {
51+
from(components["release"])
52+
}
53+
}
54+
}
55+
}

colorpick/consumer-rules.pro

Whitespace-only changes.

colorpick/proguard-rules.pro

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
</manifest>
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
package com.hyeprsoft.picker
2+
3+
import android.content.Context
4+
import android.graphics.*
5+
import android.util.AttributeSet
6+
import android.view.MotionEvent
7+
import android.view.View
8+
import android.view.ViewGroup
9+
10+
class ColorPickerView(context: Context, attrs: AttributeSet? = null) : View(context, attrs) {
11+
12+
interface OnColorChangeListener {
13+
fun onColorChanged(color: Int)
14+
fun onHexColorChanged(hexColor: String) {}
15+
}
16+
17+
private var colorChangeListener: OnColorChangeListener? = null
18+
private var parentBitmap: Bitmap? = null
19+
20+
// Paint for draggable circle (existing one)
21+
private val paint = Paint().apply {
22+
style = Paint.Style.STROKE
23+
strokeWidth = dpToPx(minStrokeWidth) // Default stroke width in pixels
24+
color = Color.BLACK // Default color
25+
}
26+
27+
// Paint for fixed outer circle (new one)
28+
private val outerCirclePaint = Paint().apply {
29+
style = Paint.Style.STROKE
30+
strokeWidth = dpToPx(10f)
31+
color = Color.RED
32+
}
33+
34+
private val matrix = Matrix()
35+
private var isDragging = false
36+
private var lastX = 0f
37+
private var lastY = 0f
38+
39+
private var fixedWidth = dpToPx(100f)
40+
private var fixedHeight = dpToPx(100f)
41+
var circleRadius = fixedWidth / 2f
42+
private set
43+
private var changeStrokeColor = true
44+
45+
// Define min and max limits
46+
private val minCircleRadius = dpToPx(30f)
47+
private val maxCircleRadius = dpToPx(200f)
48+
private val minStrokeWidth = dpToPx(10f)
49+
private val maxStrokeWidth = dpToPx(50f)
50+
51+
init {
52+
// Load custom attributes from XML
53+
context.theme.obtainStyledAttributes(
54+
attrs,
55+
R.styleable.DraggableImageView,
56+
0, 0
57+
).apply {
58+
try {
59+
// Retrieve and set the stroke width from XML with limits
60+
setStrokeWidth(getDimension(R.styleable.DraggableImageView_strokeWidth, dpToPx(5f)))
61+
setStrokeColor(getColor(R.styleable.DraggableImageView_strokeColor, Color.BLACK))
62+
setCircleRadius(getDimension(R.styleable.DraggableImageView_circleRadius, dpToPx(50f)))
63+
setChangeStrokeColorRealTime(getBoolean(R.styleable.DraggableImageView_changeStrokeColor, true))
64+
setOuterStrokeColor(getColor(R.styleable.DraggableImageView_outerCircleColor, Color.BLACK))
65+
66+
} finally {
67+
recycle() // Always recycle the TypedArray after use
68+
}
69+
}
70+
71+
post {
72+
centerCircle()
73+
invalidate() // Redraw the view to reflect the initial position
74+
}
75+
76+
if (isInEditMode) {
77+
invalidate()
78+
}
79+
}
80+
81+
private fun dpToPx(dp: Float): Float {
82+
return dp * context.resources.displayMetrics.density
83+
}
84+
85+
private fun centerCircle() {
86+
val centerX = width / 2f
87+
val centerY = height / 2f
88+
matrix.reset()
89+
matrix.postTranslate(centerX - fixedWidth / 2f, centerY - fixedHeight / 2f)
90+
}
91+
92+
93+
fun setOuterStrokeColor(color: Int) {
94+
outerCirclePaint.color = color
95+
invalidate()
96+
}
97+
98+
fun setChangeStrokeColorRealTime(change: Boolean) {
99+
changeStrokeColor = change
100+
}
101+
102+
fun setStrokeWidth(strokeWidth: Float) {
103+
paint.strokeWidth = strokeWidth.coerceIn(minStrokeWidth, maxStrokeWidth) // Enforce limits
104+
invalidate() // Refresh the view to reflect changes
105+
}
106+
107+
fun setStrokeColor(color: Int) {
108+
paint.color = color
109+
invalidate() // Refresh the view to reflect changes
110+
}
111+
112+
fun setCircleRadius(radius: Float) {
113+
circleRadius = radius.coerceIn(minCircleRadius, maxCircleRadius) // Enforce limits
114+
fixedWidth = circleRadius * 2f
115+
fixedHeight = circleRadius * 2f
116+
centerCircle() // Re-center the circle after changing the radius
117+
invalidate() // Refresh the view to reflect changes
118+
}
119+
120+
fun setOnColorChangeListener(listener: OnColorChangeListener) {
121+
this.colorChangeListener = listener
122+
}
123+
124+
private fun captureParentView() {
125+
val parentView = this.parent as? ViewGroup ?: return
126+
127+
parentBitmap = Bitmap.createBitmap(parentView.width, parentView.height, Bitmap.Config.ARGB_8888)
128+
val canvas = Canvas(parentBitmap!!)
129+
parentView.draw(canvas) // Draw the parent view into the bitmap
130+
}
131+
132+
private fun updateCircleColorFromParent(x: Float, y: Float) {
133+
parentBitmap?.let { bitmap ->
134+
if (x >= 0 && x < bitmap.width && y >= 0 && y < bitmap.height) {
135+
val pixelColor = bitmap.getPixel(x.toInt(), y.toInt())
136+
if (changeStrokeColor) paint.color = pixelColor
137+
138+
// Notify the listener about the new color
139+
colorChangeListener?.onColorChanged(pixelColor)
140+
141+
// Notify the listener about the hex color if overridden
142+
colorChangeListener?.onHexColorChanged(String.format("#%06X", (0xFFFFFF and pixelColor)))
143+
}
144+
}
145+
}
146+
147+
private fun updateCircleColorFromParent() {
148+
parentBitmap?.let { bitmap ->
149+
// Calculate the exact center of the inner circle
150+
val circlePosition = floatArrayOf(0f, 0f)
151+
matrix.mapPoints(circlePosition)
152+
val centerX = circlePosition[0] + circleRadius
153+
val centerY = circlePosition[1] + circleRadius
154+
155+
// Ensure the center is within the bitmap bounds
156+
if (centerX >= 0 && centerX < bitmap.width && centerY >= 0 && centerY < bitmap.height) {
157+
val pixelColor = bitmap.getPixel(centerX.toInt(), centerY.toInt())
158+
if (changeStrokeColor) paint.color = pixelColor
159+
160+
// Notify the listener about the new color
161+
colorChangeListener?.onColorChanged(pixelColor)
162+
163+
// Notify the listener about the hex color if overridden
164+
colorChangeListener?.onHexColorChanged(String.format("#%06X", (0xFFFFFF and pixelColor)))
165+
}
166+
}
167+
}
168+
169+
170+
171+
private fun isTouchInsideCircle(x: Float, y: Float): Boolean {
172+
val circlePosition = floatArrayOf(0f, 0f)
173+
matrix.mapPoints(circlePosition)
174+
val circleCenterX = circlePosition[0] + circleRadius
175+
val circleCenterY = circlePosition[1] + circleRadius
176+
177+
val distance = Math.sqrt(Math.pow((x - circleCenterX).toDouble(), 2.0) + Math.pow((y - circleCenterY).toDouble(), 2.0))
178+
return distance <= circleRadius
179+
}
180+
181+
override fun onDraw(canvas: Canvas) {
182+
super.onDraw(canvas)
183+
184+
if (isInEditMode) {
185+
// Just draw a simple circle for preview mode
186+
canvas.drawCircle(width / 2f, height / 2f, circleRadius, paint)
187+
return
188+
}
189+
190+
if (parentBitmap == null) {
191+
captureParentView()
192+
}
193+
194+
val circlePosition = floatArrayOf(0f, 0f)
195+
matrix.mapPoints(circlePosition)
196+
197+
// Draw the fixed outer circle
198+
canvas.drawCircle(circlePosition[0] + circleRadius, circlePosition[1] + circleRadius, circleRadius + outerCirclePaint.strokeWidth / 2, outerCirclePaint)
199+
200+
// Draw the draggable inner circle
201+
canvas.drawCircle(circlePosition[0] + circleRadius, circlePosition[1] + circleRadius, circleRadius, paint)
202+
}
203+
204+
override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) {
205+
super.onSizeChanged(w, h, oldw, oldh)
206+
centerCircle() // Re-center the circle when the size changes
207+
}
208+
209+
override fun onTouchEvent(event: MotionEvent): Boolean {
210+
when (event.action) {
211+
MotionEvent.ACTION_DOWN -> {
212+
lastX = event.x
213+
lastY = event.y
214+
isDragging = isTouchInsideCircle(event.x, event.y)
215+
if (isDragging) {
216+
updateCircleColorFromParent()
217+
return true
218+
}
219+
return false
220+
}
221+
MotionEvent.ACTION_MOVE -> {
222+
if (isDragging) {
223+
val dx = event.x - lastX
224+
val dy = event.y - lastY
225+
matrix.postTranslate(dx, dy)
226+
lastX = event.x
227+
lastY = event.y
228+
229+
updateCircleColorFromParent()
230+
invalidate()
231+
return true
232+
}
233+
}
234+
MotionEvent.ACTION_UP -> {
235+
if (isDragging) {
236+
isDragging = false
237+
return true
238+
}
239+
}
240+
}
241+
return false
242+
}
243+
244+
245+
}
246+
247+
248+
249+
250+
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
<resources>
2+
<!-- Define custom attributes for DraggableImageView -->
3+
<declare-styleable name="DraggableImageView">
4+
<attr name="strokeWidth" format="dimension" />
5+
<attr name="outerCircleColor" format="color" />
6+
<attr name="strokeColor" format="color" />
7+
<attr name="circleRadius" format="dimension" />
8+
<attr name="changeStrokeColor" format="boolean" />
9+
</declare-styleable>
10+
</resources>

0 commit comments

Comments
 (0)