diff --git a/README.md b/README.md index d77ca32..164cfad 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,8 @@ app:actionMode="last" // sets action mode 'always' or 'last'. app:initialColor="@color/colorPrimary" app:preferenceName="MyColorPicker" // sets a preference name. app:debounceDuration="200" // sets a debounce duration of the invoking color listener. +app:zoomEnabled="true" // enables the pinch zoom gesture. This attribute will work with only a bitmap palette. +app:maxZoom="5.0" // sets the maximum scale of the zoom gesture. ``` ### ColorListener diff --git a/app/build.gradle.kts b/app/build.gradle.kts index c6c393e..9c02ae2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -32,6 +32,10 @@ android { versionName = Configuration.versionName } + buildFeatures { + viewBinding = true + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 diff --git a/app/src/main/java/com/skydoves/colorpickerviewdemo/MainActivity.java b/app/src/main/java/com/skydoves/colorpickerviewdemo/MainActivity.java index 8861297..45d6b13 100644 --- a/app/src/main/java/com/skydoves/colorpickerviewdemo/MainActivity.java +++ b/app/src/main/java/com/skydoves/colorpickerviewdemo/MainActivity.java @@ -25,20 +25,17 @@ import android.net.Uri; import android.os.Bundle; import android.view.View; -import android.widget.TextView; import androidx.appcompat.app.AppCompatActivity; import androidx.core.content.ContextCompat; -import com.skydoves.colorpickerview.AlphaTileView; import com.skydoves.colorpickerview.ColorEnvelope; import com.skydoves.colorpickerview.ColorPickerDialog; import com.skydoves.colorpickerview.ColorPickerView; import com.skydoves.colorpickerview.flag.BubbleFlag; import com.skydoves.colorpickerview.flag.FlagMode; import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener; -import com.skydoves.colorpickerview.sliders.AlphaSlideBar; -import com.skydoves.colorpickerview.sliders.BrightnessSlideBar; +import com.skydoves.colorpickerviewdemo.databinding.ActivityMainBinding; import com.skydoves.powermenu.OnMenuItemClickListener; import com.skydoves.powermenu.PowerMenu; import com.skydoves.powermenu.PowerMenuItem; @@ -50,6 +47,7 @@ public class MainActivity extends AppCompatActivity { + private ActivityMainBinding binding; private ColorPickerView colorPickerView; private boolean FLAG_PALETTE = false; @@ -75,6 +73,9 @@ public void onItemClick(int position, PowerMenuItem item) { case 3: dialog(); break; + case 4: + zoom(); + break; } powerMenu.dismiss(); } @@ -83,11 +84,12 @@ public void onItemClick(int position, PowerMenuItem item) { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_main); + binding = ActivityMainBinding.inflate(getLayoutInflater()); + setContentView(binding.getRoot()); powerMenu = PowerMenuUtils.getPowerMenu(this, this, powerMenuItemClickListener); - colorPickerView = findViewById(R.id.colorPickerView); + colorPickerView = binding.colorPickerView; BubbleFlag bubbleFlag = new BubbleFlag(this); bubbleFlag.setFlagMode(FlagMode.FADE); colorPickerView.setFlagView(bubbleFlag); @@ -99,12 +101,10 @@ protected void onCreate(Bundle savedInstanceState) { }); // attach alphaSlideBar - final AlphaSlideBar alphaSlideBar = findViewById(R.id.alphaSlideBar); - colorPickerView.attachAlphaSlider(alphaSlideBar); + colorPickerView.attachAlphaSlider(binding.alphaSlideBar); // attach brightnessSlideBar - final BrightnessSlideBar brightnessSlideBar = findViewById(R.id.brightnessSlide); - colorPickerView.attachBrightnessSlider(brightnessSlideBar); + colorPickerView.attachBrightnessSlider(binding.brightnessSlide); colorPickerView.setLifecycleOwner(this); } @@ -115,11 +115,8 @@ protected void onCreate(Bundle savedInstanceState) { */ @SuppressLint("SetTextI18n") private void setLayoutColor(ColorEnvelope envelope) { - TextView textView = findViewById(R.id.textView); - textView.setText("#" + envelope.getHexCode()); - - AlphaTileView alphaTileView = findViewById(R.id.alphaTileView); - alphaTileView.setPaintColor(envelope.getColor()); + binding.textView.setText("#" + envelope.getHexCode()); + binding.alphaTileView.setPaintColor(envelope.getColor()); } /** @@ -162,6 +159,16 @@ private void selector() { FLAG_SELECTOR = !FLAG_SELECTOR; } + /** + * enables or disables the pinch zoom gesture of the palette. the zoom gesture works with only a + * bitmap palette, which can be changed by the Palette menu. + */ + private void zoom() { + boolean enabled = !colorPickerView.isZoomEnabled(); + colorPickerView.setZoomEnabled(enabled); + Timber.d("zoom enabled: %s", enabled); + } + /** * shows ColorPickerDialog */ diff --git a/app/src/main/java/com/skydoves/colorpickerviewdemo/PowerMenuUtils.java b/app/src/main/java/com/skydoves/colorpickerviewdemo/PowerMenuUtils.java index 94582be..32c6219 100644 --- a/app/src/main/java/com/skydoves/colorpickerviewdemo/PowerMenuUtils.java +++ b/app/src/main/java/com/skydoves/colorpickerviewdemo/PowerMenuUtils.java @@ -36,6 +36,7 @@ public static PowerMenu getPowerMenu( .addItem(new PowerMenuItem("Palette(Gallery)", false)) .addItem(new PowerMenuItem("Selector", false)) .addItem(new PowerMenuItem("Dialog", false)) + .addItem(new PowerMenuItem("Zoom", false)) .setLifecycleOwner(lifecycleOwner) .setAnimation(MenuAnimation.SHOWUP_TOP_LEFT) .setCircularEffect(CircularEffect.BODY) diff --git a/colorpickerview/api/colorpickerview.api b/colorpickerview/api/colorpickerview.api index 8684fe1..fcdedcb 100644 --- a/colorpickerview/api/colorpickerview.api +++ b/colorpickerview/api/colorpickerview.api @@ -14,6 +14,10 @@ public class com/skydoves/colorpickerview/AlphaTileView : android/view/View { protected fun onSizeChanged (IIII)V public fun setBackgroundColor (I)V public fun setPaintColor (I)V + public fun setStrokeColor (I)V + public fun setStrokeColorRes (I)V + public fun setStrokeSize (F)V + public fun setStrokeSizeRes (I)V } public class com/skydoves/colorpickerview/ColorEnvelope { @@ -77,10 +81,14 @@ public class com/skydoves/colorpickerview/ColorPickerDialog$Builder : androidx/a public fun setNegativeButton (ILandroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setNegativeButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setNegativeButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setNegativeButtonTextColor (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setNegativeButtonTextColorRes (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setNeutralButton (ILandroid/content/DialogInterface$OnClickListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setNeutralButton (ILandroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setNeutralButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setNeutralButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setNeutralButtonTextColor (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setNeutralButtonTextColorRes (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setOnCancelListener (Landroid/content/DialogInterface$OnCancelListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setOnCancelListener (Landroid/content/DialogInterface$OnCancelListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setOnDismissListener (Landroid/content/DialogInterface$OnDismissListener;)Landroidx/appcompat/app/AlertDialog$Builder; @@ -95,6 +103,8 @@ public class com/skydoves/colorpickerview/ColorPickerDialog$Builder : androidx/a public synthetic fun setPositiveButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setPositiveButton (Ljava/lang/CharSequence;Landroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public fun setPositiveButton (Ljava/lang/CharSequence;Lcom/skydoves/colorpickerview/listeners/ColorPickerViewListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setPositiveButtonTextColor (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; + public fun setPositiveButtonTextColorRes (I)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public fun setPreferenceName (Ljava/lang/String;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; public synthetic fun setSingleChoiceItems (IILandroid/content/DialogInterface$OnClickListener;)Landroidx/appcompat/app/AlertDialog$Builder; public fun setSingleChoiceItems (IILandroid/content/DialogInterface$OnClickListener;)Lcom/skydoves/colorpickerview/ColorPickerDialog$Builder; @@ -132,22 +142,26 @@ public class com/skydoves/colorpickerview/ColorPickerView : android/widget/Frame protected fun getColorFromBitmap (FF)I public fun getDebounceDuration ()J public fun getFlagView ()Lcom/skydoves/colorpickerview/flag/FlagView; + public fun getMaxZoom ()F public fun getPreferenceName ()Ljava/lang/String; public fun getPureColor ()I public fun getSelectedPoint ()Landroid/graphics/Point; public fun getSelector ()Landroid/widget/ImageView; public fun getSelectorX ()F public fun getSelectorY ()F + public fun getZoomScale ()F public fun isHuePalette ()Z public fun isResetBrightnessOnLowSaturationEnabled ()Z public fun isSelectorPointValidationEnabled ()Z public fun isSyncSlidersWithPaletteColorEnabled ()Z + public fun isZoomEnabled ()Z public fun moveSelectorPoint (III)V protected fun onCreateByBuilder (Lcom/skydoves/colorpickerview/ColorPickerView$Builder;)V public fun onDestroy ()V protected fun onSizeChanged (IIII)V public fun onTouchEvent (Landroid/view/MotionEvent;)Z public fun removeLifecycleOwner (Landroidx/lifecycle/LifecycleOwner;)V + public fun resetZoom ()V public fun selectByHsvColor (I)V public fun selectByHsvColorRes (I)V public fun selectCenter ()V @@ -162,6 +176,7 @@ public class com/skydoves/colorpickerview/ColorPickerView : android/widget/Frame public fun setInitialColor (I)V public fun setInitialColorRes (I)V public fun setLifecycleOwner (Landroidx/lifecycle/LifecycleOwner;)V + public fun setMaxZoom (F)V public fun setPaletteDrawable (Landroid/graphics/drawable/Drawable;)V public fun setPreferenceName (Ljava/lang/String;)V public fun setPureColor (I)V @@ -170,6 +185,7 @@ public class com/skydoves/colorpickerview/ColorPickerView : android/widget/Frame public fun setSelectorPoint (II)V public fun setSelectorPointValidation (Z)V public fun setSyncSlidersWithPaletteColor (Z)V + public fun setZoomEnabled (Z)V } public class com/skydoves/colorpickerview/ColorPickerView$Builder { @@ -187,6 +203,7 @@ public class com/skydoves/colorpickerview/ColorPickerView$Builder { public fun setInitialColor (I)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setInitialColorRes (I)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setLifecycleOwner (Landroidx/lifecycle/LifecycleOwner;)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; + public fun setMaxZoom (F)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setPaletteDrawable (Landroid/graphics/drawable/Drawable;)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setPreferenceName (Ljava/lang/String;)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setResetBrightnessOnLowSaturation (Z)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; @@ -196,6 +213,7 @@ public class com/skydoves/colorpickerview/ColorPickerView$Builder { public fun setSelectorSize (I)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setSyncSlidersWithPaletteColor (Z)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; public fun setWidth (I)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; + public fun setZoomEnabled (Z)Lcom/skydoves/colorpickerview/ColorPickerView$Builder; } public class com/skydoves/colorpickerview/FadeUtils { diff --git a/colorpickerview/src/main/java/com/skydoves/colorpickerview/AlphaTileView.java b/colorpickerview/src/main/java/com/skydoves/colorpickerview/AlphaTileView.java index 7e83166..991aa00 100644 --- a/colorpickerview/src/main/java/com/skydoves/colorpickerview/AlphaTileView.java +++ b/colorpickerview/src/main/java/com/skydoves/colorpickerview/AlphaTileView.java @@ -26,7 +26,11 @@ import android.os.Build; import android.util.AttributeSet; import android.view.View; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; +import androidx.annotation.DimenRes; import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; import com.skydoves.colorpickerview.sliders.AlphaTileDrawable; /** AlphaTileView visualizes ARGB color on the canvas using {@link AlphaTileDrawable}. */ @@ -34,7 +38,9 @@ public class AlphaTileView extends View { private Paint colorPaint; + private Paint strokePaint; private Bitmap backgroundBitmap; + private float strokeSize = 0f; private final AlphaTileDrawable.Builder builder = new AlphaTileDrawable.Builder(); public AlphaTileView(Context context) { @@ -64,6 +70,9 @@ public AlphaTileView( private void onCreate() { this.colorPaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); + this.strokePaint.setStyle(Paint.Style.STROKE); + this.strokePaint.setColor(Color.TRANSPARENT); this.setBackgroundColor(Color.WHITE); } @@ -81,6 +90,13 @@ private void getAttrs(AttributeSet attrs) { builder.setTileEvenColor( a.getInt(R.styleable.AlphaTileView_tileEvenColor, builder.getTileEvenColor())); } + if (a.hasValue(R.styleable.AlphaTileView_tileStrokeColor)) { + strokePaint.setColor( + a.getColor(R.styleable.AlphaTileView_tileStrokeColor, strokePaint.getColor())); + } + if (a.hasValue(R.styleable.AlphaTileView_tileStrokeSize)) { + setStrokeSize(a.getDimension(R.styleable.AlphaTileView_tileStrokeSize, strokeSize)); + } } finally { a.recycle(); } @@ -110,6 +126,16 @@ protected void onDraw(Canvas canvas) { canvas.drawBitmap(backgroundBitmap, 0, 0, null); } canvas.drawRect(0, 0, getWidth(), getMeasuredHeight(), colorPaint); + drawStroke(canvas); + } + + /** draws the stroke inside of the view, so it isn't covered by the painted color. */ + private void drawStroke(Canvas canvas) { + if (strokeSize <= 0 || Color.alpha(strokePaint.getColor()) == 0) return; + + float half = strokeSize * 0.5f; + canvas.drawRect( + half, half, getWidth() - half, getMeasuredHeight() - half, strokePaint); } public void setPaintColor(int color) { @@ -117,6 +143,46 @@ public void setPaintColor(int color) { invalidate(); } + /** + * sets a color of the stroke, which is drawn on the edge of the view. it can be used for + * distinguishing the painted color from the background. + * + * @param color a color of the stroke. + */ + public void setStrokeColor(@ColorInt int color) { + strokePaint.setColor(color); + invalidate(); + } + + /** + * sets a color resource of the stroke, which is drawn on the edge of the view. + * + * @param resource a color resource of the stroke. + */ + public void setStrokeColorRes(@ColorRes int resource) { + setStrokeColor(ContextCompat.getColor(getContext(), resource)); + } + + /** + * sets a size of the stroke in pixels, which is drawn on the edge of the view. + * + * @param strokeSize a size of the stroke in pixels. + */ + public void setStrokeSize(float strokeSize) { + this.strokeSize = Math.max(0f, strokeSize); + this.strokePaint.setStrokeWidth(this.strokeSize); + invalidate(); + } + + /** + * sets a size of the stroke using a dimension resource. + * + * @param resource a dimension resource of the stroke size. + */ + public void setStrokeSizeRes(@DimenRes int resource) { + setStrokeSize(getContext().getResources().getDimension(resource)); + } + @Override public void setBackgroundColor(int color) { setPaintColor(color); diff --git a/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerDialog.java b/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerDialog.java index 632d208..8371fe3 100644 --- a/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerDialog.java +++ b/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerDialog.java @@ -23,10 +23,14 @@ import android.view.LayoutInflater; import android.view.View; import android.widget.AdapterView; +import android.widget.Button; import android.widget.ListAdapter; +import androidx.annotation.ColorInt; +import androidx.annotation.ColorRes; import androidx.annotation.NonNull; import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; import com.skydoves.colorpickerview.databinding.ColorpickerviewDialogColorpickerBinding; import com.skydoves.colorpickerview.listeners.ColorEnvelopeListener; @@ -58,6 +62,9 @@ public static class Builder extends AlertDialog.Builder { private boolean shouldAttachAlphaSlideBar = true; private boolean shouldAttachBrightnessSlideBar = true; private int bottomSpace = SizeUtils.dp2Px(getContext(), 10); + private Integer positiveButtonTextColor; + private Integer negativeButtonTextColor; + private Integer neutralButtonTextColor; public Builder(Context context) { super(context); @@ -154,6 +161,69 @@ public Builder setBottomSpace(int bottomSpace) { return this; } + /** + * sets a text color of the positive button. + * + * @param color a text color of the positive button. + * @return {@link Builder}. + */ + public Builder setPositiveButtonTextColor(@ColorInt int color) { + this.positiveButtonTextColor = color; + return this; + } + + /** + * sets a text color resource of the positive button. + * + * @param resource a text color resource of the positive button. + * @return {@link Builder}. + */ + public Builder setPositiveButtonTextColorRes(@ColorRes int resource) { + return setPositiveButtonTextColor(ContextCompat.getColor(getContext(), resource)); + } + + /** + * sets a text color of the negative button. + * + * @param color a text color of the negative button. + * @return {@link Builder}. + */ + public Builder setNegativeButtonTextColor(@ColorInt int color) { + this.negativeButtonTextColor = color; + return this; + } + + /** + * sets a text color resource of the negative button. + * + * @param resource a text color resource of the negative button. + * @return {@link Builder}. + */ + public Builder setNegativeButtonTextColorRes(@ColorRes int resource) { + return setNegativeButtonTextColor(ContextCompat.getColor(getContext(), resource)); + } + + /** + * sets a text color of the neutral button. + * + * @param color a text color of the neutral button. + * @return {@link Builder}. + */ + public Builder setNeutralButtonTextColor(@ColorInt int color) { + this.neutralButtonTextColor = color; + return this; + } + + /** + * sets a text color resource of the neutral button. + * + * @param resource a text color resource of the neutral button. + * @return {@link Builder}. + */ + public Builder setNeutralButtonTextColorRes(@ColorRes int resource) { + return setNeutralButtonTextColor(ContextCompat.getColor(getContext(), resource)); + } + /** * sets positive button with {@link ColorPickerViewListener} on the {@link ColorPickerDialog}. * @@ -248,7 +318,35 @@ public AlertDialog create() { } super.setView(dialogBinding.getRoot()); - return super.create(); + + AlertDialog dialog = super.create(); + applyButtonTextColors(dialog); + return dialog; + } + + /** the buttons are created when the dialog is shown, so the colors are applied on showing. */ + private void applyButtonTextColors(AlertDialog dialog) { + if (positiveButtonTextColor == null + && negativeButtonTextColor == null + && neutralButtonTextColor == null) { + return; + } + + dialog.setOnShowListener( + dialogInterface -> { + setButtonTextColor(dialog, AlertDialog.BUTTON_POSITIVE, positiveButtonTextColor); + setButtonTextColor(dialog, AlertDialog.BUTTON_NEGATIVE, negativeButtonTextColor); + setButtonTextColor(dialog, AlertDialog.BUTTON_NEUTRAL, neutralButtonTextColor); + }); + } + + private void setButtonTextColor(AlertDialog dialog, int whichButton, Integer color) { + if (color == null) return; + + Button button = dialog.getButton(whichButton); + if (button != null) { + button.setTextColor(color); + } } @Override diff --git a/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerView.java b/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerView.java index ef01ff5..f58a1f8 100644 --- a/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerView.java +++ b/colorpickerview/src/main/java/com/skydoves/colorpickerview/ColorPickerView.java @@ -25,6 +25,7 @@ import android.graphics.Matrix; import android.graphics.Point; import android.graphics.Rect; +import android.graphics.RectF; import android.graphics.drawable.BitmapDrawable; import android.graphics.drawable.Drawable; import android.os.Build; @@ -32,6 +33,7 @@ import android.util.AttributeSet; import android.view.Gravity; import android.view.MotionEvent; +import android.view.ScaleGestureDetector; import android.view.ViewGroup; import android.view.ViewTreeObserver; import android.widget.FrameLayout; @@ -72,6 +74,9 @@ @SuppressWarnings("unused") public class ColorPickerView extends FrameLayout implements LifecycleObserver { + private static final float MIN_ZOOM = 1.0f; + private static final float DEFAULT_MAX_ZOOM = 3.0f; + @ColorInt private int selectedPureColor; @ColorInt @@ -107,6 +112,15 @@ public class ColorPickerView extends FrameLayout implements LifecycleObserver { private boolean selectorPointValidation = true; private boolean resetBrightnessOnLowSaturation = true; private boolean syncSlidersWithPaletteColor = true; + private boolean zoomEnabled = false; + private float maxZoom = DEFAULT_MAX_ZOOM; + private float zoomScale = MIN_ZOOM; + private final Matrix paletteFitMatrix = new Matrix(); + private final Matrix paletteZoomMatrix = new Matrix(); + private ScaleGestureDetector zoomGestureDetector; + private boolean multiTouching = false; + private float lastFocusX; + private float lastFocusY; private final ColorPickerPreferenceManager preferenceManager = ColorPickerPreferenceManager.getInstance(getContext()); @@ -188,6 +202,12 @@ private void getAttrs(AttributeSet attrs) { this.syncSlidersWithPaletteColor = a.getBoolean(R.styleable.ColorPickerView_syncSlidersWithPaletteColor, syncSlidersWithPaletteColor); } + if (a.hasValue(R.styleable.ColorPickerView_zoomEnabled)) { + this.zoomEnabled = a.getBoolean(R.styleable.ColorPickerView_zoomEnabled, zoomEnabled); + } + if (a.hasValue(R.styleable.ColorPickerView_maxZoom)) { + setMaxZoom(a.getFloat(R.styleable.ColorPickerView_maxZoom, maxZoom)); + } } finally { a.recycle(); } @@ -221,6 +241,17 @@ private void onCreate() { addView(selector, selectorParam); selector.setAlpha(selector_alpha); + zoomGestureDetector = + new ScaleGestureDetector( + getContext(), + new ScaleGestureDetector.SimpleOnScaleGestureListener() { + @Override + public boolean onScale(@NonNull ScaleGestureDetector detector) { + zoomPalette(detector.getScaleFactor(), detector.getFocusX(), detector.getFocusY()); + return true; + } + }); + getViewTreeObserver() .addOnGlobalLayoutListener( new ViewTreeObserver.OnGlobalLayoutListener() { @@ -236,10 +267,12 @@ public void onGlobalLayout() { protected void onSizeChanged(int width, int height, int oldWidth, int oldHeight) { super.onSizeChanged(width, height, oldWidth, oldHeight); - if (palette.getDrawable() == null) { + if (palette.getDrawable() == null && width > 0 && height > 0) { Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); palette.setImageDrawable(new ColorHsvPalette(getResources(), bitmap)); } + + updatePaletteZoom(); } private void onFinishInflated() { @@ -296,6 +329,8 @@ protected void onCreateByBuilder(Builder builder) { this.selectorPointValidation = builder.selectorPointValidation; this.resetBrightnessOnLowSaturation = builder.resetBrightnessOnLowSaturation; this.syncSlidersWithPaletteColor = builder.syncSlidersWithPaletteColor; + this.zoomEnabled = builder.zoomEnabled; + setMaxZoom(builder.maxZoom); } @SuppressLint("ClickableViewAccessibility") @@ -304,6 +339,10 @@ public boolean onTouchEvent(MotionEvent event) { if (!this.isEnabled()) { return false; } + if (isZoomable() && onZoomTouchReceived(event)) { + return true; + } + switch (event.getActionMasked()) { case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_MOVE: @@ -357,6 +396,209 @@ private boolean onTouchReceived(final MotionEvent event) { return true; } + /** + * handles a multi touch event for zooming and moving the palette. + * + * @param event {@link MotionEvent}. + * @return the event is consumed by the zoom gesture or not. + */ + private boolean onZoomTouchReceived(MotionEvent event) { + zoomGestureDetector.onTouchEvent(event); + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + multiTouching = false; + break; + case MotionEvent.ACTION_POINTER_DOWN: + multiTouching = true; + selector.setPressed(false); + updateLastFocus(event, -1); + break; + case MotionEvent.ACTION_MOVE: + if (multiTouching && event.getPointerCount() > 1) { + float focusX = getFocus(event, -1, true); + float focusY = getFocus(event, -1, false); + movePalette(focusX - lastFocusX, focusY - lastFocusY); + lastFocusX = focusX; + lastFocusY = focusY; + } + break; + case MotionEvent.ACTION_POINTER_UP: + // recalculates the focus without the released pointer to avoid jumping of the palette. + updateLastFocus(event, event.getActionIndex()); + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + boolean consumed = multiTouching; + multiTouching = false; + return consumed; + default: + break; + } + return multiTouching; + } + + private void updateLastFocus(MotionEvent event, int excludedPointerIndex) { + lastFocusX = getFocus(event, excludedPointerIndex, true); + lastFocusY = getFocus(event, excludedPointerIndex, false); + } + + /** gets the center coordinate of the pointers, which is used as a focus of the zoom gesture. */ + private float getFocus(MotionEvent event, int excludedPointerIndex, boolean isXAxis) { + float sum = 0; + int count = 0; + for (int index = 0; index < event.getPointerCount(); index++) { + if (index == excludedPointerIndex) continue; + sum += isXAxis ? event.getX(index) : event.getY(index); + count++; + } + return count == 0 ? 0 : sum / count; + } + + /** zooms the palette by the given scale factor based on the focus of the gesture. */ + private void zoomPalette(float scaleFactor, float focusX, float focusY) { + float scale = zoomScale * scaleFactor; + if (scale < MIN_ZOOM) { + scaleFactor = MIN_ZOOM / zoomScale; + } else if (scale > maxZoom) { + scaleFactor = maxZoom / zoomScale; + } + if (scaleFactor == 1.0f) return; + + Matrix delta = new Matrix(); + delta.setScale(scaleFactor, scaleFactor, focusX, focusY); + transformPalette(delta); + } + + /** moves the zoomed palette by the given distances. */ + private void movePalette(float dx, float dy) { + if (dx == 0 && dy == 0) return; + + Matrix delta = new Matrix(); + delta.setTranslate(dx, dy); + transformPalette(delta); + } + + /** applies the given transformation to the palette and the selector together. */ + private void transformPalette(Matrix delta) { + paletteZoomMatrix.postConcat(delta); + + Matrix boundary = getPaletteBoundaryMatrix(); + if (boundary != null) { + paletteZoomMatrix.postConcat(boundary); + delta.postConcat(boundary); + } + + float[] values = new float[9]; + paletteZoomMatrix.getValues(values); + zoomScale = values[Matrix.MSCALE_X]; + + applyPaletteMatrix(); + moveSelectorByMatrix(delta); + } + + /** + * gets a translation matrix, which keeps the zoomed palette covering the view. returns null if + * the palette is already in the boundary. + */ + private @Nullable Matrix getPaletteBoundaryMatrix() { + Drawable drawable = palette.getDrawable(); + if (drawable == null) return null; + + Matrix matrix = new Matrix(paletteFitMatrix); + matrix.postConcat(paletteZoomMatrix); + RectF rect = new RectF(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); + matrix.mapRect(rect); + + float dx = 0; + if (rect.width() <= getWidth()) { + dx = (getWidth() - rect.width()) * 0.5f - rect.left; + } else if (rect.left > 0) { + dx = -rect.left; + } else if (rect.right < getWidth()) { + dx = getWidth() - rect.right; + } + + float dy = 0; + if (rect.height() <= getHeight()) { + dy = (getHeight() - rect.height()) * 0.5f - rect.top; + } else if (rect.top > 0) { + dy = -rect.top; + } else if (rect.bottom < getHeight()) { + dy = getHeight() - rect.bottom; + } + + if (dx == 0 && dy == 0) return null; + + Matrix boundary = new Matrix(); + boundary.setTranslate(dx, dy); + return boundary; + } + + /** keeps the selector on the same position of the palette after the palette is transformed. */ + private void moveSelectorByMatrix(Matrix delta) { + if (selectedPoint == null) return; + + float[] points = new float[] {selectedPoint.x, selectedPoint.y}; + delta.mapPoints(points); + selectedPoint = new Point((int) points[0], (int) points[1]); + setCoordinate(selectedPoint.x, selectedPoint.y); + notifyToFlagView(selectedPoint); + } + + private void applyPaletteMatrix() { + Matrix matrix = new Matrix(paletteFitMatrix); + matrix.postConcat(paletteZoomMatrix); + palette.setScaleType(ImageView.ScaleType.MATRIX); + palette.setImageMatrix(matrix); + } + + /** + * calculates the matrix, which draws the palette drawable as the {@link + * ImageView.ScaleType#FIT_CENTER} does. + * + * @return the matrix is calculated or not. + */ + private boolean updatePaletteFitMatrix() { + Drawable drawable = palette.getDrawable(); + if (drawable == null || getWidth() == 0 || getHeight() == 0) return false; + + float width = drawable.getIntrinsicWidth(); + float height = drawable.getIntrinsicHeight(); + if (width <= 0 || height <= 0) return false; + + float scale = Math.min(getWidth() / width, getHeight() / height); + paletteFitMatrix.setScale(scale, scale); + paletteFitMatrix.postTranslate( + (getWidth() - width * scale) * 0.5f, (getHeight() - height * scale) * 0.5f); + return true; + } + + /** restores the zoom state whenever the palette drawable or the size of the view is changed. */ + private void updatePaletteZoom() { + if (!zoomEnabled || palette == null) return; + + if (isZoomable() && updatePaletteFitMatrix()) { + paletteZoomMatrix.reset(); + zoomScale = MIN_ZOOM; + applyPaletteMatrix(); + } else { + restorePaletteScaleType(); + } + } + + private void restorePaletteScaleType() { + paletteZoomMatrix.reset(); + zoomScale = MIN_ZOOM; + palette.setScaleType(ImageView.ScaleType.FIT_CENTER); + palette.setImageMatrix(new Matrix()); + } + + /** the zoom gesture works only with a bitmap palette, not with the default HSV palette. */ + private boolean isZoomable() { + return zoomEnabled && palette.getDrawable() != null && !isHuePalette(); + } + /** * synchronizes the {@link AlphaSlideBar} and the {@link BrightnessSlideBar} with a color, which * is picked from a bitmap palette. @@ -831,6 +1073,7 @@ public void setPaletteDrawable(Drawable drawable) { selectedPureColor = Color.WHITE; notifyToSlideBars(); + updatePaletteZoom(); if (flagView != null) { removeView(flagView); @@ -1092,6 +1335,79 @@ public void setSyncSlidersWithPaletteColor(boolean enabled) { this.syncSlidersWithPaletteColor = enabled; } + /** + * Returns whether the palette can be zoomed by a pinch gesture. + * + * @return true if the zoom gesture is enabled, false otherwise. + */ + public boolean isZoomEnabled() { + return zoomEnabled; + } + + /** + * Sets whether the palette can be zoomed by a pinch gesture. + * + *
When enabled, a pinch gesture zooms the palette in and out, and a two finger drag moves the + * zoomed palette. A single touch still selects a color, so tiny color spots of an image can be + * picked more precisely. + * + *
The zoom gesture works only with a bitmap palette, which is set by {@link
+ * #setPaletteDrawable(Drawable)}. It does not affect the default HSV palette.
+ *
+ * @param enabled true to enable the zoom gesture, false to disable.
+ */
+ public void setZoomEnabled(boolean enabled) {
+ if (this.zoomEnabled == enabled) return;
+
+ this.zoomEnabled = enabled;
+ if (palette == null) return;
+
+ if (enabled) {
+ updatePaletteZoom();
+ } else {
+ restorePaletteScaleType();
+ }
+ }
+
+ /**
+ * gets the maximum scale of the zoom gesture.
+ *
+ * @return the maximum scale of the zoom gesture.
+ */
+ public float getMaxZoom() {
+ return maxZoom;
+ }
+
+ /**
+ * sets the maximum scale of the zoom gesture, which should be bigger than 1.0.
+ *
+ * @param maxZoom the maximum scale of the zoom gesture.
+ */
+ public void setMaxZoom(float maxZoom) {
+ this.maxZoom = Math.max(MIN_ZOOM, maxZoom);
+ }
+
+ /**
+ * gets the current scale of the zoomed palette.
+ *
+ * @return the current scale of the zoomed palette.
+ */
+ public float getZoomScale() {
+ return zoomScale;
+ }
+
+ /** restores the zoomed palette to its original scale and position. */
+ public void resetZoom() {
+ if (!isZoomable()) return;
+
+ Matrix delta = new Matrix();
+ if (paletteZoomMatrix.invert(delta)) {
+ transformPalette(delta);
+ } else {
+ updatePaletteZoom();
+ }
+ }
+
/**
* sets the {@link LifecycleOwner}.
*
@@ -1156,6 +1472,8 @@ public static class Builder {
private boolean selectorPointValidation = true;
private boolean resetBrightnessOnLowSaturation = true;
private boolean syncSlidersWithPaletteColor = true;
+ private boolean zoomEnabled = false;
+ private float maxZoom = DEFAULT_MAX_ZOOM;
public Builder(Context context) {
this.context = context;
@@ -1266,6 +1584,16 @@ public Builder setSyncSlidersWithPaletteColor(boolean enabled) {
return this;
}
+ public Builder setZoomEnabled(boolean enabled) {
+ this.zoomEnabled = enabled;
+ return this;
+ }
+
+ public Builder setMaxZoom(float maxZoom) {
+ this.maxZoom = maxZoom;
+ return this;
+ }
+
public ColorPickerView build() {
ColorPickerView colorPickerView = new ColorPickerView(context);
colorPickerView.onCreateByBuilder(this);
diff --git a/colorpickerview/src/main/res/values/attrs_colorpicker.xml b/colorpickerview/src/main/res/values/attrs_colorpicker.xml
index c60e93c..02c7f37 100644
--- a/colorpickerview/src/main/res/values/attrs_colorpicker.xml
+++ b/colorpickerview/src/main/res/values/attrs_colorpicker.xml
@@ -47,6 +47,10 @@