├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── art
├── demo.gif
├── screenshot1.png
├── screenshot2.png
└── screenshot3.png
├── build.gradle
├── demo
├── .gitignore
├── build.gradle
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── jaredrummler
│ │ └── android
│ │ └── colorpicker
│ │ └── demo
│ │ ├── BasePreferenceFragment.java
│ │ ├── ColorPickerActivity.java
│ │ ├── DemoFragment.java
│ │ └── MainActivity.java
│ └── res
│ ├── drawable
│ ├── ic_github_white_24dp.xml
│ └── ic_palette_white_24dp.xml
│ ├── layout-v21
│ └── activity_color_picker.xml
│ ├── layout
│ └── activity_color_picker.xml
│ ├── menu
│ └── main.xml
│ ├── mipmap-hdpi
│ └── ic_launcher.png
│ ├── mipmap-mdpi
│ └── ic_launcher.png
│ ├── mipmap-xhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxhdpi
│ └── ic_launcher.png
│ ├── mipmap-xxxhdpi
│ └── ic_launcher.png
│ ├── values-v21
│ └── styles.xml
│ ├── values-w820dp
│ └── dimens.xml
│ ├── values
│ ├── colors.xml
│ ├── dimens.xml
│ ├── strings.xml
│ └── styles.xml
│ └── xml
│ └── main.xml
├── gradle.properties
├── gradle
├── maven-push.gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── library
├── .gitignore
├── build.gradle
├── gradle.properties
├── proguard-rules.pro
└── src
│ └── main
│ ├── AndroidManifest.xml
│ ├── java
│ └── com
│ │ └── jaredrummler
│ │ └── android
│ │ └── colorpicker
│ │ ├── AlphaPatternDrawable.java
│ │ ├── ColorPaletteAdapter.java
│ │ ├── ColorPanelView.java
│ │ ├── ColorPickerDialog.java
│ │ ├── ColorPickerDialogListener.java
│ │ ├── ColorPickerView.java
│ │ ├── ColorPreference.java
│ │ ├── ColorPreferenceCompat.java
│ │ ├── ColorShape.java
│ │ ├── DrawingUtils.java
│ │ └── NestedGridView.java
│ └── res
│ ├── drawable-hdpi
│ └── cpv_alpha.png
│ ├── drawable-xhdpi
│ └── cpv_alpha.png
│ ├── drawable-xxhdpi
│ └── cpv_alpha.png
│ ├── drawable
│ ├── cpv_btn_background.xml
│ ├── cpv_btn_background_pressed.xml
│ ├── cpv_ic_arrow_right_black_24dp.xml
│ └── cpv_preset_checked.xml
│ ├── layout
│ ├── cpv_color_item_circle.xml
│ ├── cpv_color_item_square.xml
│ ├── cpv_dialog_color_picker.xml
│ ├── cpv_dialog_presets.xml
│ ├── cpv_preference_circle.xml
│ ├── cpv_preference_circle_large.xml
│ ├── cpv_preference_square.xml
│ └── cpv_preference_square_large.xml
│ ├── values-ar
│ └── strings.xml
│ ├── values-be
│ └── strings.xml
│ ├── values-cs
│ └── strings.xml
│ ├── values-de
│ └── strings.xml
│ ├── values-es
│ └── strings.xml
│ ├── values-fr-rFR
│ └── strings.xml
│ ├── values-it-rIT
│ └── strings.xml
│ ├── values-iw
│ └── strings.xml
│ ├── values-ja
│ └── strings.xml
│ ├── values-nl-rNL
│ └── strings.xml
│ ├── values-pl
│ └── strings.xml
│ ├── values-pt
│ └── strings.xml
│ ├── values-ru
│ └── strings.xml
│ ├── values-sk
│ └── strings.xml
│ ├── values-tr
│ └── strings.xml
│ ├── values-zh
│ └── strings.xml
│ └── values
│ ├── attrs.xml
│ ├── dimen.xml
│ ├── ids.xml
│ ├── strings.xml
│ └── styles.xml
└── settings.gradle
/.gitignore:
--------------------------------------------------------------------------------
1 | ########### Specifies intentionally untracked files to ignore ###########
2 |
3 | ### Gradle
4 | .gradle/
5 | build/
6 |
7 | ### IntelliJ IDEA
8 | /.idea
9 | *.iml
10 | *.iws
11 | captures/
12 | .navigation/
13 | local.properties
14 | bin/
15 | gen/
16 | out/
17 | *.apk
18 | *.ap_
19 |
20 | ### Android
21 | *.jks
22 | *.dex
23 |
24 | ### Java
25 | *.class
26 | hs_err_pid*
27 |
28 | ### Windows
29 | Desktop.ini
30 | Thumbs.db
31 | ehthumbs.db
32 |
33 | ### OSX
34 | .DS_Store
35 |
36 | ### Linux
37 | *~
38 | .fuse_hidden*
39 | .directory
40 | .Trash-*
41 |
42 | ### Logs
43 | *.log
44 |
45 | ### Crashlytics
46 | com_crashlytics_export_strings.xml
47 | crashlytics.properties
48 | crashlytics-build.properties
49 | fabric.properties
50 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: android
2 |
3 | android:
4 | components:
5 | - tools # Tools
6 | - platform-tools # Platform tools
7 | - build-tools-28.0.3 # Build tools version
8 | - android-28 # Target SDK version
9 | - extra-android-m2repository # Support repo
10 | - sys-img-armeabi-v7a-android-18 # Emulator
11 |
12 | jdk:
13 | - oraclejdk8
14 |
15 | script:
16 | - ./gradlew build
17 |
18 | branches:
19 | except:
20 | - gh-pages
21 |
22 | notifications:
23 | email: false
24 |
25 | sudo: false
26 |
27 | cache:
28 | directories:
29 | - $HOME/.gradle
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "{}"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2016 Jared Rummler
190 | Copyright 2015 Daniel Nilsson
191 |
192 | Licensed under the Apache License, Version 2.0 (the "License");
193 | you may not use this file except in compliance with the License.
194 | You may obtain a copy of the License at
195 |
196 | http://www.apache.org/licenses/LICENSE-2.0
197 |
198 | Unless required by applicable law or agreed to in writing, software
199 | distributed under the License is distributed on an "AS IS" BASIS,
200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
201 | See the License for the specific language governing permissions and
202 | limitations under the License.
203 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Color Picker
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | Yet another open source color picker for Android. So, why should you use this color picker? It is highly customizable and easy to use. You can simply add the `ColorPreference` to your preferences and a beautiful color picker dialog will be displayed without additional code. The color picker supports alpha and allows you to set your own presets.
12 |
13 | The original ColorPickerView was written by [Daniel Nilsson](https://github.com/danielnilsson9/color-picker-view).
14 |
15 | ## Screenshots
16 | 
17 |
18 |
19 |
20 | ## Usage
21 |
22 | Add the `ColorPreference` to your preference XML:
23 |
24 | ```xml
25 |
A dialog to pick a color.
59 | * 60 | *The {@link Activity activity} that shows this dialog should implement {@link ColorPickerDialogListener}
61 | * 62 | *Example usage:
63 | * 64 | *65 | * ColorPickerDialog.newBuilder().show(activity); 66 | *67 | */ 68 | public class ColorPickerDialog extends DialogFragment implements ColorPickerView.OnColorChangedListener, TextWatcher { 69 | 70 | private static final String TAG = "ColorPickerDialog"; 71 | 72 | public static final int TYPE_CUSTOM = 0; 73 | public static final int TYPE_PRESETS = 1; 74 | 75 | /** 76 | * Material design colors used as the default color presets 77 | */ 78 | public static final int[] MATERIAL_COLORS = { 79 | 0xFFF44336, // RED 500 80 | 0xFFE91E63, // PINK 500 81 | 0xFFFF2C93, // LIGHT PINK 500 82 | 0xFF9C27B0, // PURPLE 500 83 | 0xFF673AB7, // DEEP PURPLE 500 84 | 0xFF3F51B5, // INDIGO 500 85 | 0xFF2196F3, // BLUE 500 86 | 0xFF03A9F4, // LIGHT BLUE 500 87 | 0xFF00BCD4, // CYAN 500 88 | 0xFF009688, // TEAL 500 89 | 0xFF4CAF50, // GREEN 500 90 | 0xFF8BC34A, // LIGHT GREEN 500 91 | 0xFFCDDC39, // LIME 500 92 | 0xFFFFEB3B, // YELLOW 500 93 | 0xFFFFC107, // AMBER 500 94 | 0xFFFF9800, // ORANGE 500 95 | 0xFF795548, // BROWN 500 96 | 0xFF607D8B, // BLUE GREY 500 97 | 0xFF9E9E9E, // GREY 500 98 | }; 99 | 100 | static final int ALPHA_THRESHOLD = 165; 101 | 102 | private static final String ARG_ID = "id"; 103 | private static final String ARG_TYPE = "dialogType"; 104 | private static final String ARG_COLOR = "color"; 105 | private static final String ARG_ALPHA = "alpha"; 106 | private static final String ARG_PRESETS = "presets"; 107 | private static final String ARG_ALLOW_PRESETS = "allowPresets"; 108 | private static final String ARG_ALLOW_CUSTOM = "allowCustom"; 109 | private static final String ARG_DIALOG_TITLE = "dialogTitle"; 110 | private static final String ARG_SHOW_COLOR_SHADES = "showColorShades"; 111 | private static final String ARG_COLOR_SHAPE = "colorShape"; 112 | private static final String ARG_PRESETS_BUTTON_TEXT = "presetsButtonText"; 113 | private static final String ARG_CUSTOM_BUTTON_TEXT = "customButtonText"; 114 | private static final String ARG_SELECTED_BUTTON_TEXT = "selectedButtonText"; 115 | 116 | ColorPickerDialogListener colorPickerDialogListener; 117 | FrameLayout rootView; 118 | int[] presets; 119 | @ColorInt int color; 120 | int dialogType; 121 | int dialogId; 122 | boolean showColorShades; 123 | int colorShape; 124 | 125 | // -- PRESETS -------------------------- 126 | ColorPaletteAdapter adapter; 127 | LinearLayout shadesLayout; 128 | SeekBar transparencySeekBar; 129 | TextView transparencyPercText; 130 | 131 | // -- CUSTOM --------------------------- 132 | ColorPickerView colorPicker; 133 | ColorPanelView newColorPanel; 134 | EditText hexEditText; 135 | boolean showAlphaSlider; 136 | private int presetsButtonStringRes; 137 | private boolean fromEditText; 138 | private int customButtonStringRes; 139 | 140 | private final OnTouchListener onPickerTouchListener = new OnTouchListener() { 141 | @Override public boolean onTouch(View v, MotionEvent event) { 142 | if (v != hexEditText && hexEditText.hasFocus()) { 143 | hexEditText.clearFocus(); 144 | InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 145 | imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); 146 | hexEditText.clearFocus(); 147 | return true; 148 | } 149 | return false; 150 | } 151 | }; 152 | 153 | /** 154 | * Create a new Builder for creating a {@link ColorPickerDialog} instance 155 | * 156 | * @return The {@link Builder builder} to create the {@link ColorPickerDialog}. 157 | */ 158 | public static Builder newBuilder() { 159 | return new Builder(); 160 | } 161 | 162 | @Override public Dialog onCreateDialog(Bundle savedInstanceState) { 163 | dialogId = getArguments().getInt(ARG_ID); 164 | showAlphaSlider = getArguments().getBoolean(ARG_ALPHA); 165 | showColorShades = getArguments().getBoolean(ARG_SHOW_COLOR_SHADES); 166 | colorShape = getArguments().getInt(ARG_COLOR_SHAPE); 167 | if (savedInstanceState == null) { 168 | color = getArguments().getInt(ARG_COLOR); 169 | dialogType = getArguments().getInt(ARG_TYPE); 170 | } else { 171 | color = savedInstanceState.getInt(ARG_COLOR); 172 | dialogType = savedInstanceState.getInt(ARG_TYPE); 173 | } 174 | 175 | rootView = new FrameLayout(requireActivity()); 176 | if (dialogType == TYPE_CUSTOM) { 177 | rootView.addView(createPickerView()); 178 | } else if (dialogType == TYPE_PRESETS) { 179 | rootView.addView(createPresetsView()); 180 | } 181 | 182 | int selectedButtonStringRes = getArguments().getInt(ARG_SELECTED_BUTTON_TEXT); 183 | if (selectedButtonStringRes == 0) { 184 | selectedButtonStringRes = R.string.cpv_select; 185 | } 186 | 187 | AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()).setView(rootView) 188 | .setPositiveButton(selectedButtonStringRes, new DialogInterface.OnClickListener() { 189 | @Override public void onClick(DialogInterface dialog, int which) { 190 | onColorSelected(color); 191 | } 192 | }); 193 | 194 | int dialogTitleStringRes = getArguments().getInt(ARG_DIALOG_TITLE); 195 | if (dialogTitleStringRes != 0) { 196 | builder.setTitle(dialogTitleStringRes); 197 | } 198 | 199 | presetsButtonStringRes = getArguments().getInt(ARG_PRESETS_BUTTON_TEXT); 200 | customButtonStringRes = getArguments().getInt(ARG_CUSTOM_BUTTON_TEXT); 201 | 202 | int neutralButtonStringRes; 203 | if (dialogType == TYPE_CUSTOM && getArguments().getBoolean(ARG_ALLOW_PRESETS)) { 204 | neutralButtonStringRes = (presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); 205 | } else if (dialogType == TYPE_PRESETS && getArguments().getBoolean(ARG_ALLOW_CUSTOM)) { 206 | neutralButtonStringRes = (customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); 207 | } else { 208 | neutralButtonStringRes = 0; 209 | } 210 | 211 | if (neutralButtonStringRes != 0) { 212 | builder.setNeutralButton(neutralButtonStringRes, null); 213 | } 214 | 215 | return builder.create(); 216 | } 217 | 218 | @Override public void onStart() { 219 | super.onStart(); 220 | AlertDialog dialog = (AlertDialog) getDialog(); 221 | 222 | // http://stackoverflow.com/a/16972670/1048340 223 | //noinspection ConstantConditions 224 | dialog.getWindow() 225 | .clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); 226 | dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); 227 | 228 | // Do not dismiss the dialog when clicking the neutral button. 229 | Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); 230 | if (neutralButton != null) { 231 | neutralButton.setOnClickListener(new View.OnClickListener() { 232 | @Override public void onClick(View v) { 233 | rootView.removeAllViews(); 234 | switch (dialogType) { 235 | case TYPE_CUSTOM: 236 | dialogType = TYPE_PRESETS; 237 | ((Button) v).setText(customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); 238 | rootView.addView(createPresetsView()); 239 | break; 240 | case TYPE_PRESETS: 241 | dialogType = TYPE_CUSTOM; 242 | ((Button) v).setText(presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); 243 | rootView.addView(createPickerView()); 244 | } 245 | } 246 | }); 247 | } 248 | } 249 | 250 | @Override public void onDismiss(DialogInterface dialog) { 251 | super.onDismiss(dialog); 252 | onDialogDismissed(); 253 | } 254 | 255 | @Override public void onSaveInstanceState(Bundle outState) { 256 | outState.putInt(ARG_COLOR, color); 257 | outState.putInt(ARG_TYPE, dialogType); 258 | super.onSaveInstanceState(outState); 259 | } 260 | 261 | /** 262 | * Set the callback. 263 | * 264 | * Note: The preferred way to handle the callback is to have the calling Activity implement 265 | * {@link ColorPickerDialogListener} as this will not survive an orientation change. 266 | * 267 | * @param colorPickerDialogListener The callback invoked when a color is selected or the dialog is dismissed. 268 | */ 269 | public void setColorPickerDialogListener(ColorPickerDialogListener colorPickerDialogListener) { 270 | this.colorPickerDialogListener = colorPickerDialogListener; 271 | } 272 | 273 | // region Custom Picker 274 | 275 | View createPickerView() { 276 | View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null); 277 | colorPicker = (ColorPickerView) contentView.findViewById(R.id.cpv_color_picker_view); 278 | ColorPanelView oldColorPanel = (ColorPanelView) contentView.findViewById(R.id.cpv_color_panel_old); 279 | newColorPanel = (ColorPanelView) contentView.findViewById(R.id.cpv_color_panel_new); 280 | ImageView arrowRight = (ImageView) contentView.findViewById(R.id.cpv_arrow_right); 281 | hexEditText = (EditText) contentView.findViewById(R.id.cpv_hex); 282 | 283 | try { 284 | final TypedValue value = new TypedValue(); 285 | TypedArray typedArray = 286 | getActivity().obtainStyledAttributes(value.data, new int[] { android.R.attr.textColorPrimary }); 287 | int arrowColor = typedArray.getColor(0, Color.BLACK); 288 | typedArray.recycle(); 289 | arrowRight.setColorFilter(arrowColor); 290 | } catch (Exception ignored) { 291 | } 292 | 293 | colorPicker.setAlphaSliderVisible(showAlphaSlider); 294 | oldColorPanel.setColor(getArguments().getInt(ARG_COLOR)); 295 | colorPicker.setColor(color, true); 296 | newColorPanel.setColor(color); 297 | setHex(color); 298 | 299 | if (!showAlphaSlider) { 300 | hexEditText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(6) }); 301 | } 302 | 303 | newColorPanel.setOnClickListener(new View.OnClickListener() { 304 | @Override public void onClick(View v) { 305 | if (newColorPanel.getColor() == color) { 306 | onColorSelected(color); 307 | dismiss(); 308 | } 309 | } 310 | }); 311 | 312 | contentView.setOnTouchListener(onPickerTouchListener); 313 | colorPicker.setOnColorChangedListener(this); 314 | hexEditText.addTextChangedListener(this); 315 | 316 | hexEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { 317 | @Override public void onFocusChange(View v, boolean hasFocus) { 318 | if (hasFocus) { 319 | InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 320 | imm.showSoftInput(hexEditText, InputMethodManager.SHOW_IMPLICIT); 321 | } 322 | } 323 | }); 324 | 325 | return contentView; 326 | } 327 | 328 | @Override public void onColorChanged(int newColor) { 329 | color = newColor; 330 | if (newColorPanel != null) { 331 | newColorPanel.setColor(newColor); 332 | } 333 | if (!fromEditText && hexEditText != null) { 334 | setHex(newColor); 335 | if (hexEditText.hasFocus()) { 336 | InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); 337 | imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); 338 | hexEditText.clearFocus(); 339 | } 340 | } 341 | fromEditText = false; 342 | } 343 | 344 | @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { 345 | 346 | } 347 | 348 | @Override public void onTextChanged(CharSequence s, int start, int before, int count) { 349 | 350 | } 351 | 352 | @Override public void afterTextChanged(Editable s) { 353 | if (hexEditText.isFocused()) { 354 | int color = parseColorString(s.toString()); 355 | if (color != colorPicker.getColor()) { 356 | fromEditText = true; 357 | colorPicker.setColor(color, true); 358 | } 359 | } 360 | } 361 | 362 | private void setHex(int color) { 363 | if (showAlphaSlider) { 364 | hexEditText.setText(String.format("%08X", (color))); 365 | } else { 366 | hexEditText.setText(String.format("%06X", (0xFFFFFF & color))); 367 | } 368 | } 369 | 370 | private int parseColorString(String colorString) throws NumberFormatException { 371 | int a, r, g, b = 0; 372 | if (colorString.startsWith("#")) { 373 | colorString = colorString.substring(1); 374 | } 375 | if (colorString.length() == 0) { 376 | r = 0; 377 | a = 255; 378 | g = 0; 379 | } else if (colorString.length() <= 2) { 380 | a = 255; 381 | r = 0; 382 | b = Integer.parseInt(colorString, 16); 383 | g = 0; 384 | } else if (colorString.length() == 3) { 385 | a = 255; 386 | r = Integer.parseInt(colorString.substring(0, 1), 16); 387 | g = Integer.parseInt(colorString.substring(1, 2), 16); 388 | b = Integer.parseInt(colorString.substring(2, 3), 16); 389 | } else if (colorString.length() == 4) { 390 | a = 255; 391 | r = Integer.parseInt(colorString.substring(0, 2), 16); 392 | g = r; 393 | r = 0; 394 | b = Integer.parseInt(colorString.substring(2, 4), 16); 395 | } else if (colorString.length() == 5) { 396 | a = 255; 397 | r = Integer.parseInt(colorString.substring(0, 1), 16); 398 | g = Integer.parseInt(colorString.substring(1, 3), 16); 399 | b = Integer.parseInt(colorString.substring(3, 5), 16); 400 | } else if (colorString.length() == 6) { 401 | a = 255; 402 | r = Integer.parseInt(colorString.substring(0, 2), 16); 403 | g = Integer.parseInt(colorString.substring(2, 4), 16); 404 | b = Integer.parseInt(colorString.substring(4, 6), 16); 405 | } else if (colorString.length() == 7) { 406 | a = Integer.parseInt(colorString.substring(0, 1), 16); 407 | r = Integer.parseInt(colorString.substring(1, 3), 16); 408 | g = Integer.parseInt(colorString.substring(3, 5), 16); 409 | b = Integer.parseInt(colorString.substring(5, 7), 16); 410 | } else if (colorString.length() == 8) { 411 | a = Integer.parseInt(colorString.substring(0, 2), 16); 412 | r = Integer.parseInt(colorString.substring(2, 4), 16); 413 | g = Integer.parseInt(colorString.substring(4, 6), 16); 414 | b = Integer.parseInt(colorString.substring(6, 8), 16); 415 | } else { 416 | b = -1; 417 | g = -1; 418 | r = -1; 419 | a = -1; 420 | } 421 | return Color.argb(a, r, g, b); 422 | } 423 | 424 | // -- endregion -- 425 | 426 | // region Presets Picker 427 | 428 | View createPresetsView() { 429 | View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_presets, null); 430 | shadesLayout = (LinearLayout) contentView.findViewById(R.id.shades_layout); 431 | transparencySeekBar = (SeekBar) contentView.findViewById(R.id.transparency_seekbar); 432 | transparencyPercText = (TextView) contentView.findViewById(R.id.transparency_text); 433 | GridView gridView = (GridView) contentView.findViewById(R.id.gridView); 434 | 435 | loadPresets(); 436 | 437 | if (showColorShades) { 438 | createColorShades(color); 439 | } else { 440 | shadesLayout.setVisibility(View.GONE); 441 | contentView.findViewById(R.id.shades_divider).setVisibility(View.GONE); 442 | } 443 | 444 | adapter = new ColorPaletteAdapter(new ColorPaletteAdapter.OnColorSelectedListener() { 445 | @Override public void onColorSelected(int newColor) { 446 | if (color == newColor) { 447 | // Double tab selects the color 448 | ColorPickerDialog.this.onColorSelected(color); 449 | dismiss(); 450 | return; 451 | } 452 | color = newColor; 453 | if (showColorShades) { 454 | createColorShades(color); 455 | } 456 | } 457 | }, presets, getSelectedItemPosition(), colorShape); 458 | 459 | gridView.setAdapter(adapter); 460 | 461 | if (showAlphaSlider) { 462 | setupTransparency(); 463 | } else { 464 | contentView.findViewById(R.id.transparency_layout).setVisibility(View.GONE); 465 | contentView.findViewById(R.id.transparency_title).setVisibility(View.GONE); 466 | } 467 | 468 | return contentView; 469 | } 470 | 471 | private void loadPresets() { 472 | int alpha = Color.alpha(color); 473 | presets = getArguments().getIntArray(ARG_PRESETS); 474 | if (presets == null) presets = MATERIAL_COLORS; 475 | boolean isMaterialColors = presets == MATERIAL_COLORS; 476 | presets = Arrays.copyOf(presets, presets.length); // don't update the original array when modifying alpha 477 | if (alpha != 255) { 478 | // add alpha to the presets 479 | for (int i = 0; i < presets.length; i++) { 480 | int color = presets[i]; 481 | int red = Color.red(color); 482 | int green = Color.green(color); 483 | int blue = Color.blue(color); 484 | presets[i] = Color.argb(alpha, red, green, blue); 485 | } 486 | } 487 | presets = unshiftIfNotExists(presets, color); 488 | int initialColor = getArguments().getInt(ARG_COLOR); 489 | if (initialColor != color) { 490 | // The user clicked a color and a configuration change occurred. Make sure the initial color is in the presets 491 | presets = unshiftIfNotExists(presets, initialColor); 492 | } 493 | if (isMaterialColors && presets.length == 19) { 494 | // Add black to have a total of 20 colors if the current color is in the material color palette 495 | presets = pushIfNotExists(presets, Color.argb(alpha, 0, 0, 0)); 496 | } 497 | } 498 | 499 | void createColorShades(@ColorInt final int color) { 500 | final int[] colorShades = getColorShades(color); 501 | 502 | if (shadesLayout.getChildCount() != 0) { 503 | for (int i = 0; i < shadesLayout.getChildCount(); i++) { 504 | FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); 505 | final ColorPanelView cpv = (ColorPanelView) layout.findViewById(R.id.cpv_color_panel_view); 506 | ImageView iv = (ImageView) layout.findViewById(R.id.cpv_color_image_view); 507 | cpv.setColor(colorShades[i]); 508 | cpv.setTag(false); 509 | iv.setImageDrawable(null); 510 | } 511 | return; 512 | } 513 | 514 | final int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding); 515 | 516 | for (final int colorShade : colorShades) { 517 | int layoutResId; 518 | if (colorShape == ColorShape.SQUARE) { 519 | layoutResId = R.layout.cpv_color_item_square; 520 | } else { 521 | layoutResId = R.layout.cpv_color_item_circle; 522 | } 523 | 524 | final View view = View.inflate(getActivity(), layoutResId, null); 525 | final ColorPanelView colorPanelView = (ColorPanelView) view.findViewById(R.id.cpv_color_panel_view); 526 | 527 | ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) colorPanelView.getLayoutParams(); 528 | params.leftMargin = params.rightMargin = horizontalPadding; 529 | colorPanelView.setLayoutParams(params); 530 | colorPanelView.setColor(colorShade); 531 | shadesLayout.addView(view); 532 | 533 | colorPanelView.post(new Runnable() { 534 | @Override public void run() { 535 | // The color is black when rotating the dialog. This is a dirty fix. WTF!? 536 | colorPanelView.setColor(colorShade); 537 | } 538 | }); 539 | 540 | colorPanelView.setOnClickListener(new View.OnClickListener() { 541 | @Override public void onClick(View v) { 542 | if (v.getTag() instanceof Boolean && (Boolean) v.getTag()) { 543 | onColorSelected(ColorPickerDialog.this.color); 544 | dismiss(); 545 | return; // already selected 546 | } 547 | ColorPickerDialog.this.color = colorPanelView.getColor(); 548 | adapter.selectNone(); 549 | for (int i = 0; i < shadesLayout.getChildCount(); i++) { 550 | FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); 551 | ColorPanelView cpv = (ColorPanelView) layout.findViewById(R.id.cpv_color_panel_view); 552 | ImageView iv = (ImageView) layout.findViewById(R.id.cpv_color_image_view); 553 | iv.setImageResource(cpv == v ? R.drawable.cpv_preset_checked : 0); 554 | if (cpv == v && ColorUtils.calculateLuminance(cpv.getColor()) >= 0.65 555 | || Color.alpha(cpv.getColor()) <= ALPHA_THRESHOLD) { 556 | iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); 557 | } else { 558 | iv.setColorFilter(null); 559 | } 560 | cpv.setTag(cpv == v); 561 | } 562 | } 563 | }); 564 | colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { 565 | @Override public boolean onLongClick(View v) { 566 | colorPanelView.showHint(); 567 | return true; 568 | } 569 | }); 570 | } 571 | } 572 | 573 | private void onColorSelected(int color) { 574 | if (colorPickerDialogListener != null) { 575 | Log.w(TAG, "Using deprecated listener which may be remove in future releases"); 576 | colorPickerDialogListener.onColorSelected(dialogId, color); 577 | return; 578 | } 579 | Activity activity = getActivity(); 580 | if (activity instanceof ColorPickerDialogListener) { 581 | ((ColorPickerDialogListener) activity).onColorSelected(dialogId, color); 582 | } else { 583 | throw new IllegalStateException("The activity must implement ColorPickerDialogListener"); 584 | } 585 | } 586 | 587 | private void onDialogDismissed() { 588 | if (colorPickerDialogListener != null) { 589 | Log.w(TAG, "Using deprecated listener which may be remove in future releases"); 590 | colorPickerDialogListener.onDialogDismissed(dialogId); 591 | return; 592 | } 593 | Activity activity = getActivity(); 594 | if (activity instanceof ColorPickerDialogListener) { 595 | ((ColorPickerDialogListener) activity).onDialogDismissed(dialogId); 596 | } 597 | } 598 | 599 | private int shadeColor(@ColorInt int color, double percent) { 600 | String hex = String.format("#%06X", (0xFFFFFF & color)); 601 | long f = Long.parseLong(hex.substring(1), 16); 602 | double t = percent < 0 ? 0 : 255; 603 | double p = percent < 0 ? percent * -1 : percent; 604 | long R = f >> 16; 605 | long G = f >> 8 & 0x00FF; 606 | long B = f & 0x0000FF; 607 | int alpha = Color.alpha(color); 608 | int red = (int) (Math.round((t - R) * p) + R); 609 | int green = (int) (Math.round((t - G) * p) + G); 610 | int blue = (int) (Math.round((t - B) * p) + B); 611 | return Color.argb(alpha, red, green, blue); 612 | } 613 | 614 | private int[] getColorShades(@ColorInt int color) { 615 | return new int[] { 616 | shadeColor(color, 0.9), shadeColor(color, 0.7), shadeColor(color, 0.5), shadeColor(color, 0.333), 617 | shadeColor(color, 0.166), shadeColor(color, -0.125), shadeColor(color, -0.25), shadeColor(color, -0.375), 618 | shadeColor(color, -0.5), shadeColor(color, -0.675), shadeColor(color, -0.7), shadeColor(color, -0.775), 619 | }; 620 | } 621 | 622 | private void setupTransparency() { 623 | int progress = 255 - Color.alpha(color); 624 | transparencySeekBar.setMax(255); 625 | transparencySeekBar.setProgress(progress); 626 | int percentage = (int) ((double) progress * 100 / 255); 627 | transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); 628 | transparencySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { 629 | @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { 630 | int percentage = (int) ((double) progress * 100 / 255); 631 | transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); 632 | int alpha = 255 - progress; 633 | // update items in GridView: 634 | for (int i = 0; i < adapter.colors.length; i++) { 635 | int color = adapter.colors[i]; 636 | int red = Color.red(color); 637 | int green = Color.green(color); 638 | int blue = Color.blue(color); 639 | adapter.colors[i] = Color.argb(alpha, red, green, blue); 640 | } 641 | adapter.notifyDataSetChanged(); 642 | // update shades: 643 | for (int i = 0; i < shadesLayout.getChildCount(); i++) { 644 | FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); 645 | ColorPanelView cpv = (ColorPanelView) layout.findViewById(R.id.cpv_color_panel_view); 646 | ImageView iv = (ImageView) layout.findViewById(R.id.cpv_color_image_view); 647 | if (layout.getTag() == null) { 648 | // save the original border color 649 | layout.setTag(cpv.getBorderColor()); 650 | } 651 | int color = cpv.getColor(); 652 | color = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); 653 | if (alpha <= ALPHA_THRESHOLD) { 654 | cpv.setBorderColor(color | 0xFF000000); 655 | } else { 656 | cpv.setBorderColor((int) layout.getTag()); 657 | } 658 | if (cpv.getTag() != null && (Boolean) cpv.getTag()) { 659 | // The alpha changed on the selected shaded color. Update the checkmark color filter. 660 | if (alpha <= ALPHA_THRESHOLD) { 661 | iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); 662 | } else { 663 | if (ColorUtils.calculateLuminance(color) >= 0.65) { 664 | iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); 665 | } else { 666 | iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); 667 | } 668 | } 669 | } 670 | cpv.setColor(color); 671 | } 672 | // update color: 673 | int red = Color.red(color); 674 | int green = Color.green(color); 675 | int blue = Color.blue(color); 676 | color = Color.argb(alpha, red, green, blue); 677 | } 678 | 679 | @Override public void onStartTrackingTouch(SeekBar seekBar) { 680 | 681 | } 682 | 683 | @Override public void onStopTrackingTouch(SeekBar seekBar) { 684 | 685 | } 686 | }); 687 | } 688 | 689 | private int[] unshiftIfNotExists(int[] array, int value) { 690 | boolean present = false; 691 | for (int i : array) { 692 | if (i == value) { 693 | present = true; 694 | break; 695 | } 696 | } 697 | if (!present) { 698 | int[] newArray = new int[array.length + 1]; 699 | newArray[0] = value; 700 | System.arraycopy(array, 0, newArray, 1, newArray.length - 1); 701 | return newArray; 702 | } 703 | return array; 704 | } 705 | 706 | private int[] pushIfNotExists(int[] array, int value) { 707 | boolean present = false; 708 | for (int i : array) { 709 | if (i == value) { 710 | present = true; 711 | break; 712 | } 713 | } 714 | if (!present) { 715 | int[] newArray = new int[array.length + 1]; 716 | newArray[newArray.length - 1] = value; 717 | System.arraycopy(array, 0, newArray, 0, newArray.length - 1); 718 | return newArray; 719 | } 720 | return array; 721 | } 722 | 723 | private int getSelectedItemPosition() { 724 | for (int i = 0; i < presets.length; i++) { 725 | if (presets[i] == color) { 726 | return i; 727 | } 728 | } 729 | return -1; 730 | } 731 | 732 | // endregion 733 | 734 | // region Builder 735 | 736 | @IntDef({ TYPE_CUSTOM, TYPE_PRESETS }) public @interface DialogType { 737 | 738 | } 739 | 740 | public static final class Builder { 741 | 742 | ColorPickerDialogListener colorPickerDialogListener; 743 | @StringRes int dialogTitle = R.string.cpv_default_title; 744 | @StringRes int presetsButtonText = R.string.cpv_presets; 745 | @StringRes int customButtonText = R.string.cpv_custom; 746 | @StringRes int selectedButtonText = R.string.cpv_select; 747 | @DialogType int dialogType = TYPE_PRESETS; 748 | int[] presets = MATERIAL_COLORS; 749 | @ColorInt int color = Color.BLACK; 750 | int dialogId = 0; 751 | boolean showAlphaSlider = false; 752 | boolean allowPresets = true; 753 | boolean allowCustom = true; 754 | boolean showColorShades = true; 755 | @ColorShape int colorShape = ColorShape.CIRCLE; 756 | 757 | /*package*/ Builder() { 758 | 759 | } 760 | 761 | /** 762 | * Set the dialog title string resource id 763 | * 764 | * @param dialogTitle The string resource used for the dialog title 765 | * @return This builder object for chaining method calls 766 | */ 767 | public Builder setDialogTitle(@StringRes int dialogTitle) { 768 | this.dialogTitle = dialogTitle; 769 | return this; 770 | } 771 | 772 | /** 773 | * Set the selected button text string resource id 774 | * 775 | * @param selectedButtonText The string resource used for the selected button text 776 | * @return This builder object for chaining method calls 777 | */ 778 | public Builder setSelectedButtonText(@StringRes int selectedButtonText) { 779 | this.selectedButtonText = selectedButtonText; 780 | return this; 781 | } 782 | 783 | /** 784 | * Set the presets button text string resource id 785 | * 786 | * @param presetsButtonText The string resource used for the presets button text 787 | * @return This builder object for chaining method calls 788 | */ 789 | public Builder setPresetsButtonText(@StringRes int presetsButtonText) { 790 | this.presetsButtonText = presetsButtonText; 791 | return this; 792 | } 793 | 794 | /** 795 | * Set the custom button text string resource id 796 | * 797 | * @param customButtonText The string resource used for the custom button text 798 | * @return This builder object for chaining method calls 799 | */ 800 | public Builder setCustomButtonText(@StringRes int customButtonText) { 801 | this.customButtonText = customButtonText; 802 | return this; 803 | } 804 | 805 | /** 806 | * Set which dialog view to show. 807 | * 808 | * @param dialogType Either {@link ColorPickerDialog#TYPE_CUSTOM} or {@link ColorPickerDialog#TYPE_PRESETS}. 809 | * @return This builder object for chaining method calls 810 | */ 811 | public Builder setDialogType(@DialogType int dialogType) { 812 | this.dialogType = dialogType; 813 | return this; 814 | } 815 | 816 | /** 817 | * Set the colors used for the presets 818 | * 819 | * @param presets An array of color ints. 820 | * @return This builder object for chaining method calls 821 | */ 822 | public Builder setPresets(@NonNull int[] presets) { 823 | this.presets = presets; 824 | return this; 825 | } 826 | 827 | /** 828 | * Set the original color 829 | * 830 | * @param color The default color for the color picker 831 | * @return This builder object for chaining method calls 832 | */ 833 | public Builder setColor(int color) { 834 | this.color = color; 835 | return this; 836 | } 837 | 838 | /** 839 | * Set the dialog id used for callbacks 840 | * 841 | * @param dialogId The id that is sent back to the {@link ColorPickerDialogListener}. 842 | * @return This builder object for chaining method calls 843 | */ 844 | public Builder setDialogId(int dialogId) { 845 | this.dialogId = dialogId; 846 | return this; 847 | } 848 | 849 | /** 850 | * Show the alpha slider 851 | * 852 | * @param showAlphaSlider {@code true} to show the alpha slider. Currently only supported with the {@link 853 | * ColorPickerView}. 854 | * @return This builder object for chaining method calls 855 | */ 856 | public Builder setShowAlphaSlider(boolean showAlphaSlider) { 857 | this.showAlphaSlider = showAlphaSlider; 858 | return this; 859 | } 860 | 861 | /** 862 | * Show/Hide a neutral button to select preset colors. 863 | * 864 | * @param allowPresets {@code false} to disable showing the presets button. 865 | * @return This builder object for chaining method calls 866 | */ 867 | public Builder setAllowPresets(boolean allowPresets) { 868 | this.allowPresets = allowPresets; 869 | return this; 870 | } 871 | 872 | /** 873 | * Show/Hide the neutral button to select a custom color. 874 | * 875 | * @param allowCustom {@code false} to disable showing the custom button. 876 | * @return This builder object for chaining method calls 877 | */ 878 | public Builder setAllowCustom(boolean allowCustom) { 879 | this.allowCustom = allowCustom; 880 | return this; 881 | } 882 | 883 | /** 884 | * Show/Hide the color shades in the presets picker 885 | * 886 | * @param showColorShades {@code false} to hide the color shades. 887 | * @return This builder object for chaining method calls 888 | */ 889 | public Builder setShowColorShades(boolean showColorShades) { 890 | this.showColorShades = showColorShades; 891 | return this; 892 | } 893 | 894 | /** 895 | * Set the shape of the color panel view. 896 | * 897 | * @param colorShape Either {@link ColorShape#CIRCLE} or {@link ColorShape#SQUARE}. 898 | * @return This builder object for chaining method calls 899 | */ 900 | public Builder setColorShape(int colorShape) { 901 | this.colorShape = colorShape; 902 | return this; 903 | } 904 | 905 | /** 906 | * Create the {@link ColorPickerDialog} instance. 907 | * 908 | * @return A new {@link ColorPickerDialog}. 909 | * @see #show(FragmentActivity) 910 | */ 911 | public ColorPickerDialog create() { 912 | ColorPickerDialog dialog = new ColorPickerDialog(); 913 | Bundle args = new Bundle(); 914 | args.putInt(ARG_ID, dialogId); 915 | args.putInt(ARG_TYPE, dialogType); 916 | args.putInt(ARG_COLOR, color); 917 | args.putIntArray(ARG_PRESETS, presets); 918 | args.putBoolean(ARG_ALPHA, showAlphaSlider); 919 | args.putBoolean(ARG_ALLOW_CUSTOM, allowCustom); 920 | args.putBoolean(ARG_ALLOW_PRESETS, allowPresets); 921 | args.putInt(ARG_DIALOG_TITLE, dialogTitle); 922 | args.putBoolean(ARG_SHOW_COLOR_SHADES, showColorShades); 923 | args.putInt(ARG_COLOR_SHAPE, colorShape); 924 | args.putInt(ARG_PRESETS_BUTTON_TEXT, presetsButtonText); 925 | args.putInt(ARG_CUSTOM_BUTTON_TEXT, customButtonText); 926 | args.putInt(ARG_SELECTED_BUTTON_TEXT, selectedButtonText); 927 | dialog.setArguments(args); 928 | return dialog; 929 | } 930 | 931 | /** 932 | * Create and show the {@link ColorPickerDialog} created with this builder. 933 | * 934 | * @param activity The current activity. 935 | */ 936 | public void show(FragmentActivity activity) { 937 | create().show(activity.getSupportFragmentManager(), "color-picker-dialog"); 938 | } 939 | } 940 | 941 | // endregion 942 | } 943 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialogListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import androidx.annotation.ColorInt; 20 | 21 | /** 22 | * Callback used for getting the selected color from a color picker dialog. 23 | */ 24 | public interface ColorPickerDialogListener { 25 | 26 | /** 27 | * Callback that is invoked when a color is selected from the color picker dialog. 28 | * 29 | * @param dialogId The dialog id used to create the dialog instance. 30 | * @param color The selected color 31 | */ 32 | void onColorSelected(int dialogId, @ColorInt int color); 33 | 34 | /** 35 | * Callback that is invoked when the color picker dialog was dismissed. 36 | * 37 | * @param dialogId The dialog id used to create the dialog instance. 38 | */ 39 | void onDialogDismissed(int dialogId); 40 | } 41 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Bitmap; 22 | import android.graphics.Bitmap.Config; 23 | import android.graphics.Canvas; 24 | import android.graphics.Color; 25 | import android.graphics.ComposeShader; 26 | import android.graphics.LinearGradient; 27 | import android.graphics.Paint; 28 | import android.graphics.Paint.Align; 29 | import android.graphics.Paint.Style; 30 | import android.graphics.Point; 31 | import android.graphics.PorterDuff; 32 | import android.graphics.Rect; 33 | import android.graphics.RectF; 34 | import android.graphics.Shader; 35 | import android.graphics.Shader.TileMode; 36 | import android.os.Bundle; 37 | import android.os.Parcelable; 38 | import android.util.AttributeSet; 39 | import android.util.TypedValue; 40 | import android.view.MotionEvent; 41 | import android.view.View; 42 | 43 | /** 44 | * Displays a color picker to the user and allow them to select a color. A slider for the alpha channel is also 45 | * available. 46 | * Enable it by setting setAlphaSliderVisible(boolean) to true. 47 | */ 48 | public class ColorPickerView extends View { 49 | 50 | private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E; 51 | private final static int DEFAULT_SLIDER_COLOR = 0xFFBDBDBD; 52 | 53 | private final static int HUE_PANEL_WDITH_DP = 30; 54 | private final static int ALPHA_PANEL_HEIGH_DP = 20; 55 | private final static int PANEL_SPACING_DP = 10; 56 | private final static int CIRCLE_TRACKER_RADIUS_DP = 5; 57 | private final static int SLIDER_TRACKER_SIZE_DP = 4; 58 | private final static int SLIDER_TRACKER_OFFSET_DP = 2; 59 | 60 | /** 61 | * The width in pixels of the border 62 | * surrounding all color panels. 63 | */ 64 | private final static int BORDER_WIDTH_PX = 1; 65 | 66 | /** 67 | * The width in px of the hue panel. 68 | */ 69 | private int huePanelWidthPx; 70 | /** 71 | * The height in px of the alpha panel 72 | */ 73 | private int alphaPanelHeightPx; 74 | /** 75 | * The distance in px between the different 76 | * color panels. 77 | */ 78 | private int panelSpacingPx; 79 | /** 80 | * The radius in px of the color palette tracker circle. 81 | */ 82 | private int circleTrackerRadiusPx; 83 | /** 84 | * The px which the tracker of the hue or alpha panel 85 | * will extend outside of its bounds. 86 | */ 87 | private int sliderTrackerOffsetPx; 88 | /** 89 | * Height of slider tracker on hue panel, 90 | * width of slider on alpha panel. 91 | */ 92 | private int sliderTrackerSizePx; 93 | 94 | private Paint satValPaint; 95 | private Paint satValTrackerPaint; 96 | 97 | private Paint alphaPaint; 98 | private Paint alphaTextPaint; 99 | private Paint hueAlphaTrackerPaint; 100 | 101 | private Paint borderPaint; 102 | 103 | private Shader valShader; 104 | private Shader satShader; 105 | private Shader alphaShader; 106 | 107 | /* 108 | * We cache a bitmap of the sat/val panel which is expensive to draw each time. 109 | * We can reuse it when the user is sliding the circle picker as long as the hue isn't changed. 110 | */ 111 | private BitmapCache satValBackgroundCache; 112 | /* We cache the hue background to since its also very expensive now. */ 113 | private BitmapCache hueBackgroundCache; 114 | 115 | /* Current values */ 116 | private int alpha = 0xff; 117 | private float hue = 360f; 118 | private float sat = 0f; 119 | private float val = 0f; 120 | 121 | private boolean showAlphaPanel = false; 122 | private String alphaSliderText = null; 123 | private int sliderTrackerColor = DEFAULT_SLIDER_COLOR; 124 | private int borderColor = DEFAULT_BORDER_COLOR; 125 | 126 | /** 127 | * Minimum required padding. The offset from the 128 | * edge we must have or else the finger tracker will 129 | * get clipped when it's drawn outside of the view. 130 | */ 131 | private int mRequiredPadding; 132 | 133 | /** 134 | * The Rect in which we are allowed to draw. 135 | * Trackers can extend outside slightly, 136 | * due to the required padding we have set. 137 | */ 138 | private Rect drawingRect; 139 | 140 | private Rect satValRect; 141 | private Rect hueRect; 142 | private Rect alphaRect; 143 | 144 | private Point startTouchPoint = null; 145 | 146 | private AlphaPatternDrawable alphaPatternDrawable; 147 | private OnColorChangedListener onColorChangedListener; 148 | 149 | public ColorPickerView(Context context) { 150 | this(context, null); 151 | } 152 | 153 | public ColorPickerView(Context context, AttributeSet attrs) { 154 | this(context, attrs, 0); 155 | } 156 | 157 | public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { 158 | super(context, attrs, defStyle); 159 | init(context, attrs); 160 | } 161 | 162 | @Override public Parcelable onSaveInstanceState() { 163 | Bundle state = new Bundle(); 164 | state.putParcelable("instanceState", super.onSaveInstanceState()); 165 | state.putInt("alpha", alpha); 166 | state.putFloat("hue", hue); 167 | state.putFloat("sat", sat); 168 | state.putFloat("val", val); 169 | state.putBoolean("show_alpha", showAlphaPanel); 170 | state.putString("alpha_text", alphaSliderText); 171 | 172 | return state; 173 | } 174 | 175 | @Override public void onRestoreInstanceState(Parcelable state) { 176 | 177 | if (state instanceof Bundle) { 178 | Bundle bundle = (Bundle) state; 179 | 180 | alpha = bundle.getInt("alpha"); 181 | hue = bundle.getFloat("hue"); 182 | sat = bundle.getFloat("sat"); 183 | val = bundle.getFloat("val"); 184 | showAlphaPanel = bundle.getBoolean("show_alpha"); 185 | alphaSliderText = bundle.getString("alpha_text"); 186 | 187 | state = bundle.getParcelable("instanceState"); 188 | } 189 | super.onRestoreInstanceState(state); 190 | } 191 | 192 | private void init(Context context, AttributeSet attrs) { 193 | //Load those if set in xml resource file. 194 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPickerView); 195 | showAlphaPanel = a.getBoolean(R.styleable.ColorPickerView_cpv_alphaChannelVisible, false); 196 | alphaSliderText = a.getString(R.styleable.ColorPickerView_cpv_alphaChannelText); 197 | sliderTrackerColor = a.getColor(R.styleable.ColorPickerView_cpv_sliderColor, 0xFFBDBDBD); 198 | borderColor = a.getColor(R.styleable.ColorPickerView_cpv_borderColor, 0xFF6E6E6E); 199 | a.recycle(); 200 | 201 | applyThemeColors(context); 202 | 203 | huePanelWidthPx = DrawingUtils.dpToPx(getContext(), HUE_PANEL_WDITH_DP); 204 | alphaPanelHeightPx = DrawingUtils.dpToPx(getContext(), ALPHA_PANEL_HEIGH_DP); 205 | panelSpacingPx = DrawingUtils.dpToPx(getContext(), PANEL_SPACING_DP); 206 | circleTrackerRadiusPx = DrawingUtils.dpToPx(getContext(), CIRCLE_TRACKER_RADIUS_DP); 207 | sliderTrackerSizePx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_SIZE_DP); 208 | sliderTrackerOffsetPx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_OFFSET_DP); 209 | 210 | mRequiredPadding = getResources().getDimensionPixelSize(R.dimen.cpv_required_padding); 211 | 212 | initPaintTools(); 213 | 214 | //Needed for receiving trackball motion events. 215 | setFocusable(true); 216 | setFocusableInTouchMode(true); 217 | } 218 | 219 | private void applyThemeColors(Context c) { 220 | // If no specific border/slider color has been 221 | // set we take the default secondary text color 222 | // as border/slider color. Thus it will adopt 223 | // to theme changes automatically. 224 | 225 | final TypedValue value = new TypedValue(); 226 | TypedArray a = c.obtainStyledAttributes(value.data, new int[] { android.R.attr.textColorSecondary }); 227 | 228 | if (borderColor == DEFAULT_BORDER_COLOR) { 229 | borderColor = a.getColor(0, DEFAULT_BORDER_COLOR); 230 | } 231 | 232 | if (sliderTrackerColor == DEFAULT_SLIDER_COLOR) { 233 | sliderTrackerColor = a.getColor(0, DEFAULT_SLIDER_COLOR); 234 | } 235 | 236 | a.recycle(); 237 | } 238 | 239 | private void initPaintTools() { 240 | 241 | satValPaint = new Paint(); 242 | satValTrackerPaint = new Paint(); 243 | hueAlphaTrackerPaint = new Paint(); 244 | alphaPaint = new Paint(); 245 | alphaTextPaint = new Paint(); 246 | borderPaint = new Paint(); 247 | 248 | satValTrackerPaint.setStyle(Style.STROKE); 249 | satValTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2)); 250 | satValTrackerPaint.setAntiAlias(true); 251 | 252 | hueAlphaTrackerPaint.setColor(sliderTrackerColor); 253 | hueAlphaTrackerPaint.setStyle(Style.STROKE); 254 | hueAlphaTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2)); 255 | hueAlphaTrackerPaint.setAntiAlias(true); 256 | 257 | alphaTextPaint.setColor(0xff1c1c1c); 258 | alphaTextPaint.setTextSize(DrawingUtils.dpToPx(getContext(), 14)); 259 | alphaTextPaint.setAntiAlias(true); 260 | alphaTextPaint.setTextAlign(Align.CENTER); 261 | alphaTextPaint.setFakeBoldText(true); 262 | } 263 | 264 | @Override protected void onDraw(Canvas canvas) { 265 | if (drawingRect.width() <= 0 || drawingRect.height() <= 0) { 266 | return; 267 | } 268 | 269 | drawSatValPanel(canvas); 270 | drawHuePanel(canvas); 271 | drawAlphaPanel(canvas); 272 | } 273 | 274 | private void drawSatValPanel(Canvas canvas) { 275 | final Rect rect = satValRect; 276 | 277 | if (BORDER_WIDTH_PX > 0) { 278 | borderPaint.setColor(borderColor); 279 | canvas.drawRect(drawingRect.left, drawingRect.top, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, 280 | borderPaint); 281 | } 282 | 283 | if (valShader == null) { 284 | //Black gradient has either not been created or the view has been resized. 285 | valShader = 286 | new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, 0xffffffff, 0xff000000, TileMode.CLAMP); 287 | } 288 | 289 | //If the hue has changed we need to recreate the cache. 290 | if (satValBackgroundCache == null || satValBackgroundCache.value != hue) { 291 | 292 | if (satValBackgroundCache == null) { 293 | satValBackgroundCache = new BitmapCache(); 294 | } 295 | 296 | //We create our bitmap in the cache if it doesn't exist. 297 | if (satValBackgroundCache.bitmap == null) { 298 | satValBackgroundCache.bitmap = Bitmap.createBitmap(rect.width(), rect.height(), Config.ARGB_8888); 299 | } 300 | 301 | //We create the canvas once so we can draw on our bitmap and the hold on to it. 302 | if (satValBackgroundCache.canvas == null) { 303 | satValBackgroundCache.canvas = new Canvas(satValBackgroundCache.bitmap); 304 | } 305 | 306 | int rgb = Color.HSVToColor(new float[] { hue, 1f, 1f }); 307 | 308 | satShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, TileMode.CLAMP); 309 | 310 | ComposeShader mShader = new ComposeShader(valShader, satShader, PorterDuff.Mode.MULTIPLY); 311 | satValPaint.setShader(mShader); 312 | 313 | // Finally we draw on our canvas, the result will be 314 | // stored in our bitmap which is already in the cache. 315 | // Since this is drawn on a canvas not rendered on 316 | // screen it will automatically not be using the 317 | // hardware acceleration. And this was the code that 318 | // wasn't supported by hardware acceleration which mean 319 | // there is no need to turn it of anymore. The rest of 320 | // the view will still be hw accelerated. 321 | satValBackgroundCache.canvas.drawRect(0, 0, satValBackgroundCache.bitmap.getWidth(), 322 | satValBackgroundCache.bitmap.getHeight(), satValPaint); 323 | 324 | //We set the hue value in our cache to which hue it was drawn with, 325 | //then we know that if it hasn't changed we can reuse our cached bitmap. 326 | satValBackgroundCache.value = hue; 327 | } 328 | 329 | // We draw our bitmap from the cached, if the hue has changed 330 | // then it was just recreated otherwise the old one will be used. 331 | canvas.drawBitmap(satValBackgroundCache.bitmap, null, rect, null); 332 | 333 | Point p = satValToPoint(sat, val); 334 | 335 | satValTrackerPaint.setColor(0xff000000); 336 | canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx - DrawingUtils.dpToPx(getContext(), 1), satValTrackerPaint); 337 | 338 | satValTrackerPaint.setColor(0xffdddddd); 339 | canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx, satValTrackerPaint); 340 | } 341 | 342 | private void drawHuePanel(Canvas canvas) { 343 | final Rect rect = hueRect; 344 | 345 | if (BORDER_WIDTH_PX > 0) { 346 | borderPaint.setColor(borderColor); 347 | 348 | canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, 349 | rect.bottom + BORDER_WIDTH_PX, borderPaint); 350 | } 351 | 352 | if (hueBackgroundCache == null) { 353 | hueBackgroundCache = new BitmapCache(); 354 | hueBackgroundCache.bitmap = Bitmap.createBitmap(rect.width(), rect.height(), Config.ARGB_8888); 355 | hueBackgroundCache.canvas = new Canvas(hueBackgroundCache.bitmap); 356 | 357 | int[] hueColors = new int[(int) (rect.height() + 0.5f)]; 358 | 359 | // Generate array of all colors, will be drawn as individual lines. 360 | float h = 360f; 361 | for (int i = 0; i < hueColors.length; i++) { 362 | hueColors[i] = Color.HSVToColor(new float[] { h, 1f, 1f }); 363 | h -= 360f / hueColors.length; 364 | } 365 | 366 | // Time to draw the hue color gradient, 367 | // its drawn as individual lines which 368 | // will be quite many when the resolution is high 369 | // and/or the panel is large. 370 | Paint linePaint = new Paint(); 371 | linePaint.setStrokeWidth(0); 372 | for (int i = 0; i < hueColors.length; i++) { 373 | linePaint.setColor(hueColors[i]); 374 | hueBackgroundCache.canvas.drawLine(0, i, hueBackgroundCache.bitmap.getWidth(), i, linePaint); 375 | } 376 | } 377 | 378 | canvas.drawBitmap(hueBackgroundCache.bitmap, null, rect, null); 379 | 380 | Point p = hueToPoint(hue); 381 | 382 | RectF r = new RectF(); 383 | r.left = rect.left - sliderTrackerOffsetPx; 384 | r.right = rect.right + sliderTrackerOffsetPx; 385 | r.top = p.y - (sliderTrackerSizePx / 2); 386 | r.bottom = p.y + (sliderTrackerSizePx / 2); 387 | 388 | canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint); 389 | } 390 | 391 | private void drawAlphaPanel(Canvas canvas) { 392 | /* 393 | * Will be drawn with hw acceleration, very fast. 394 | * Also the AlphaPatternDrawable is backed by a bitmap 395 | * generated only once if the size does not change. 396 | */ 397 | 398 | if (!showAlphaPanel || alphaRect == null || alphaPatternDrawable == null) return; 399 | 400 | final Rect rect = alphaRect; 401 | 402 | if (BORDER_WIDTH_PX > 0) { 403 | borderPaint.setColor(borderColor); 404 | canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, 405 | rect.bottom + BORDER_WIDTH_PX, borderPaint); 406 | } 407 | 408 | alphaPatternDrawable.draw(canvas); 409 | 410 | float[] hsv = new float[] { hue, sat, val }; 411 | int color = Color.HSVToColor(hsv); 412 | int acolor = Color.HSVToColor(0, hsv); 413 | 414 | alphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor, TileMode.CLAMP); 415 | 416 | alphaPaint.setShader(alphaShader); 417 | 418 | canvas.drawRect(rect, alphaPaint); 419 | 420 | if (alphaSliderText != null && !alphaSliderText.equals("")) { 421 | canvas.drawText(alphaSliderText, rect.centerX(), rect.centerY() + DrawingUtils.dpToPx(getContext(), 4), 422 | alphaTextPaint); 423 | } 424 | 425 | Point p = alphaToPoint(alpha); 426 | 427 | RectF r = new RectF(); 428 | r.left = p.x - (sliderTrackerSizePx / 2); 429 | r.right = p.x + (sliderTrackerSizePx / 2); 430 | r.top = rect.top - sliderTrackerOffsetPx; 431 | r.bottom = rect.bottom + sliderTrackerOffsetPx; 432 | 433 | canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint); 434 | } 435 | 436 | private Point hueToPoint(float hue) { 437 | 438 | final Rect rect = hueRect; 439 | final float height = rect.height(); 440 | 441 | Point p = new Point(); 442 | 443 | p.y = (int) (height - (hue * height / 360f) + rect.top); 444 | p.x = rect.left; 445 | 446 | return p; 447 | } 448 | 449 | private Point satValToPoint(float sat, float val) { 450 | 451 | final Rect rect = satValRect; 452 | final float height = rect.height(); 453 | final float width = rect.width(); 454 | 455 | Point p = new Point(); 456 | 457 | p.x = (int) (sat * width + rect.left); 458 | p.y = (int) ((1f - val) * height + rect.top); 459 | 460 | return p; 461 | } 462 | 463 | private Point alphaToPoint(int alpha) { 464 | 465 | final Rect rect = alphaRect; 466 | final float width = rect.width(); 467 | 468 | Point p = new Point(); 469 | 470 | p.x = (int) (width - (alpha * width / 0xff) + rect.left); 471 | p.y = rect.top; 472 | 473 | return p; 474 | } 475 | 476 | private float[] pointToSatVal(float x, float y) { 477 | 478 | final Rect rect = satValRect; 479 | float[] result = new float[2]; 480 | 481 | float width = rect.width(); 482 | float height = rect.height(); 483 | 484 | if (x < rect.left) { 485 | x = 0f; 486 | } else if (x > rect.right) { 487 | x = width; 488 | } else { 489 | x = x - rect.left; 490 | } 491 | 492 | if (y < rect.top) { 493 | y = 0f; 494 | } else if (y > rect.bottom) { 495 | y = height; 496 | } else { 497 | y = y - rect.top; 498 | } 499 | 500 | result[0] = 1.f / width * x; 501 | result[1] = 1.f - (1.f / height * y); 502 | 503 | return result; 504 | } 505 | 506 | private float pointToHue(float y) { 507 | 508 | final Rect rect = hueRect; 509 | 510 | float height = rect.height(); 511 | 512 | if (y < rect.top) { 513 | y = 0f; 514 | } else if (y > rect.bottom) { 515 | y = height; 516 | } else { 517 | y = y - rect.top; 518 | } 519 | 520 | float hue = 360f - (y * 360f / height); 521 | 522 | return hue; 523 | } 524 | 525 | private int pointToAlpha(int x) { 526 | 527 | final Rect rect = alphaRect; 528 | final int width = rect.width(); 529 | 530 | if (x < rect.left) { 531 | x = 0; 532 | } else if (x > rect.right) { 533 | x = width; 534 | } else { 535 | x = x - rect.left; 536 | } 537 | 538 | return 0xff - (x * 0xff / width); 539 | } 540 | 541 | @Override public boolean onTouchEvent(MotionEvent event) { 542 | boolean update = false; 543 | 544 | switch (event.getAction()) { 545 | 546 | case MotionEvent.ACTION_DOWN: 547 | startTouchPoint = new Point((int) event.getX(), (int) event.getY()); 548 | update = moveTrackersIfNeeded(event); 549 | break; 550 | case MotionEvent.ACTION_MOVE: 551 | update = moveTrackersIfNeeded(event); 552 | break; 553 | case MotionEvent.ACTION_UP: 554 | startTouchPoint = null; 555 | update = moveTrackersIfNeeded(event); 556 | break; 557 | } 558 | 559 | if (update) { 560 | if (onColorChangedListener != null) { 561 | onColorChangedListener.onColorChanged(Color.HSVToColor(alpha, new float[] { hue, sat, val })); 562 | } 563 | invalidate(); 564 | return true; 565 | } 566 | 567 | return super.onTouchEvent(event); 568 | } 569 | 570 | private boolean moveTrackersIfNeeded(MotionEvent event) { 571 | if (startTouchPoint == null) { 572 | return false; 573 | } 574 | 575 | boolean update = false; 576 | 577 | int startX = startTouchPoint.x; 578 | int startY = startTouchPoint.y; 579 | 580 | if (hueRect.contains(startX, startY)) { 581 | hue = pointToHue(event.getY()); 582 | 583 | update = true; 584 | } else if (satValRect.contains(startX, startY)) { 585 | float[] result = pointToSatVal(event.getX(), event.getY()); 586 | 587 | sat = result[0]; 588 | val = result[1]; 589 | 590 | update = true; 591 | } else if (alphaRect != null && alphaRect.contains(startX, startY)) { 592 | alpha = pointToAlpha((int) event.getX()); 593 | 594 | update = true; 595 | } 596 | 597 | return update; 598 | } 599 | 600 | @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 601 | int finalWidth; 602 | int finalHeight; 603 | 604 | int widthMode = MeasureSpec.getMode(widthMeasureSpec); 605 | int heightMode = MeasureSpec.getMode(heightMeasureSpec); 606 | 607 | int widthAllowed = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); 608 | int heightAllowed = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop(); 609 | 610 | if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) { 611 | //A exact value has been set in either direction, we need to stay within this size. 612 | 613 | if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) { 614 | //The with has been specified exactly, we need to adopt the height to fit. 615 | int h = (widthAllowed - panelSpacingPx - huePanelWidthPx); 616 | 617 | if (showAlphaPanel) { 618 | h += panelSpacingPx + alphaPanelHeightPx; 619 | } 620 | 621 | if (h > heightAllowed) { 622 | //We can't fit the view in this container, set the size to whatever was allowed. 623 | finalHeight = heightAllowed; 624 | } else { 625 | finalHeight = h; 626 | } 627 | 628 | finalWidth = widthAllowed; 629 | } else if (heightMode == MeasureSpec.EXACTLY && widthMode != MeasureSpec.EXACTLY) { 630 | //The height has been specified exactly, we need to stay within this height and adopt the width. 631 | 632 | int w = (heightAllowed + panelSpacingPx + huePanelWidthPx); 633 | 634 | if (showAlphaPanel) { 635 | w -= (panelSpacingPx + alphaPanelHeightPx); 636 | } 637 | 638 | if (w > widthAllowed) { 639 | //we can't fit within this container, set the size to whatever was allowed. 640 | finalWidth = widthAllowed; 641 | } else { 642 | finalWidth = w; 643 | } 644 | 645 | finalHeight = heightAllowed; 646 | } else { 647 | //If we get here the dev has set the width and height to exact sizes. For example match_parent or 300dp. 648 | //This will mean that the sat/val panel will not be square but it doesn't matter. It will work anyway. 649 | //In all other senarios our goal is to make that panel square. 650 | 651 | //We set the sizes to exactly what we were told. 652 | finalWidth = widthAllowed; 653 | finalHeight = heightAllowed; 654 | } 655 | } else { 656 | //If no exact size has been set we try to make our view as big as possible 657 | //within the allowed space. 658 | 659 | //Calculate the needed width to layout using max allowed height. 660 | int widthNeeded = (heightAllowed + panelSpacingPx + huePanelWidthPx); 661 | 662 | //Calculate the needed height to layout using max allowed width. 663 | int heightNeeded = (widthAllowed - panelSpacingPx - huePanelWidthPx); 664 | 665 | if (showAlphaPanel) { 666 | widthNeeded -= (panelSpacingPx + alphaPanelHeightPx); 667 | heightNeeded += panelSpacingPx + alphaPanelHeightPx; 668 | } 669 | 670 | boolean widthOk = false; 671 | boolean heightOk = false; 672 | 673 | if (widthNeeded <= widthAllowed) { 674 | widthOk = true; 675 | } 676 | 677 | if (heightNeeded <= heightAllowed) { 678 | heightOk = true; 679 | } 680 | 681 | if (widthOk && heightOk) { 682 | finalWidth = widthAllowed; 683 | finalHeight = heightNeeded; 684 | } else if (!heightOk && widthOk) { 685 | finalHeight = heightAllowed; 686 | finalWidth = widthNeeded; 687 | } else if (!widthOk && heightOk) { 688 | finalHeight = heightNeeded; 689 | finalWidth = widthAllowed; 690 | } else { 691 | finalHeight = heightAllowed; 692 | finalWidth = widthAllowed; 693 | } 694 | } 695 | 696 | setMeasuredDimension(finalWidth + getPaddingLeft() + getPaddingRight(), 697 | finalHeight + getPaddingTop() + getPaddingBottom()); 698 | } 699 | 700 | private int getPreferredWidth() { 701 | //Our preferred width and height is 200dp for the square sat / val rectangle. 702 | int width = DrawingUtils.dpToPx(getContext(), 200); 703 | 704 | return (width + huePanelWidthPx + panelSpacingPx); 705 | } 706 | 707 | private int getPreferredHeight() { 708 | int height = DrawingUtils.dpToPx(getContext(), 200); 709 | 710 | if (showAlphaPanel) { 711 | height += panelSpacingPx + alphaPanelHeightPx; 712 | } 713 | return height; 714 | } 715 | 716 | @Override public int getPaddingTop() { 717 | return Math.max(super.getPaddingTop(), mRequiredPadding); 718 | } 719 | 720 | @Override public int getPaddingBottom() { 721 | return Math.max(super.getPaddingBottom(), mRequiredPadding); 722 | } 723 | 724 | @Override public int getPaddingLeft() { 725 | return Math.max(super.getPaddingLeft(), mRequiredPadding); 726 | } 727 | 728 | @Override public int getPaddingRight() { 729 | return Math.max(super.getPaddingRight(), mRequiredPadding); 730 | } 731 | 732 | @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { 733 | super.onSizeChanged(w, h, oldw, oldh); 734 | 735 | drawingRect = new Rect(); 736 | drawingRect.left = getPaddingLeft(); 737 | drawingRect.right = w - getPaddingRight(); 738 | drawingRect.top = getPaddingTop(); 739 | drawingRect.bottom = h - getPaddingBottom(); 740 | 741 | //The need to be recreated because they depend on the size of the view. 742 | valShader = null; 743 | satShader = null; 744 | alphaShader = null; 745 | 746 | // Clear those bitmap caches since the size may have changed. 747 | satValBackgroundCache = null; 748 | hueBackgroundCache = null; 749 | 750 | setUpSatValRect(); 751 | setUpHueRect(); 752 | setUpAlphaRect(); 753 | } 754 | 755 | private void setUpSatValRect() { 756 | //Calculate the size for the big color rectangle. 757 | final Rect dRect = drawingRect; 758 | 759 | int left = dRect.left + BORDER_WIDTH_PX; 760 | int top = dRect.top + BORDER_WIDTH_PX; 761 | int bottom = dRect.bottom - BORDER_WIDTH_PX; 762 | int right = dRect.right - BORDER_WIDTH_PX - panelSpacingPx - huePanelWidthPx; 763 | 764 | if (showAlphaPanel) { 765 | bottom -= (alphaPanelHeightPx + panelSpacingPx); 766 | } 767 | 768 | satValRect = new Rect(left, top, right, bottom); 769 | } 770 | 771 | private void setUpHueRect() { 772 | //Calculate the size for the hue slider on the left. 773 | final Rect dRect = drawingRect; 774 | 775 | int left = dRect.right - huePanelWidthPx + BORDER_WIDTH_PX; 776 | int top = dRect.top + BORDER_WIDTH_PX; 777 | int bottom = dRect.bottom - BORDER_WIDTH_PX - (showAlphaPanel ? (panelSpacingPx + alphaPanelHeightPx) : 0); 778 | int right = dRect.right - BORDER_WIDTH_PX; 779 | 780 | hueRect = new Rect(left, top, right, bottom); 781 | } 782 | 783 | private void setUpAlphaRect() { 784 | 785 | if (!showAlphaPanel) return; 786 | 787 | final Rect dRect = drawingRect; 788 | 789 | int left = dRect.left + BORDER_WIDTH_PX; 790 | int top = dRect.bottom - alphaPanelHeightPx + BORDER_WIDTH_PX; 791 | int bottom = dRect.bottom - BORDER_WIDTH_PX; 792 | int right = dRect.right - BORDER_WIDTH_PX; 793 | 794 | alphaRect = new Rect(left, top, right, bottom); 795 | 796 | alphaPatternDrawable = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4)); 797 | alphaPatternDrawable.setBounds(Math.round(alphaRect.left), Math.round(alphaRect.top), Math.round(alphaRect.right), 798 | Math.round(alphaRect.bottom)); 799 | } 800 | 801 | /** 802 | * Set a OnColorChangedListener to get notified when the color 803 | * selected by the user has changed. 804 | * 805 | * @param listener the listener 806 | */ 807 | public void setOnColorChangedListener(OnColorChangedListener listener) { 808 | onColorChangedListener = listener; 809 | } 810 | 811 | /** 812 | * Get the current color this view is showing. 813 | * 814 | * @return the current color. 815 | */ 816 | public int getColor() { 817 | return Color.HSVToColor(alpha, new float[] { hue, sat, val }); 818 | } 819 | 820 | /** 821 | * Set the color the view should show. 822 | * 823 | * @param color The color that should be selected. #argb 824 | */ 825 | public void setColor(int color) { 826 | setColor(color, false); 827 | } 828 | 829 | /** 830 | * Set the color this view should show. 831 | * 832 | * @param color The color that should be selected. #argb 833 | * @param callback If you want to get a callback to your OnColorChangedListener. 834 | */ 835 | public void setColor(int color, boolean callback) { 836 | 837 | int alpha = Color.alpha(color); 838 | int red = Color.red(color); 839 | int blue = Color.blue(color); 840 | int green = Color.green(color); 841 | 842 | float[] hsv = new float[3]; 843 | 844 | Color.RGBToHSV(red, green, blue, hsv); 845 | 846 | this.alpha = alpha; 847 | hue = hsv[0]; 848 | sat = hsv[1]; 849 | val = hsv[2]; 850 | 851 | if (callback && onColorChangedListener != null) { 852 | onColorChangedListener.onColorChanged(Color.HSVToColor(this.alpha, new float[] { hue, sat, val })); 853 | } 854 | 855 | invalidate(); 856 | } 857 | 858 | /** 859 | * Set if the user is allowed to adjust the alpha panel. Default is false. 860 | * If it is set to false no alpha will be set. 861 | * 862 | * @param visible {@code true} to show the alpha slider 863 | */ 864 | public void setAlphaSliderVisible(boolean visible) { 865 | if (showAlphaPanel != visible) { 866 | showAlphaPanel = visible; 867 | 868 | /* 869 | * Force recreation. 870 | */ 871 | valShader = null; 872 | satShader = null; 873 | alphaShader = null; 874 | hueBackgroundCache = null; 875 | satValBackgroundCache = null; 876 | 877 | requestLayout(); 878 | } 879 | } 880 | 881 | /** 882 | * Get color of the tracker slider on the hue and alpha panel. 883 | * 884 | * @return the color value 885 | */ 886 | public int getSliderTrackerColor() { 887 | return sliderTrackerColor; 888 | } 889 | 890 | /** 891 | * Set the color of the tracker slider on the hue and alpha panel. 892 | * 893 | * @param color a color value 894 | */ 895 | public void setSliderTrackerColor(int color) { 896 | sliderTrackerColor = color; 897 | hueAlphaTrackerPaint.setColor(sliderTrackerColor); 898 | invalidate(); 899 | } 900 | 901 | /** 902 | * Get the color of the border surrounding all panels. 903 | */ 904 | public int getBorderColor() { 905 | return borderColor; 906 | } 907 | 908 | /** 909 | * Set the color of the border surrounding all panels. 910 | * 911 | * @param color a color value 912 | */ 913 | public void setBorderColor(int color) { 914 | borderColor = color; 915 | invalidate(); 916 | } 917 | 918 | /** 919 | * Set the text that should be shown in the 920 | * alpha slider. Set to null to disable text. 921 | * 922 | * @param res string resource id. 923 | */ 924 | public void setAlphaSliderText(int res) { 925 | String text = getContext().getString(res); 926 | setAlphaSliderText(text); 927 | } 928 | 929 | /** 930 | * Get the current value of the text 931 | * that will be shown in the alpha 932 | * slider. 933 | * 934 | * @return the slider text 935 | */ 936 | public String getAlphaSliderText() { 937 | return alphaSliderText; 938 | } 939 | 940 | /** 941 | * Set the text that should be shown in the 942 | * alpha slider. Set to null to disable text. 943 | * 944 | * @param text Text that should be shown. 945 | */ 946 | public void setAlphaSliderText(String text) { 947 | alphaSliderText = text; 948 | invalidate(); 949 | } 950 | 951 | public interface OnColorChangedListener { 952 | 953 | void onColorChanged(int newColor); 954 | } 955 | 956 | private class BitmapCache { 957 | 958 | public Canvas canvas; 959 | public Bitmap bitmap; 960 | public float value; 961 | } 962 | } 963 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/ColorPreference.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import android.content.Context; 20 | import android.content.res.TypedArray; 21 | import android.graphics.Color; 22 | import android.preference.Preference; 23 | import android.util.AttributeSet; 24 | import android.view.View; 25 | import androidx.annotation.ColorInt; 26 | import androidx.annotation.NonNull; 27 | import androidx.fragment.app.FragmentActivity; 28 | 29 | /** 30 | * A Preference to select a color 31 | */ 32 | public class ColorPreference extends Preference implements ColorPickerDialogListener { 33 | 34 | private static final int SIZE_NORMAL = 0; 35 | private static final int SIZE_LARGE = 1; 36 | 37 | private OnShowDialogListener onShowDialogListener; 38 | private int color = Color.BLACK; 39 | private boolean showDialog; 40 | @ColorPickerDialog.DialogType private int dialogType; 41 | private int colorShape; 42 | private boolean allowPresets; 43 | private boolean allowCustom; 44 | private boolean showAlphaSlider; 45 | private boolean showColorShades; 46 | private int previewSize; 47 | private int[] presets; 48 | private int dialogTitle; 49 | 50 | public ColorPreference(Context context, AttributeSet attrs) { 51 | super(context, attrs); 52 | init(attrs); 53 | } 54 | 55 | public ColorPreference(Context context, AttributeSet attrs, int defStyle) { 56 | super(context, attrs, defStyle); 57 | init(attrs); 58 | } 59 | 60 | private void init(AttributeSet attrs) { 61 | setPersistent(true); 62 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPreference); 63 | showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true); 64 | //noinspection WrongConstant 65 | dialogType = a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS); 66 | colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE); 67 | allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true); 68 | allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true); 69 | showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false); 70 | showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true); 71 | previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL); 72 | final int presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0); 73 | dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title); 74 | if (presetsResId != 0) { 75 | presets = getContext().getResources().getIntArray(presetsResId); 76 | } else { 77 | presets = ColorPickerDialog.MATERIAL_COLORS; 78 | } 79 | if (colorShape == ColorShape.CIRCLE) { 80 | setWidgetLayoutResource( 81 | previewSize == SIZE_LARGE ? R.layout.cpv_preference_circle_large : R.layout.cpv_preference_circle); 82 | } else { 83 | setWidgetLayoutResource( 84 | previewSize == SIZE_LARGE ? R.layout.cpv_preference_square_large : R.layout.cpv_preference_square); 85 | } 86 | a.recycle(); 87 | } 88 | 89 | @Override protected void onClick() { 90 | super.onClick(); 91 | if (onShowDialogListener != null) { 92 | onShowDialogListener.onShowColorPickerDialog((String) getTitle(), color); 93 | } else if (showDialog) { 94 | ColorPickerDialog dialog = ColorPickerDialog.newBuilder() 95 | .setDialogType(dialogType) 96 | .setDialogTitle(dialogTitle) 97 | .setColorShape(colorShape) 98 | .setPresets(presets) 99 | .setAllowPresets(allowPresets) 100 | .setAllowCustom(allowCustom) 101 | .setShowAlphaSlider(showAlphaSlider) 102 | .setShowColorShades(showColorShades) 103 | .setColor(color) 104 | .create(); 105 | dialog.setColorPickerDialogListener(this); 106 | FragmentActivity activity = (FragmentActivity) getContext(); 107 | activity.getSupportFragmentManager() 108 | .beginTransaction() 109 | .add(dialog, getFragmentTag()) 110 | .commitAllowingStateLoss(); 111 | } 112 | } 113 | 114 | @Override protected void onAttachedToActivity() { 115 | super.onAttachedToActivity(); 116 | 117 | if (showDialog) { 118 | FragmentActivity activity = (FragmentActivity) getContext(); 119 | ColorPickerDialog fragment = 120 | (ColorPickerDialog) activity.getSupportFragmentManager().findFragmentByTag(getFragmentTag()); 121 | if (fragment != null) { 122 | // re-bind preference to fragment 123 | fragment.setColorPickerDialogListener(this); 124 | } 125 | } 126 | } 127 | 128 | @Override protected void onBindView(View view) { 129 | super.onBindView(view); 130 | ColorPanelView preview = (ColorPanelView) view.findViewById(R.id.cpv_preference_preview_color_panel); 131 | if (preview != null) { 132 | preview.setColor(color); 133 | } 134 | } 135 | 136 | @Override protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { 137 | if (restorePersistedValue) { 138 | color = getPersistedInt(0xFF000000); 139 | } else { 140 | color = (Integer) defaultValue; 141 | persistInt(color); 142 | } 143 | } 144 | 145 | @Override protected Object onGetDefaultValue(TypedArray a, int index) { 146 | return a.getInteger(index, Color.BLACK); 147 | } 148 | 149 | @Override public void onColorSelected(int dialogId, @ColorInt int color) { 150 | saveValue(color); 151 | } 152 | 153 | @Override public void onDialogDismissed(int dialogId) { 154 | // no-op 155 | } 156 | 157 | /** 158 | * Set the new color 159 | * 160 | * @param color The newly selected color 161 | */ 162 | public void saveValue(@ColorInt int color) { 163 | this.color = color; 164 | persistInt(this.color); 165 | notifyChanged(); 166 | callChangeListener(color); 167 | } 168 | 169 | /** 170 | * Get the colors that will be shown in the {@link ColorPickerDialog}. 171 | * 172 | * @return An array of color ints 173 | */ 174 | public int[] getPresets() { 175 | return presets; 176 | } 177 | 178 | /** 179 | * Set the colors shown in the {@link ColorPickerDialog}. 180 | * 181 | * @param presets An array of color ints 182 | */ 183 | public void setPresets(@NonNull int[] presets) { 184 | this.presets = presets; 185 | } 186 | 187 | /** 188 | * The listener used for showing the {@link ColorPickerDialog}. 189 | * Call {@link #saveValue(int)} after the user chooses a color. 190 | * If this is set then it is up to you to show the dialog. 191 | * 192 | * @param listener The listener to show the dialog 193 | */ 194 | public void setOnShowDialogListener(OnShowDialogListener listener) { 195 | onShowDialogListener = listener; 196 | } 197 | 198 | /** 199 | * The tag used for the {@link ColorPickerDialog}. 200 | * 201 | * @return The tag 202 | */ 203 | public String getFragmentTag() { 204 | return "color_" + getKey(); 205 | } 206 | 207 | public interface OnShowDialogListener { 208 | 209 | void onShowColorPickerDialog(String title, int currentColor); 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/ColorPreferenceCompat.java: -------------------------------------------------------------------------------- 1 | package com.jaredrummler.android.colorpicker; 2 | 3 | import android.content.Context; 4 | import android.content.ContextWrapper; 5 | import android.content.res.TypedArray; 6 | import android.graphics.Color; 7 | import android.util.AttributeSet; 8 | import androidx.annotation.ColorInt; 9 | import androidx.annotation.NonNull; 10 | import androidx.fragment.app.FragmentActivity; 11 | import androidx.preference.Preference; 12 | import androidx.preference.PreferenceViewHolder; 13 | 14 | /** 15 | * A Preference to select a color 16 | */ 17 | public class ColorPreferenceCompat extends Preference implements ColorPickerDialogListener { 18 | 19 | private static final int SIZE_NORMAL = 0; 20 | private static final int SIZE_LARGE = 1; 21 | 22 | private OnShowDialogListener onShowDialogListener; 23 | private int color = Color.BLACK; 24 | private boolean showDialog; 25 | @ColorPickerDialog.DialogType private int dialogType; 26 | private int colorShape; 27 | private boolean allowPresets; 28 | private boolean allowCustom; 29 | private boolean showAlphaSlider; 30 | private boolean showColorShades; 31 | private int previewSize; 32 | private int[] presets; 33 | private int dialogTitle; 34 | 35 | public ColorPreferenceCompat(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | init(attrs); 38 | } 39 | 40 | public ColorPreferenceCompat(Context context, AttributeSet attrs, int defStyle) { 41 | super(context, attrs, defStyle); 42 | init(attrs); 43 | } 44 | 45 | private void init(AttributeSet attrs) { 46 | setPersistent(true); 47 | TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPreference); 48 | showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true); 49 | //noinspection WrongConstant 50 | dialogType = a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS); 51 | colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE); 52 | allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true); 53 | allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true); 54 | showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false); 55 | showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true); 56 | previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL); 57 | final int presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0); 58 | dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title); 59 | if (presetsResId != 0) { 60 | presets = getContext().getResources().getIntArray(presetsResId); 61 | } else { 62 | presets = ColorPickerDialog.MATERIAL_COLORS; 63 | } 64 | if (colorShape == ColorShape.CIRCLE) { 65 | setWidgetLayoutResource( 66 | previewSize == SIZE_LARGE ? R.layout.cpv_preference_circle_large : R.layout.cpv_preference_circle); 67 | } else { 68 | setWidgetLayoutResource( 69 | previewSize == SIZE_LARGE ? R.layout.cpv_preference_square_large : R.layout.cpv_preference_square); 70 | } 71 | a.recycle(); 72 | } 73 | 74 | @Override protected void onClick() { 75 | super.onClick(); 76 | if (onShowDialogListener != null) { 77 | onShowDialogListener.onShowColorPickerDialog((String) getTitle(), color); 78 | } else if (showDialog) { 79 | ColorPickerDialog dialog = ColorPickerDialog.newBuilder() 80 | .setDialogType(dialogType) 81 | .setDialogTitle(dialogTitle) 82 | .setColorShape(colorShape) 83 | .setPresets(presets) 84 | .setAllowPresets(allowPresets) 85 | .setAllowCustom(allowCustom) 86 | .setShowAlphaSlider(showAlphaSlider) 87 | .setShowColorShades(showColorShades) 88 | .setColor(color) 89 | .create(); 90 | dialog.setColorPickerDialogListener(this); 91 | getActivity().getSupportFragmentManager() 92 | .beginTransaction() 93 | .add(dialog, getFragmentTag()) 94 | .commitAllowingStateLoss(); 95 | } 96 | } 97 | 98 | public FragmentActivity getActivity() { 99 | Context context = getContext(); 100 | if (context instanceof FragmentActivity) { 101 | return (FragmentActivity) context; 102 | } else if (context instanceof ContextWrapper) { 103 | Context baseContext = ((ContextWrapper) context).getBaseContext(); 104 | if (baseContext instanceof FragmentActivity) { 105 | return (FragmentActivity) baseContext; 106 | } 107 | } 108 | throw new IllegalStateException("Error getting activity from context"); 109 | } 110 | 111 | @Override public void onAttached() { 112 | super.onAttached(); 113 | if (showDialog) { 114 | ColorPickerDialog fragment = 115 | (ColorPickerDialog) getActivity().getSupportFragmentManager().findFragmentByTag(getFragmentTag()); 116 | if (fragment != null) { 117 | // re-bind preference to fragment 118 | fragment.setColorPickerDialogListener(this); 119 | } 120 | } 121 | } 122 | 123 | @Override public void onBindViewHolder(PreferenceViewHolder holder) { 124 | super.onBindViewHolder(holder); 125 | ColorPanelView preview = (ColorPanelView) holder.itemView.findViewById(R.id.cpv_preference_preview_color_panel); 126 | if (preview != null) { 127 | preview.setColor(color); 128 | } 129 | } 130 | 131 | @Override protected void onSetInitialValue(Object defaultValue) { 132 | super.onSetInitialValue(defaultValue); 133 | if (defaultValue instanceof Integer) { 134 | color = (Integer) defaultValue; 135 | persistInt(color); 136 | } else { 137 | color = getPersistedInt(0xFF000000); 138 | } 139 | } 140 | 141 | @Override protected Object onGetDefaultValue(TypedArray a, int index) { 142 | return a.getInteger(index, Color.BLACK); 143 | } 144 | 145 | @Override public void onColorSelected(int dialogId, @ColorInt int color) { 146 | saveValue(color); 147 | } 148 | 149 | @Override public void onDialogDismissed(int dialogId) { 150 | // no-op 151 | } 152 | 153 | /** 154 | * Set the new color 155 | * 156 | * @param color The newly selected color 157 | */ 158 | public void saveValue(@ColorInt int color) { 159 | this.color = color; 160 | persistInt(this.color); 161 | notifyChanged(); 162 | callChangeListener(color); 163 | } 164 | 165 | /** 166 | * Get the colors that will be shown in the {@link ColorPickerDialog}. 167 | * 168 | * @return An array of color ints 169 | */ 170 | public int[] getPresets() { 171 | return presets; 172 | } 173 | 174 | /** 175 | * Set the colors shown in the {@link ColorPickerDialog}. 176 | * 177 | * @param presets An array of color ints 178 | */ 179 | public void setPresets(@NonNull int[] presets) { 180 | this.presets = presets; 181 | } 182 | 183 | /** 184 | * The listener used for showing the {@link ColorPickerDialog}. 185 | * Call {@link #saveValue(int)} after the user chooses a color. 186 | * If this is set then it is up to you to show the dialog. 187 | * 188 | * @param listener The listener to show the dialog 189 | */ 190 | public void setOnShowDialogListener(OnShowDialogListener listener) { 191 | onShowDialogListener = listener; 192 | } 193 | 194 | /** 195 | * The tag used for the {@link ColorPickerDialog}. 196 | * 197 | * @return The tag 198 | */ 199 | public String getFragmentTag() { 200 | return "color_" + getKey(); 201 | } 202 | 203 | public interface OnShowDialogListener { 204 | 205 | void onShowColorPickerDialog(String title, int currentColor); 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/ColorShape.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import androidx.annotation.IntDef; 20 | 21 | /** 22 | * The shape of the color preview 23 | */ 24 | @IntDef({ ColorShape.SQUARE, ColorShape.CIRCLE }) public @interface ColorShape { 25 | 26 | int SQUARE = 0; 27 | 28 | int CIRCLE = 1; 29 | } 30 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/DrawingUtils.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import android.content.Context; 20 | import android.util.DisplayMetrics; 21 | import android.util.TypedValue; 22 | 23 | final class DrawingUtils { 24 | 25 | static int dpToPx(Context c, float dipValue) { 26 | DisplayMetrics metrics = c.getResources().getDisplayMetrics(); 27 | float val = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics); 28 | int res = (int) (val + 0.5); // Round 29 | // Ensure at least 1 pixel if val was > 0 30 | return res == 0 && val > 0 ? 1 : res; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /library/src/main/java/com/jaredrummler/android/colorpicker/NestedGridView.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2017 Jared Rummler 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.jaredrummler.android.colorpicker; 18 | 19 | import android.content.Context; 20 | import android.util.AttributeSet; 21 | import android.widget.GridView; 22 | import androidx.annotation.RestrictTo; 23 | 24 | @RestrictTo(RestrictTo.Scope.LIBRARY) public class NestedGridView extends GridView { 25 | 26 | public NestedGridView(Context context) { 27 | super(context); 28 | } 29 | 30 | public NestedGridView(Context context, AttributeSet attrs) { 31 | super(context, attrs); 32 | } 33 | 34 | public NestedGridView(Context context, AttributeSet attrs, int defStyle) { 35 | super(context, attrs, defStyle); 36 | } 37 | 38 | @Override public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 39 | int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); 40 | super.onMeasure(widthMeasureSpec, expandSpec); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /library/src/main/res/drawable-hdpi/cpv_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredrummler/ColorPicker/eb76c92f53087cebff5521e217015ba95e49ad39/library/src/main/res/drawable-hdpi/cpv_alpha.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xhdpi/cpv_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredrummler/ColorPicker/eb76c92f53087cebff5521e217015ba95e49ad39/library/src/main/res/drawable-xhdpi/cpv_alpha.png -------------------------------------------------------------------------------- /library/src/main/res/drawable-xxhdpi/cpv_alpha.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jaredrummler/ColorPicker/eb76c92f53087cebff5521e217015ba95e49ad39/library/src/main/res/drawable-xxhdpi/cpv_alpha.png -------------------------------------------------------------------------------- /library/src/main/res/drawable/cpv_btn_background.xml: -------------------------------------------------------------------------------- 1 | 2 |