diff --git a/app/build.gradle b/app/build.gradle index 03673aaa..f2c3a7ee 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -96,12 +96,11 @@ allprojects { } } dependencies { - implementation project(':autoimageslider') + implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.8.0' - implementation 'com.jaredrummler:colorpicker:1.1.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' implementation "com.google.code.gson:gson:2.9.1" @@ -121,15 +120,18 @@ dependencies { } implementation "org.jsoup:jsoup:1.15.1" - implementation 'com.github.mergehez:ArgPlayer:v3.1' + + implementation project(':autoimageslider') implementation project(path: ':mytransl') implementation project(path: ':ratethisapp') implementation project(path: ':sparkbutton') + implementation project(path: ':colorPicker') + implementation project(path: ':mathjaxandroid') + implementation 'com.burhanrashid52:photoeditor:1.5.1' implementation("com.vanniktech:android-image-cropper:4.3.3") - implementation project(path: ':mathjaxandroid') annotationProcessor "com.github.bumptech.glide:compiler:4.12.0" implementation 'jp.wasabeef:glide-transformations:4.3.0' implementation 'com.github.penfeizhou.android.animation:glide-plugin:2.23.0' @@ -165,7 +167,6 @@ dependencies { implementation 'com.r0adkll:slidableactivity:2.1.0' coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:1.1.5' - implementation 'androidx.vectordrawable:vectordrawable:1.1.0' implementation "androidx.fragment:fragment:1.5.5" implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0' diff --git a/app/src/main/java/app/fedilab/android/BaseMainActivity.java b/app/src/main/java/app/fedilab/android/BaseMainActivity.java index 71740d27..2521198a 100644 --- a/app/src/main/java/app/fedilab/android/BaseMainActivity.java +++ b/app/src/main/java/app/fedilab/android/BaseMainActivity.java @@ -1067,6 +1067,16 @@ public abstract class BaseMainActivity extends BaseActivity implements NetworkSt } catch (DBException e) { e.printStackTrace(); } + if (currentAccount != null && currentInstance == null) { + currentInstance = currentAccount.instance; + currentUserID = currentAccount.user_id; + Handler mainHandler = new Handler(Looper.getMainLooper()); + Runnable myRunnable = () -> { + recreate(); + }; + mainHandler.post(myRunnable); + } + if (currentAccount != null && currentAccount.peertube_account != null) { //It is a peertube user Intent intent = getIntent(); diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseActivity.java index c71a9989..b36be9d5 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseActivity.java @@ -30,7 +30,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.PreferenceManager; -import com.google.android.material.color.DynamicColors; import com.vanniktech.emoji.EmojiManager; import com.vanniktech.emoji.one.EmojiOneProvider; @@ -52,6 +51,7 @@ public class BaseActivity extends AppCompatActivity { EmojiManager.install(new EmojiOneProvider()); } + @SuppressLint("RestrictedApi") @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -70,9 +70,8 @@ public class BaseActivity extends AppCompatActivity { String currentTheme = sharedpreferences.getString(getString(R.string.SET_THEME_BASE), getString(R.string.SET_DEFAULT_THEME)); //Default automatic switch + int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (currentTheme.equals(getString(R.string.SET_DEFAULT_THEME))) { - - int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (currentNightMode) { case Configuration.UI_MODE_NIGHT_NO: String defaultLight = sharedpreferences.getString(getString(R.string.SET_THEME_DEFAULT_LIGHT), "LIGHT"); @@ -144,10 +143,8 @@ public class BaseActivity extends AppCompatActivity { } } super.onCreate(savedInstanceState); - boolean dynamicColor = sharedpreferences.getBoolean(getString(R.string.SET_DYNAMICCOLOR), false); - if (dynamicColor) { - DynamicColors.applyToActivityIfAvailable(this); - } + ThemeHelper.applyThemeColor(this); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.N) { ThemeHelper.adjustFontScale(this, getResources().getConfiguration()); } diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseBarActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseBarActivity.java index fd567575..87292e1d 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseBarActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseBarActivity.java @@ -30,7 +30,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.PreferenceManager; -import com.google.android.material.color.DynamicColors; import com.vanniktech.emoji.EmojiManager; import com.vanniktech.emoji.one.EmojiOneProvider; @@ -66,9 +65,8 @@ public class BaseBarActivity extends AppCompatActivity { } String currentTheme = sharedpreferences.getString(getString(R.string.SET_THEME_BASE), getString(R.string.SET_DEFAULT_THEME)); //Default automatic switch + int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (currentTheme.equals(getString(R.string.SET_DEFAULT_THEME))) { - - int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (currentNightMode) { case Configuration.UI_MODE_NIGHT_NO: String defaultLight = sharedpreferences.getString(getString(R.string.SET_THEME_DEFAULT_LIGHT), "LIGHT"); @@ -129,10 +127,8 @@ public class BaseBarActivity extends AppCompatActivity { } } super.onCreate(savedInstanceState); - boolean dynamicColor = sharedpreferences.getBoolean(getString(R.string.SET_DYNAMICCOLOR), false); - if (dynamicColor) { - DynamicColors.applyToActivityIfAvailable(this); - } + ThemeHelper.applyThemeColor(this); + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); diff --git a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseTransparentActivity.java b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseTransparentActivity.java index d31acbe9..8e77ec54 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/activities/BaseTransparentActivity.java +++ b/app/src/main/java/app/fedilab/android/mastodon/activities/BaseTransparentActivity.java @@ -30,7 +30,6 @@ import androidx.appcompat.app.AppCompatActivity; import androidx.appcompat.app.AppCompatDelegate; import androidx.preference.PreferenceManager; -import com.google.android.material.color.DynamicColors; import com.vanniktech.emoji.EmojiManager; import com.vanniktech.emoji.one.EmojiOneProvider; @@ -66,9 +65,9 @@ public class BaseTransparentActivity extends AppCompatActivity { } String currentTheme = sharedpreferences.getString(getString(R.string.SET_THEME_BASE), getString(R.string.SET_DEFAULT_THEME)); //Default automatic switch + int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; if (currentTheme.equals(getString(R.string.SET_DEFAULT_THEME))) { - int currentNightMode = getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; switch (currentNightMode) { case Configuration.UI_MODE_NIGHT_NO: String defaultLight = sharedpreferences.getString(getString(R.string.SET_THEME_DEFAULT_LIGHT), "LIGHT"); @@ -129,10 +128,7 @@ public class BaseTransparentActivity extends AppCompatActivity { } } super.onCreate(savedInstanceState); - boolean dynamicColor = sharedpreferences.getBoolean(getString(R.string.SET_DYNAMICCOLOR), false); - if (dynamicColor) { - DynamicColors.applyToActivityIfAvailable(this); - } + ThemeHelper.applyThemeColor(this); if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.M) { Window window = getWindow(); window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS); diff --git a/app/src/main/java/app/fedilab/android/mastodon/helper/ThemeHelper.java b/app/src/main/java/app/fedilab/android/mastodon/helper/ThemeHelper.java index d2f690da..334d8309 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/helper/ThemeHelper.java +++ b/app/src/main/java/app/fedilab/android/mastodon/helper/ThemeHelper.java @@ -15,6 +15,8 @@ package app.fedilab.android.mastodon.helper; * see . */ import static android.content.Context.WINDOW_SERVICE; +import static app.fedilab.android.BaseMainActivity.currentInstance; +import static app.fedilab.android.BaseMainActivity.currentUserID; import android.app.Activity; import android.content.Context; @@ -23,6 +25,8 @@ import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Canvas; import android.os.Build; import android.util.DisplayMetrics; import android.util.TypedValue; @@ -37,6 +41,9 @@ import androidx.appcompat.app.AppCompatDelegate; import androidx.core.content.ContextCompat; import androidx.preference.PreferenceManager; +import com.google.android.material.color.DynamicColors; +import com.google.android.material.color.DynamicColorsOptions; + import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; @@ -278,6 +285,31 @@ public class ThemeHelper { } } + public static void applyThemeColor(Activity activity) { + int currentNightMode = activity.getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK; + final SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(activity); + boolean dynamicColor = sharedpreferences.getBoolean(activity.getString(R.string.SET_DYNAMICCOLOR), false); + boolean customAccentEnabled = sharedpreferences.getBoolean(activity.getString(R.string.SET_CUSTOM_ACCENT) + currentUserID + currentInstance, false); + int customAccentLight = sharedpreferences.getInt(activity.getString(R.string.SET_CUSTOM_ACCENT_LIGHT_VALUE) + currentUserID + currentInstance, -1); + int customAccentDark = sharedpreferences.getInt(activity.getString(R.string.SET_CUSTOM_ACCENT_DARK_VALUE) + currentUserID + currentInstance, -1); + if (customAccentEnabled) { + Bitmap bmp = Bitmap.createBitmap(50, 50, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bmp); + //Light theme enabled + if (currentNightMode == Configuration.UI_MODE_NIGHT_NO && customAccentLight != -1) { + canvas.drawColor(customAccentLight); + } else if (customAccentDark != -1) { + canvas.drawColor(customAccentDark); + } + DynamicColorsOptions.Builder builder = new DynamicColorsOptions.Builder(); + builder.setContentBasedSource(bmp); + DynamicColorsOptions dynamicColorsOptions = builder.build(); + DynamicColors.applyToActivityIfAvailable(activity, dynamicColorsOptions); + } else if (dynamicColor) { + DynamicColors.applyToActivityIfAvailable(activity); + } + } + public enum themes { LIGHT, DARK, diff --git a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentThemingSettings.java b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentThemingSettings.java index a61b3174..68effe5f 100644 --- a/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentThemingSettings.java +++ b/app/src/main/java/app/fedilab/android/mastodon/ui/fragment/settings/FragmentThemingSettings.java @@ -15,8 +15,13 @@ package app.fedilab.android.mastodon.ui.fragment.settings; * see . */ +import static app.fedilab.android.BaseMainActivity.currentInstance; +import static app.fedilab.android.BaseMainActivity.currentUserID; + import android.content.SharedPreferences; +import android.os.Build; import android.os.Bundle; +import android.util.Log; import androidx.appcompat.app.AlertDialog; import androidx.navigation.NavOptions; @@ -24,16 +29,21 @@ import androidx.navigation.Navigation; import androidx.preference.ListPreference; import androidx.preference.Preference; import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; +import androidx.preference.SwitchPreferenceCompat; import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.jaredrummler.android.colorpicker.ColorPreferenceCompat; import app.fedilab.android.R; +import app.fedilab.android.activities.MainActivity; import app.fedilab.android.mastodon.helper.Helper; import es.dmoral.toasty.Toasty; public class FragmentThemingSettings extends PreferenceFragmentCompat implements SharedPreferences.OnSharedPreferenceChangeListener { + boolean prefChanged = false; @Override public void onCreatePreferences(Bundle bundle, String s) { createPref(); @@ -55,22 +65,45 @@ public class FragmentThemingSettings extends PreferenceFragmentCompat implements if (getPreferenceScreen() != null && getPreferenceScreen().getSharedPreferences() != null) { getPreferenceScreen().getSharedPreferences() .unregisterOnSharedPreferenceChangeListener(this); + if (prefChanged) { + Helper.recreateMainActivity(requireActivity()); + prefChanged = false; + } } } @Override public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - + SharedPreferences sharedpreferences = PreferenceManager.getDefaultSharedPreferences(requireActivity()); + SharedPreferences.Editor editor = sharedpreferences.edit(); + prefChanged = true; if (key.compareTo(getString(R.string.SET_THEME_BASE)) == 0) { ListPreference SET_THEME_BASE = findPreference(getString(R.string.SET_THEME_BASE)); if (SET_THEME_BASE != null) { requireActivity().finish(); startActivity(requireActivity().getIntent()); } + Helper.recreateMainActivity(requireActivity()); + } else if (key.compareTo(getString(R.string.SET_CUSTOM_ACCENT)) == 0) { + SwitchPreferenceCompat SET_CUSTOM_ACCENT = findPreference(getString(R.string.SET_CUSTOM_ACCENT)); + if (SET_CUSTOM_ACCENT != null) { + editor.putBoolean(getString(R.string.SET_CUSTOM_ACCENT) + MainActivity.currentUserID + MainActivity.currentInstance, SET_CUSTOM_ACCENT.isChecked()); + } + } else if (key.compareTo(getString(R.string.SET_CUSTOM_ACCENT_LIGHT_VALUE)) == 0) { + ColorPreferenceCompat SET_CUSTOM_ACCENT_VALUE = findPreference(getString(R.string.SET_CUSTOM_ACCENT_LIGHT_VALUE)); + if (SET_CUSTOM_ACCENT_VALUE != null) { + editor.putInt(getString(R.string.SET_CUSTOM_ACCENT_LIGHT_VALUE) + MainActivity.currentUserID + MainActivity.currentInstance, SET_CUSTOM_ACCENT_VALUE.getColor()); + } + } else if (key.compareTo(getString(R.string.SET_CUSTOM_ACCENT_DARK_VALUE)) == 0) { + ColorPreferenceCompat SET_CUSTOM_ACCENT_VALUE = findPreference(getString(R.string.SET_CUSTOM_ACCENT_DARK_VALUE)); + if (SET_CUSTOM_ACCENT_VALUE != null) { + editor.putInt(getString(R.string.SET_CUSTOM_ACCENT_DARK_VALUE) + MainActivity.currentUserID + MainActivity.currentInstance, SET_CUSTOM_ACCENT_VALUE.getColor()); + } } - //TODO: check if can be removed - Helper.recreateMainActivity(requireActivity()); + Log.v(Helper.TAG, "currentUserID: " + currentUserID); + Log.v(Helper.TAG, "currentInstance: " + currentInstance); + editor.apply(); } @@ -83,6 +116,24 @@ public class FragmentThemingSettings extends PreferenceFragmentCompat implements Toasty.error(requireActivity(), getString(R.string.toast_error), Toasty.LENGTH_SHORT).show(); } + SwitchPreferenceCompat SET_DYNAMIC_COLOR = findPreference(getString(R.string.SET_DYNAMICCOLOR)); + SwitchPreferenceCompat SET_CUSTOM_ACCENT = findPreference(getString(R.string.SET_CUSTOM_ACCENT)); + ColorPreferenceCompat SET_CUSTOM_ACCENT_DARK_VALUE = findPreference(getString(R.string.SET_CUSTOM_ACCENT_DARK_VALUE)); + ColorPreferenceCompat SET_CUSTOM_ACCENT_LIGHT_VALUE = findPreference(getString(R.string.SET_CUSTOM_ACCENT_LIGHT_VALUE)); + if (android.os.Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (SET_DYNAMIC_COLOR != null) { + getPreferenceScreen().removePreference(SET_DYNAMIC_COLOR); + } + if (SET_CUSTOM_ACCENT != null) { + getPreferenceScreen().removePreference(SET_CUSTOM_ACCENT); + } + if (SET_CUSTOM_ACCENT_DARK_VALUE != null) { + getPreferenceScreen().removePreference(SET_CUSTOM_ACCENT_DARK_VALUE); + } + if (SET_CUSTOM_ACCENT_LIGHT_VALUE != null) { + getPreferenceScreen().removePreference(SET_CUSTOM_ACCENT_LIGHT_VALUE); + } + } Preference SET_CUSTOMIZE_LIGHT_COLORS_ACTION = findPreference(getString(R.string.SET_CUSTOMIZE_LIGHT_COLORS_ACTION)); if (SET_CUSTOMIZE_LIGHT_COLORS_ACTION != null) { diff --git a/app/src/main/res/layouts/mastodon/values/strings.xml b/app/src/main/res/layouts/mastodon/values/strings.xml index 0d6b2421..d319693c 100644 --- a/app/src/main/res/layouts/mastodon/values/strings.xml +++ b/app/src/main/res/layouts/mastodon/values/strings.xml @@ -3,4 +3,10 @@ Auto + Custom accent color + Define a theme color per account + Light accent color + Dark accent color + Color that will be applied to the light theme + Color that will be applied to the dark theme \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0f130646..2dc50eb8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1037,6 +1037,10 @@ SET_THEME_BASE SET_DYNAMICCOLOR + SET_CUSTOM_ACCENT + SET_CUSTOM_ACCENT_LIGHT_VALUE + SET_CUSTOM_ACCENT_DARK_VALUE + SET_CARDVIEW SET_CUSTOMIZE_LIGHT_COLORS SET_CHAT_FOR_CONVERSATION diff --git a/app/src/main/res/xml/pref_theming.xml b/app/src/main/res/xml/pref_theming.xml index 3751a455..5fa34c58 100644 --- a/app/src/main/res/xml/pref_theming.xml +++ b/app/src/main/res/xml/pref_theming.xml @@ -24,6 +24,28 @@ app:summary="@string/set_dynamic_color_indication" app:title="@string/set_dynamic_color" /> + + + + + + diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java new file mode 100644 index 00000000..806ab027 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/AlphaPatternDrawable.java @@ -0,0 +1,113 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.ColorFilter; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.drawable.Drawable; + +/** + * This drawable will draw a simple white and gray chessboard pattern. + * It's the pattern you will often see as a background behind a partly transparent image in many applications. + */ +class AlphaPatternDrawable extends Drawable { + + private final Paint paint = new Paint(); + private final Paint paintWhite = new Paint(); + private final Paint paintGray = new Paint(); + private int rectangleSize = 10; + private int numRectanglesHorizontal; + private int numRectanglesVertical; + + /** + * Bitmap in which the pattern will be cached. + * This is so the pattern will not have to be recreated each time draw() gets called. + * Because recreating the pattern i rather expensive. I will only be recreated if the size changes. + */ + private Bitmap bitmap; + + AlphaPatternDrawable(int rectangleSize) { + this.rectangleSize = rectangleSize; + paintWhite.setColor(0xFFFFFFFF); + paintGray.setColor(0xFFCBCBCB); + } + + @Override + public void draw(Canvas canvas) { + if (bitmap != null && !bitmap.isRecycled()) { + canvas.drawBitmap(bitmap, null, getBounds(), paint); + } + } + + @Override + public int getOpacity() { + return 0; + } + + @Override + public void setAlpha(int alpha) { + throw new UnsupportedOperationException("Alpha is not supported by this drawable."); + } + + @Override + public void setColorFilter(ColorFilter cf) { + throw new UnsupportedOperationException("ColorFilter is not supported by this drawable."); + } + + @Override + protected void onBoundsChange(Rect bounds) { + super.onBoundsChange(bounds); + int height = bounds.height(); + int width = bounds.width(); + numRectanglesHorizontal = (int) Math.ceil((width / rectangleSize)); + numRectanglesVertical = (int) Math.ceil(height / rectangleSize); + generatePatternBitmap(); + } + + /** + * This will generate a bitmap with the pattern as big as the rectangle we were allow to draw on. + * We do this to chache the bitmap so we don't need to recreate it each time draw() is called since it takes a few + * milliseconds + */ + private void generatePatternBitmap() { + if (getBounds().width() <= 0 || getBounds().height() <= 0) { + return; + } + + bitmap = Bitmap.createBitmap(getBounds().width(), getBounds().height(), Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + Rect r = new Rect(); + boolean verticalStartWhite = true; + for (int i = 0; i <= numRectanglesVertical; i++) { + boolean isWhite = verticalStartWhite; + for (int j = 0; j <= numRectanglesHorizontal; j++) { + r.top = i * rectangleSize; + r.left = j * rectangleSize; + r.bottom = r.top + rectangleSize; + r.right = r.left + rectangleSize; + canvas.drawRect(r, isWhite ? paintWhite : paintGray); + isWhite = !isWhite; + } + verticalStartWhite = !verticalStartWhite; + } + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java new file mode 100644 index 00000000..8e960d54 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPaletteAdapter.java @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; + +import androidx.core.graphics.ColorUtils; + +class ColorPaletteAdapter extends BaseAdapter { + + /*package*/ final OnColorSelectedListener listener; + /*package*/ final int[] colors; + /*package*/ int selectedPosition; + /*package*/ int colorShape; + + ColorPaletteAdapter(OnColorSelectedListener listener, int[] colors, int selectedPosition, + @ColorShape int colorShape) { + this.listener = listener; + this.colors = colors; + this.selectedPosition = selectedPosition; + this.colorShape = colorShape; + } + + @Override + public int getCount() { + return colors.length; + } + + @Override + public Object getItem(int position) { + return colors[position]; + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + final ViewHolder holder; + if (convertView == null) { + holder = new ViewHolder(parent.getContext()); + convertView = holder.view; + } else { + holder = (ViewHolder) convertView.getTag(); + } + holder.setup(position); + return convertView; + } + + void selectNone() { + selectedPosition = -1; + notifyDataSetChanged(); + } + + interface OnColorSelectedListener { + + void onColorSelected(int color); + } + + private final class ViewHolder { + + View view; + ColorPanelView colorPanelView; + ImageView imageView; + int originalBorderColor; + + ViewHolder(Context context) { + int layoutResId; + if (colorShape == ColorShape.SQUARE) { + layoutResId = R.layout.cpv_color_item_square; + } else { + layoutResId = R.layout.cpv_color_item_circle; + } + view = View.inflate(context, layoutResId, null); + colorPanelView = view.findViewById(R.id.cpv_color_panel_view); + imageView = view.findViewById(R.id.cpv_color_image_view); + originalBorderColor = colorPanelView.getBorderColor(); + view.setTag(this); + } + + void setup(int position) { + int color = colors[position]; + int alpha = Color.alpha(color); + colorPanelView.setColor(color); + imageView.setImageResource(selectedPosition == position ? R.drawable.cpv_preset_checked : 0); + if (alpha != 255) { + if (alpha <= ColorPickerDialog.ALPHA_THRESHOLD) { + colorPanelView.setBorderColor(color | 0xFF000000); + imageView.setColorFilter(/*color | 0xFF000000*/Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + colorPanelView.setBorderColor(originalBorderColor); + imageView.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + } + } else { + setColorFilter(position); + } + setOnClickListener(position); + } + + private void setOnClickListener(final int position) { + colorPanelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (selectedPosition != position) { + selectedPosition = position; + notifyDataSetChanged(); + } + listener.onColorSelected(colors[position]); + } + }); + colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + colorPanelView.showHint(); + return true; + } + }); + } + + private void setColorFilter(int position) { + if (position == selectedPosition && ColorUtils.calculateLuminance(colors[position]) >= 0.65) { + imageView.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + imageView.setColorFilter(null); + } + } + } +} \ No newline at end of file diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java new file mode 100644 index 00000000..ad047682 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPanelView.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.drawable.BitmapDrawable; +import android.graphics.drawable.Drawable; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.View; +import android.widget.Toast; + +import androidx.annotation.ColorInt; +import androidx.core.view.GravityCompat; +import androidx.core.view.ViewCompat; + +import java.util.Locale; + +/** + * This class draws a panel which which will be filled with a color which can be set. It can be used to show the + * currently selected color which you will get from the {@link ColorPickerView}. + */ +public class ColorPanelView extends View { + + private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E; + + private Drawable alphaPattern; + private Paint borderPaint; + private Paint colorPaint; + private Paint alphaPaint; + private Paint originalPaint; + private Rect drawingRect; + private Rect colorRect; + private RectF centerRect = new RectF(); + private boolean showOldColor; + + /* The width in pixels of the border surrounding the color panel. */ + private int borderWidthPx; + private int borderColor = DEFAULT_BORDER_COLOR; + private int color = Color.BLACK; + private int shape; + + public ColorPanelView(Context context) { + this(context, null); + } + + public ColorPanelView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPanelView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle state = new Bundle(); + state.putParcelable("instanceState", super.onSaveInstanceState()); + state.putInt("color", color); + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + color = bundle.getInt("color"); + state = bundle.getParcelable("instanceState"); + } + super.onRestoreInstanceState(state); + } + + private void init(Context context, AttributeSet attrs) { + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPanelView); + shape = a.getInt(R.styleable.ColorPanelView_cpv_colorShape, ColorShape.CIRCLE); + showOldColor = a.getBoolean(R.styleable.ColorPanelView_cpv_showOldColor, false); + if (showOldColor && shape != ColorShape.CIRCLE) { + throw new IllegalStateException("Color preview is only available in circle mode"); + } + borderColor = a.getColor(R.styleable.ColorPanelView_cpv_borderColor, DEFAULT_BORDER_COLOR); + a.recycle(); + if (borderColor == DEFAULT_BORDER_COLOR) { + // If no specific border color has been set we take the default secondary text color as border/slider color. + // Thus it will adopt to theme changes automatically. + final TypedValue value = new TypedValue(); + TypedArray typedArray = + context.obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorSecondary}); + borderColor = typedArray.getColor(0, borderColor); + typedArray.recycle(); + } + borderWidthPx = DrawingUtils.dpToPx(context, 1); + borderPaint = new Paint(); + borderPaint.setAntiAlias(true); + colorPaint = new Paint(); + colorPaint.setAntiAlias(true); + if (showOldColor) { + originalPaint = new Paint(); + } + if (shape == ColorShape.CIRCLE) { + Bitmap bitmap = ((BitmapDrawable) context.getResources().getDrawable(R.drawable.cpv_alpha)).getBitmap(); + alphaPaint = new Paint(); + alphaPaint.setAntiAlias(true); + BitmapShader shader = new BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT); + alphaPaint.setShader(shader); + } + } + + @Override + protected void onDraw(Canvas canvas) { + borderPaint.setColor(borderColor); + colorPaint.setColor(color); + if (shape == ColorShape.SQUARE) { + if (borderWidthPx > 0) { + canvas.drawRect(drawingRect, borderPaint); + } + if (alphaPattern != null) { + alphaPattern.draw(canvas); + } + canvas.drawRect(colorRect, colorPaint); + } else if (shape == ColorShape.CIRCLE) { + final int outerRadius = getMeasuredWidth() / 2; + if (borderWidthPx > 0) { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius, borderPaint); + } + if (Color.alpha(color) < 255) { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius - borderWidthPx, alphaPaint); + } + if (showOldColor) { + canvas.drawArc(centerRect, 90, 180, true, originalPaint); + canvas.drawArc(centerRect, 270, 180, true, colorPaint); + } else { + canvas.drawCircle(getMeasuredWidth() / 2, getMeasuredHeight() / 2, outerRadius - borderWidthPx, colorPaint); + } + } + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + if (shape == ColorShape.SQUARE) { + int width = MeasureSpec.getSize(widthMeasureSpec); + int height = MeasureSpec.getSize(heightMeasureSpec); + setMeasuredDimension(width, height); + } else if (shape == ColorShape.CIRCLE) { + super.onMeasure(widthMeasureSpec, widthMeasureSpec); + setMeasuredDimension(getMeasuredWidth(), getMeasuredWidth()); + } else { + super.onMeasure(widthMeasureSpec, heightMeasureSpec); + } + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + if (shape == ColorShape.SQUARE || showOldColor) { + drawingRect = new Rect(); + drawingRect.left = getPaddingLeft(); + drawingRect.right = w - getPaddingRight(); + drawingRect.top = getPaddingTop(); + drawingRect.bottom = h - getPaddingBottom(); + if (showOldColor) { + setUpCenterRect(); + } else { + setUpColorRect(); + } + } + } + + private void setUpCenterRect() { + final Rect dRect = drawingRect; + int left = dRect.left + borderWidthPx; + int top = dRect.top + borderWidthPx; + int bottom = dRect.bottom - borderWidthPx; + int right = dRect.right - borderWidthPx; + centerRect = new RectF(left, top, right, bottom); + } + + private void setUpColorRect() { + final Rect dRect = drawingRect; + int left = dRect.left + borderWidthPx; + int top = dRect.top + borderWidthPx; + int bottom = dRect.bottom - borderWidthPx; + int right = dRect.right - borderWidthPx; + colorRect = new Rect(left, top, right, bottom); + alphaPattern = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4)); + alphaPattern.setBounds(Math.round(colorRect.left), Math.round(colorRect.top), Math.round(colorRect.right), + Math.round(colorRect.bottom)); + } + + /** + * Get the color currently show by this view. + * + * @return the color value + */ + public int getColor() { + return color; + } + + /** + * Set the color that should be shown by this view. + * + * @param color the color value + */ + public void setColor(int color) { + this.color = color; + invalidate(); + } + + /** + * Set the original color. This is only used for previewing colors. + * + * @param color The original color + */ + public void setOriginalColor(@ColorInt int color) { + if (originalPaint != null) { + originalPaint.setColor(color); + } + } + + /** + * @return the color of the border surrounding the panel. + */ + public int getBorderColor() { + return borderColor; + } + + /** + * Set the color of the border surrounding the panel. + * + * @param color the color value + */ + public void setBorderColor(int color) { + borderColor = color; + invalidate(); + } + + /** + * Get the shape + * + * @return Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}. + */ + @ColorShape + public int getShape() { + return shape; + } + + /** + * Set the shape. + * + * @param shape Either {@link ColorShape#SQUARE} or {@link ColorShape#CIRCLE}. + */ + public void setShape(@ColorShape int shape) { + this.shape = shape; + invalidate(); + } + + /** + * Show a toast message with the hex color code below the view. + */ + public void showHint() { + final int[] screenPos = new int[2]; + final Rect displayFrame = new Rect(); + getLocationOnScreen(screenPos); + getWindowVisibleDisplayFrame(displayFrame); + final Context context = getContext(); + final int width = getWidth(); + final int height = getHeight(); + final int midy = screenPos[1] + height / 2; + int referenceX = screenPos[0] + width / 2; + if (ViewCompat.getLayoutDirection(this) == ViewCompat.LAYOUT_DIRECTION_LTR) { + final int screenWidth = context.getResources().getDisplayMetrics().widthPixels; + referenceX = screenWidth - referenceX; // mirror + } + StringBuilder hint = new StringBuilder("#"); + if (Color.alpha(color) != 255) { + hint.append(Integer.toHexString(color).toUpperCase(Locale.ENGLISH)); + } else { + hint.append(String.format("%06X", 0xFFFFFF & color).toUpperCase(Locale.ENGLISH)); + } + Toast cheatSheet = Toast.makeText(context, hint.toString(), Toast.LENGTH_SHORT); + if (midy < displayFrame.height()) { + // Show along the top; follow action buttons + cheatSheet.setGravity(Gravity.TOP | GravityCompat.END, referenceX, screenPos[1] + height - displayFrame.top); + } else { + // Show along the bottom center + cheatSheet.setGravity(Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL, 0, height); + } + cheatSheet.show(); + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java new file mode 100644 index 00000000..7cbac400 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialog.java @@ -0,0 +1,968 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.app.Activity; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.graphics.PorterDuff; +import android.os.Bundle; +import android.text.Editable; +import android.text.InputFilter; +import android.text.TextWatcher; +import android.util.Log; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; +import android.view.View.OnTouchListener; +import android.view.ViewGroup; +import android.view.WindowManager; +import android.view.inputmethod.InputMethodManager; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.GridView; +import android.widget.ImageView; +import android.widget.LinearLayout; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.ColorInt; +import androidx.annotation.IntDef; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; +import androidx.appcompat.app.AlertDialog; +import androidx.core.graphics.ColorUtils; +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.FragmentActivity; + +import java.util.Arrays; +import java.util.Locale; + +/** + *

A dialog to pick a color.

+ * + *

The {@link Activity activity} that shows this dialog should implement {@link ColorPickerDialogListener}

+ * + *

Example usage:

+ * + *
+ *   ColorPickerDialog.newBuilder().show(activity);
+ * 
+ */ +public class ColorPickerDialog extends DialogFragment implements ColorPickerView.OnColorChangedListener, TextWatcher { + + public static final int TYPE_CUSTOM = 0; + public static final int TYPE_PRESETS = 1; + /** + * Material design colors used as the default color presets + */ + public static final int[] MATERIAL_COLORS = { + 0xFFF44336, // RED 500 + 0xFFE91E63, // PINK 500 + 0xFFFF2C93, // LIGHT PINK 500 + 0xFF9C27B0, // PURPLE 500 + 0xFF673AB7, // DEEP PURPLE 500 + 0xFF3F51B5, // INDIGO 500 + 0xFF2196F3, // BLUE 500 + 0xFF03A9F4, // LIGHT BLUE 500 + 0xFF00BCD4, // CYAN 500 + 0xFF009688, // TEAL 500 + 0xFF4CAF50, // GREEN 500 + 0xFF8BC34A, // LIGHT GREEN 500 + 0xFFCDDC39, // LIME 500 + 0xFFFFEB3B, // YELLOW 500 + 0xFFFFC107, // AMBER 500 + 0xFFFF9800, // ORANGE 500 + 0xFF795548, // BROWN 500 + 0xFF607D8B, // BLUE GREY 500 + 0xFF9E9E9E, // GREY 500 + }; + static final int ALPHA_THRESHOLD = 165; + private static final String TAG = "ColorPickerDialog"; + private static final String ARG_ID = "id"; + private static final String ARG_TYPE = "dialogType"; + private static final String ARG_COLOR = "color"; + private static final String ARG_ALPHA = "alpha"; + private static final String ARG_PRESETS = "presets"; + private static final String ARG_ALLOW_PRESETS = "allowPresets"; + private static final String ARG_ALLOW_CUSTOM = "allowCustom"; + private static final String ARG_DIALOG_TITLE = "dialogTitle"; + private static final String ARG_SHOW_COLOR_SHADES = "showColorShades"; + private static final String ARG_COLOR_SHAPE = "colorShape"; + private static final String ARG_PRESETS_BUTTON_TEXT = "presetsButtonText"; + private static final String ARG_CUSTOM_BUTTON_TEXT = "customButtonText"; + private static final String ARG_SELECTED_BUTTON_TEXT = "selectedButtonText"; + + ColorPickerDialogListener colorPickerDialogListener; + FrameLayout rootView; + int[] presets; + @ColorInt + int color; + int dialogType; + int dialogId; + boolean showColorShades; + int colorShape; + + // -- PRESETS -------------------------- + ColorPaletteAdapter adapter; + LinearLayout shadesLayout; + SeekBar transparencySeekBar; + TextView transparencyPercText; + + // -- CUSTOM --------------------------- + ColorPickerView colorPicker; + ColorPanelView newColorPanel; + EditText hexEditText; + private final OnTouchListener onPickerTouchListener = new OnTouchListener() { + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v != hexEditText && hexEditText.hasFocus()) { + hexEditText.clearFocus(); + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); + hexEditText.clearFocus(); + return true; + } + return false; + } + }; + boolean showAlphaSlider; + private int presetsButtonStringRes; + private boolean fromEditText; + private int customButtonStringRes; + + /** + * Create a new Builder for creating a {@link ColorPickerDialog} instance + * + * @return The {@link Builder builder} to create the {@link ColorPickerDialog}. + */ + public static Builder newBuilder() { + return new Builder(); + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + dialogId = getArguments().getInt(ARG_ID); + showAlphaSlider = getArguments().getBoolean(ARG_ALPHA); + showColorShades = getArguments().getBoolean(ARG_SHOW_COLOR_SHADES); + colorShape = getArguments().getInt(ARG_COLOR_SHAPE); + if (savedInstanceState == null) { + color = getArguments().getInt(ARG_COLOR); + dialogType = getArguments().getInt(ARG_TYPE); + } else { + color = savedInstanceState.getInt(ARG_COLOR); + dialogType = savedInstanceState.getInt(ARG_TYPE); + } + + rootView = new FrameLayout(requireActivity()); + if (dialogType == TYPE_CUSTOM) { + rootView.addView(createPickerView()); + } else if (dialogType == TYPE_PRESETS) { + rootView.addView(createPresetsView()); + } + + int selectedButtonStringRes = getArguments().getInt(ARG_SELECTED_BUTTON_TEXT); + if (selectedButtonStringRes == 0) { + selectedButtonStringRes = R.string.cpv_select; + } + + AlertDialog.Builder builder = new AlertDialog.Builder(requireActivity()).setView(rootView) + .setPositiveButton(selectedButtonStringRes, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + onColorSelected(color); + } + }); + + int dialogTitleStringRes = getArguments().getInt(ARG_DIALOG_TITLE); + if (dialogTitleStringRes != 0) { + builder.setTitle(dialogTitleStringRes); + } + + presetsButtonStringRes = getArguments().getInt(ARG_PRESETS_BUTTON_TEXT); + customButtonStringRes = getArguments().getInt(ARG_CUSTOM_BUTTON_TEXT); + + int neutralButtonStringRes; + if (dialogType == TYPE_CUSTOM && getArguments().getBoolean(ARG_ALLOW_PRESETS)) { + neutralButtonStringRes = (presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); + } else if (dialogType == TYPE_PRESETS && getArguments().getBoolean(ARG_ALLOW_CUSTOM)) { + neutralButtonStringRes = (customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); + } else { + neutralButtonStringRes = 0; + } + + if (neutralButtonStringRes != 0) { + builder.setNeutralButton(neutralButtonStringRes, null); + } + + return builder.create(); + } + + @Override + public void onStart() { + super.onStart(); + AlertDialog dialog = (AlertDialog) getDialog(); + + // http://stackoverflow.com/a/16972670/1048340 + //noinspection ConstantConditions + dialog.getWindow() + .clearFlags(WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM); + dialog.getWindow().setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE); + + // Do not dismiss the dialog when clicking the neutral button. + Button neutralButton = dialog.getButton(AlertDialog.BUTTON_NEUTRAL); + if (neutralButton != null) { + neutralButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + rootView.removeAllViews(); + switch (dialogType) { + case TYPE_CUSTOM: + dialogType = TYPE_PRESETS; + ((Button) v).setText(customButtonStringRes != 0 ? customButtonStringRes : R.string.cpv_custom); + rootView.addView(createPresetsView()); + break; + case TYPE_PRESETS: + dialogType = TYPE_CUSTOM; + ((Button) v).setText(presetsButtonStringRes != 0 ? presetsButtonStringRes : R.string.cpv_presets); + rootView.addView(createPickerView()); + } + } + }); + } + } + + @Override + public void onDismiss(DialogInterface dialog) { + super.onDismiss(dialog); + onDialogDismissed(); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(ARG_COLOR, color); + outState.putInt(ARG_TYPE, dialogType); + super.onSaveInstanceState(outState); + } + + /** + * Set the callback. + *

+ * Note: The preferred way to handle the callback is to have the calling Activity implement + * {@link ColorPickerDialogListener} as this will not survive an orientation change. + * + * @param colorPickerDialogListener The callback invoked when a color is selected or the dialog is dismissed. + */ + public void setColorPickerDialogListener(ColorPickerDialogListener colorPickerDialogListener) { + this.colorPickerDialogListener = colorPickerDialogListener; + } + + // region Custom Picker + + View createPickerView() { + View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_color_picker, null); + colorPicker = contentView.findViewById(R.id.cpv_color_picker_view); + ColorPanelView oldColorPanel = contentView.findViewById(R.id.cpv_color_panel_old); + newColorPanel = contentView.findViewById(R.id.cpv_color_panel_new); + ImageView arrowRight = contentView.findViewById(R.id.cpv_arrow_right); + hexEditText = contentView.findViewById(R.id.cpv_hex); + + try { + final TypedValue value = new TypedValue(); + TypedArray typedArray = + getActivity().obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorPrimary}); + int arrowColor = typedArray.getColor(0, Color.BLACK); + typedArray.recycle(); + arrowRight.setColorFilter(arrowColor); + } catch (Exception ignored) { + } + + colorPicker.setAlphaSliderVisible(showAlphaSlider); + oldColorPanel.setColor(getArguments().getInt(ARG_COLOR)); + colorPicker.setColor(color, true); + newColorPanel.setColor(color); + setHex(color); + + if (!showAlphaSlider) { + hexEditText.setFilters(new InputFilter[]{new InputFilter.LengthFilter(6)}); + } + + newColorPanel.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (newColorPanel.getColor() == color) { + onColorSelected(color); + dismiss(); + } + } + }); + + contentView.setOnTouchListener(onPickerTouchListener); + colorPicker.setOnColorChangedListener(this); + hexEditText.addTextChangedListener(this); + + hexEditText.setOnFocusChangeListener(new View.OnFocusChangeListener() { + @Override + public void onFocusChange(View v, boolean hasFocus) { + if (hasFocus) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.showSoftInput(hexEditText, InputMethodManager.SHOW_IMPLICIT); + } + } + }); + + return contentView; + } + + @Override + public void onColorChanged(int newColor) { + color = newColor; + if (newColorPanel != null) { + newColorPanel.setColor(newColor); + } + if (!fromEditText && hexEditText != null) { + setHex(newColor); + if (hexEditText.hasFocus()) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(hexEditText.getWindowToken(), 0); + hexEditText.clearFocus(); + } + } + fromEditText = false; + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) { + + } + + @Override + public void afterTextChanged(Editable s) { + if (hexEditText.isFocused()) { + int color = parseColorString(s.toString()); + if (color != colorPicker.getColor()) { + fromEditText = true; + colorPicker.setColor(color, true); + } + } + } + + private void setHex(int color) { + if (showAlphaSlider) { + hexEditText.setText(String.format("%08X", (color))); + } else { + hexEditText.setText(String.format("%06X", (0xFFFFFF & color))); + } + } + + private int parseColorString(String colorString) throws NumberFormatException { + int a, r, g, b = 0; + if (colorString.startsWith("#")) { + colorString = colorString.substring(1); + } + if (colorString.length() == 0) { + r = 0; + a = 255; + g = 0; + } else if (colorString.length() <= 2) { + a = 255; + r = 0; + b = Integer.parseInt(colorString, 16); + g = 0; + } else if (colorString.length() == 3) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 1), 16); + g = Integer.parseInt(colorString.substring(1, 2), 16); + b = Integer.parseInt(colorString.substring(2, 3), 16); + } else if (colorString.length() == 4) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 2), 16); + g = r; + r = 0; + b = Integer.parseInt(colorString.substring(2, 4), 16); + } else if (colorString.length() == 5) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 1), 16); + g = Integer.parseInt(colorString.substring(1, 3), 16); + b = Integer.parseInt(colorString.substring(3, 5), 16); + } else if (colorString.length() == 6) { + a = 255; + r = Integer.parseInt(colorString.substring(0, 2), 16); + g = Integer.parseInt(colorString.substring(2, 4), 16); + b = Integer.parseInt(colorString.substring(4, 6), 16); + } else if (colorString.length() == 7) { + a = Integer.parseInt(colorString.substring(0, 1), 16); + r = Integer.parseInt(colorString.substring(1, 3), 16); + g = Integer.parseInt(colorString.substring(3, 5), 16); + b = Integer.parseInt(colorString.substring(5, 7), 16); + } else if (colorString.length() == 8) { + a = Integer.parseInt(colorString.substring(0, 2), 16); + r = Integer.parseInt(colorString.substring(2, 4), 16); + g = Integer.parseInt(colorString.substring(4, 6), 16); + b = Integer.parseInt(colorString.substring(6, 8), 16); + } else { + b = -1; + g = -1; + r = -1; + a = -1; + } + return Color.argb(a, r, g, b); + } + + // -- endregion -- + + // region Presets Picker + + View createPresetsView() { + View contentView = View.inflate(getActivity(), R.layout.cpv_dialog_presets, null); + shadesLayout = contentView.findViewById(R.id.shades_layout); + transparencySeekBar = contentView.findViewById(R.id.transparency_seekbar); + transparencyPercText = contentView.findViewById(R.id.transparency_text); + GridView gridView = contentView.findViewById(R.id.gridView); + + loadPresets(); + + if (showColorShades) { + createColorShades(color); + } else { + shadesLayout.setVisibility(View.GONE); + contentView.findViewById(R.id.shades_divider).setVisibility(View.GONE); + } + + adapter = new ColorPaletteAdapter(new ColorPaletteAdapter.OnColorSelectedListener() { + @Override + public void onColorSelected(int newColor) { + if (color == newColor) { + // Double tab selects the color + ColorPickerDialog.this.onColorSelected(color); + dismiss(); + return; + } + color = newColor; + if (showColorShades) { + createColorShades(color); + } + } + }, presets, getSelectedItemPosition(), colorShape); + + gridView.setAdapter(adapter); + + if (showAlphaSlider) { + setupTransparency(); + } else { + contentView.findViewById(R.id.transparency_layout).setVisibility(View.GONE); + contentView.findViewById(R.id.transparency_title).setVisibility(View.GONE); + } + + return contentView; + } + + private void loadPresets() { + int alpha = Color.alpha(color); + presets = getArguments().getIntArray(ARG_PRESETS); + if (presets == null) presets = MATERIAL_COLORS; + boolean isMaterialColors = presets == MATERIAL_COLORS; + presets = Arrays.copyOf(presets, presets.length); // don't update the original array when modifying alpha + if (alpha != 255) { + // add alpha to the presets + for (int i = 0; i < presets.length; i++) { + int color = presets[i]; + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + presets[i] = Color.argb(alpha, red, green, blue); + } + } + presets = unshiftIfNotExists(presets, color); + int initialColor = getArguments().getInt(ARG_COLOR); + if (initialColor != color) { + // The user clicked a color and a configuration change occurred. Make sure the initial color is in the presets + presets = unshiftIfNotExists(presets, initialColor); + } + if (isMaterialColors && presets.length == 19) { + // Add black to have a total of 20 colors if the current color is in the material color palette + presets = pushIfNotExists(presets, Color.argb(alpha, 0, 0, 0)); + } + } + + void createColorShades(@ColorInt final int color) { + final int[] colorShades = getColorShades(color); + + if (shadesLayout.getChildCount() != 0) { + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + final ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + cpv.setColor(colorShades[i]); + cpv.setTag(false); + iv.setImageDrawable(null); + } + return; + } + + final int horizontalPadding = getResources().getDimensionPixelSize(R.dimen.cpv_item_horizontal_padding); + + for (final int colorShade : colorShades) { + int layoutResId; + if (colorShape == ColorShape.SQUARE) { + layoutResId = R.layout.cpv_color_item_square; + } else { + layoutResId = R.layout.cpv_color_item_circle; + } + + final View view = View.inflate(getActivity(), layoutResId, null); + final ColorPanelView colorPanelView = view.findViewById(R.id.cpv_color_panel_view); + + ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) colorPanelView.getLayoutParams(); + params.leftMargin = params.rightMargin = horizontalPadding; + colorPanelView.setLayoutParams(params); + colorPanelView.setColor(colorShade); + shadesLayout.addView(view); + + colorPanelView.post(new Runnable() { + @Override + public void run() { + // The color is black when rotating the dialog. This is a dirty fix. WTF!? + colorPanelView.setColor(colorShade); + } + }); + + colorPanelView.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + if (v.getTag() instanceof Boolean && (Boolean) v.getTag()) { + onColorSelected(ColorPickerDialog.this.color); + dismiss(); + return; // already selected + } + ColorPickerDialog.this.color = colorPanelView.getColor(); + adapter.selectNone(); + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + iv.setImageResource(cpv == v ? R.drawable.cpv_preset_checked : 0); + if (cpv == v && ColorUtils.calculateLuminance(cpv.getColor()) >= 0.65 + || Color.alpha(cpv.getColor()) <= ALPHA_THRESHOLD) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + iv.setColorFilter(null); + } + cpv.setTag(cpv == v); + } + } + }); + colorPanelView.setOnLongClickListener(new View.OnLongClickListener() { + @Override + public boolean onLongClick(View v) { + colorPanelView.showHint(); + return true; + } + }); + } + } + + private void onColorSelected(int color) { + if (colorPickerDialogListener != null) { + Log.w(TAG, "Using deprecated listener which may be remove in future releases"); + colorPickerDialogListener.onColorSelected(dialogId, color); + return; + } + Activity activity = getActivity(); + if (activity instanceof ColorPickerDialogListener) { + ((ColorPickerDialogListener) activity).onColorSelected(dialogId, color); + } else { + throw new IllegalStateException("The activity must implement ColorPickerDialogListener"); + } + } + + private void onDialogDismissed() { + if (colorPickerDialogListener != null) { + Log.w(TAG, "Using deprecated listener which may be remove in future releases"); + colorPickerDialogListener.onDialogDismissed(dialogId); + return; + } + Activity activity = getActivity(); + if (activity instanceof ColorPickerDialogListener) { + ((ColorPickerDialogListener) activity).onDialogDismissed(dialogId); + } + } + + private int shadeColor(@ColorInt int color, double percent) { + String hex = String.format("#%06X", (0xFFFFFF & color)); + long f = Long.parseLong(hex.substring(1), 16); + double t = percent < 0 ? 0 : 255; + double p = percent < 0 ? percent * -1 : percent; + long R = f >> 16; + long G = f >> 8 & 0x00FF; + long B = f & 0x0000FF; + int alpha = Color.alpha(color); + int red = (int) (Math.round((t - R) * p) + R); + int green = (int) (Math.round((t - G) * p) + G); + int blue = (int) (Math.round((t - B) * p) + B); + return Color.argb(alpha, red, green, blue); + } + + private int[] getColorShades(@ColorInt int color) { + return new int[]{ + shadeColor(color, 0.9), shadeColor(color, 0.7), shadeColor(color, 0.5), shadeColor(color, 0.333), + shadeColor(color, 0.166), shadeColor(color, -0.125), shadeColor(color, -0.25), shadeColor(color, -0.375), + shadeColor(color, -0.5), shadeColor(color, -0.675), shadeColor(color, -0.7), shadeColor(color, -0.775), + }; + } + + private void setupTransparency() { + int progress = 255 - Color.alpha(color); + transparencySeekBar.setMax(255); + transparencySeekBar.setProgress(progress); + int percentage = (int) ((double) progress * 100 / 255); + transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); + transparencySeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + int percentage = (int) ((double) progress * 100 / 255); + transparencyPercText.setText(String.format(Locale.ENGLISH, "%d%%", percentage)); + int alpha = 255 - progress; + // update items in GridView: + for (int i = 0; i < adapter.colors.length; i++) { + int color = adapter.colors[i]; + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + adapter.colors[i] = Color.argb(alpha, red, green, blue); + } + adapter.notifyDataSetChanged(); + // update shades: + for (int i = 0; i < shadesLayout.getChildCount(); i++) { + FrameLayout layout = (FrameLayout) shadesLayout.getChildAt(i); + ColorPanelView cpv = layout.findViewById(R.id.cpv_color_panel_view); + ImageView iv = layout.findViewById(R.id.cpv_color_image_view); + if (layout.getTag() == null) { + // save the original border color + layout.setTag(cpv.getBorderColor()); + } + int color = cpv.getColor(); + color = Color.argb(alpha, Color.red(color), Color.green(color), Color.blue(color)); + if (alpha <= ALPHA_THRESHOLD) { + cpv.setBorderColor(color | 0xFF000000); + } else { + cpv.setBorderColor((int) layout.getTag()); + } + if (cpv.getTag() != null && (Boolean) cpv.getTag()) { + // The alpha changed on the selected shaded color. Update the checkmark color filter. + if (alpha <= ALPHA_THRESHOLD) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + if (ColorUtils.calculateLuminance(color) >= 0.65) { + iv.setColorFilter(Color.BLACK, PorterDuff.Mode.SRC_IN); + } else { + iv.setColorFilter(Color.WHITE, PorterDuff.Mode.SRC_IN); + } + } + } + cpv.setColor(color); + } + // update color: + int red = Color.red(color); + int green = Color.green(color); + int blue = Color.blue(color); + color = Color.argb(alpha, red, green, blue); + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + }); + } + + private int[] unshiftIfNotExists(int[] array, int value) { + boolean present = false; + for (int i : array) { + if (i == value) { + present = true; + break; + } + } + if (!present) { + int[] newArray = new int[array.length + 1]; + newArray[0] = value; + System.arraycopy(array, 0, newArray, 1, newArray.length - 1); + return newArray; + } + return array; + } + + private int[] pushIfNotExists(int[] array, int value) { + boolean present = false; + for (int i : array) { + if (i == value) { + present = true; + break; + } + } + if (!present) { + int[] newArray = new int[array.length + 1]; + newArray[newArray.length - 1] = value; + System.arraycopy(array, 0, newArray, 0, newArray.length - 1); + return newArray; + } + return array; + } + + private int getSelectedItemPosition() { + for (int i = 0; i < presets.length; i++) { + if (presets[i] == color) { + return i; + } + } + return -1; + } + + // endregion + + // region Builder + + @IntDef({TYPE_CUSTOM, TYPE_PRESETS}) + public @interface DialogType { + + } + + public static final class Builder { + + ColorPickerDialogListener colorPickerDialogListener; + @StringRes + int dialogTitle = R.string.cpv_default_title; + @StringRes + int presetsButtonText = R.string.cpv_presets; + @StringRes + int customButtonText = R.string.cpv_custom; + @StringRes + int selectedButtonText = R.string.cpv_select; + @DialogType + int dialogType = TYPE_PRESETS; + int[] presets = MATERIAL_COLORS; + @ColorInt + int color = Color.BLACK; + int dialogId = 0; + boolean showAlphaSlider = false; + boolean allowPresets = true; + boolean allowCustom = true; + boolean showColorShades = true; + @ColorShape + int colorShape = ColorShape.CIRCLE; + + /*package*/ Builder() { + + } + + /** + * Set the dialog title string resource id + * + * @param dialogTitle The string resource used for the dialog title + * @return This builder object for chaining method calls + */ + public Builder setDialogTitle(@StringRes int dialogTitle) { + this.dialogTitle = dialogTitle; + return this; + } + + /** + * Set the selected button text string resource id + * + * @param selectedButtonText The string resource used for the selected button text + * @return This builder object for chaining method calls + */ + public Builder setSelectedButtonText(@StringRes int selectedButtonText) { + this.selectedButtonText = selectedButtonText; + return this; + } + + /** + * Set the presets button text string resource id + * + * @param presetsButtonText The string resource used for the presets button text + * @return This builder object for chaining method calls + */ + public Builder setPresetsButtonText(@StringRes int presetsButtonText) { + this.presetsButtonText = presetsButtonText; + return this; + } + + /** + * Set the custom button text string resource id + * + * @param customButtonText The string resource used for the custom button text + * @return This builder object for chaining method calls + */ + public Builder setCustomButtonText(@StringRes int customButtonText) { + this.customButtonText = customButtonText; + return this; + } + + /** + * Set which dialog view to show. + * + * @param dialogType Either {@link ColorPickerDialog#TYPE_CUSTOM} or {@link ColorPickerDialog#TYPE_PRESETS}. + * @return This builder object for chaining method calls + */ + public Builder setDialogType(@DialogType int dialogType) { + this.dialogType = dialogType; + return this; + } + + /** + * Set the colors used for the presets + * + * @param presets An array of color ints. + * @return This builder object for chaining method calls + */ + public Builder setPresets(@NonNull int[] presets) { + this.presets = presets; + return this; + } + + /** + * Set the original color + * + * @param color The default color for the color picker + * @return This builder object for chaining method calls + */ + public Builder setColor(int color) { + this.color = color; + return this; + } + + /** + * Set the dialog id used for callbacks + * + * @param dialogId The id that is sent back to the {@link ColorPickerDialogListener}. + * @return This builder object for chaining method calls + */ + public Builder setDialogId(int dialogId) { + this.dialogId = dialogId; + return this; + } + + /** + * Show the alpha slider + * + * @param showAlphaSlider {@code true} to show the alpha slider. Currently only supported with the {@link + * ColorPickerView}. + * @return This builder object for chaining method calls + */ + public Builder setShowAlphaSlider(boolean showAlphaSlider) { + this.showAlphaSlider = showAlphaSlider; + return this; + } + + /** + * Show/Hide a neutral button to select preset colors. + * + * @param allowPresets {@code false} to disable showing the presets button. + * @return This builder object for chaining method calls + */ + public Builder setAllowPresets(boolean allowPresets) { + this.allowPresets = allowPresets; + return this; + } + + /** + * Show/Hide the neutral button to select a custom color. + * + * @param allowCustom {@code false} to disable showing the custom button. + * @return This builder object for chaining method calls + */ + public Builder setAllowCustom(boolean allowCustom) { + this.allowCustom = allowCustom; + return this; + } + + /** + * Show/Hide the color shades in the presets picker + * + * @param showColorShades {@code false} to hide the color shades. + * @return This builder object for chaining method calls + */ + public Builder setShowColorShades(boolean showColorShades) { + this.showColorShades = showColorShades; + return this; + } + + /** + * Set the shape of the color panel view. + * + * @param colorShape Either {@link ColorShape#CIRCLE} or {@link ColorShape#SQUARE}. + * @return This builder object for chaining method calls + */ + public Builder setColorShape(int colorShape) { + this.colorShape = colorShape; + return this; + } + + /** + * Create the {@link ColorPickerDialog} instance. + * + * @return A new {@link ColorPickerDialog}. + * @see #show(FragmentActivity) + */ + public ColorPickerDialog create() { + ColorPickerDialog dialog = new ColorPickerDialog(); + Bundle args = new Bundle(); + args.putInt(ARG_ID, dialogId); + args.putInt(ARG_TYPE, dialogType); + args.putInt(ARG_COLOR, color); + args.putIntArray(ARG_PRESETS, presets); + args.putBoolean(ARG_ALPHA, showAlphaSlider); + args.putBoolean(ARG_ALLOW_CUSTOM, allowCustom); + args.putBoolean(ARG_ALLOW_PRESETS, allowPresets); + args.putInt(ARG_DIALOG_TITLE, dialogTitle); + args.putBoolean(ARG_SHOW_COLOR_SHADES, showColorShades); + args.putInt(ARG_COLOR_SHAPE, colorShape); + args.putInt(ARG_PRESETS_BUTTON_TEXT, presetsButtonText); + args.putInt(ARG_CUSTOM_BUTTON_TEXT, customButtonText); + args.putInt(ARG_SELECTED_BUTTON_TEXT, selectedButtonText); + dialog.setArguments(args); + return dialog; + } + + /** + * Create and show the {@link ColorPickerDialog} created with this builder. + * + * @param activity The current activity. + */ + public void show(FragmentActivity activity) { + create().show(activity.getSupportFragmentManager(), "color-picker-dialog"); + } + } + + // endregion +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialogListener.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialogListener.java new file mode 100644 index 00000000..1bd0cabe --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerDialogListener.java @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import androidx.annotation.ColorInt; + +/** + * Callback used for getting the selected color from a color picker dialog. + */ +public interface ColorPickerDialogListener { + + /** + * Callback that is invoked when a color is selected from the color picker dialog. + * + * @param dialogId The dialog id used to create the dialog instance. + * @param color The selected color + */ + void onColorSelected(int dialogId, @ColorInt int color); + + /** + * Callback that is invoked when the color picker dialog was dismissed. + * + * @param dialogId The dialog id used to create the dialog instance. + */ + void onDialogDismissed(int dialogId); +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerView.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerView.java new file mode 100644 index 00000000..cfb5cb32 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPickerView.java @@ -0,0 +1,972 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Bitmap; +import android.graphics.Bitmap.Config; +import android.graphics.Canvas; +import android.graphics.Color; +import android.graphics.ComposeShader; +import android.graphics.LinearGradient; +import android.graphics.Paint; +import android.graphics.Paint.Align; +import android.graphics.Paint.Style; +import android.graphics.Point; +import android.graphics.PorterDuff; +import android.graphics.Rect; +import android.graphics.RectF; +import android.graphics.Shader; +import android.graphics.Shader.TileMode; +import android.os.Bundle; +import android.os.Parcelable; +import android.util.AttributeSet; +import android.util.TypedValue; +import android.view.MotionEvent; +import android.view.View; + +/** + * Displays a color picker to the user and allow them to select a color. A slider for the alpha channel is also + * available. + * Enable it by setting setAlphaSliderVisible(boolean) to true. + */ +public class ColorPickerView extends View { + + private final static int DEFAULT_BORDER_COLOR = 0xFF6E6E6E; + private final static int DEFAULT_SLIDER_COLOR = 0xFFBDBDBD; + + private final static int HUE_PANEL_WDITH_DP = 30; + private final static int ALPHA_PANEL_HEIGH_DP = 20; + private final static int PANEL_SPACING_DP = 10; + private final static int CIRCLE_TRACKER_RADIUS_DP = 5; + private final static int SLIDER_TRACKER_SIZE_DP = 4; + private final static int SLIDER_TRACKER_OFFSET_DP = 2; + + /** + * The width in pixels of the border + * surrounding all color panels. + */ + private final static int BORDER_WIDTH_PX = 1; + + /** + * The width in px of the hue panel. + */ + private int huePanelWidthPx; + /** + * The height in px of the alpha panel + */ + private int alphaPanelHeightPx; + /** + * The distance in px between the different + * color panels. + */ + private int panelSpacingPx; + /** + * The radius in px of the color palette tracker circle. + */ + private int circleTrackerRadiusPx; + /** + * The px which the tracker of the hue or alpha panel + * will extend outside of its bounds. + */ + private int sliderTrackerOffsetPx; + /** + * Height of slider tracker on hue panel, + * width of slider on alpha panel. + */ + private int sliderTrackerSizePx; + + private Paint satValPaint; + private Paint satValTrackerPaint; + + private Paint alphaPaint; + private Paint alphaTextPaint; + private Paint hueAlphaTrackerPaint; + + private Paint borderPaint; + + private Shader valShader; + private Shader satShader; + private Shader alphaShader; + + /* + * We cache a bitmap of the sat/val panel which is expensive to draw each time. + * We can reuse it when the user is sliding the circle picker as long as the hue isn't changed. + */ + private BitmapCache satValBackgroundCache; + /* We cache the hue background to since its also very expensive now. */ + private BitmapCache hueBackgroundCache; + + /* Current values */ + private int alpha = 0xff; + private float hue = 360f; + private float sat = 0f; + private float val = 0f; + + private boolean showAlphaPanel = false; + private String alphaSliderText = null; + private int sliderTrackerColor = DEFAULT_SLIDER_COLOR; + private int borderColor = DEFAULT_BORDER_COLOR; + + /** + * Minimum required padding. The offset from the + * edge we must have or else the finger tracker will + * get clipped when it's drawn outside of the view. + */ + private int mRequiredPadding; + + /** + * The Rect in which we are allowed to draw. + * Trackers can extend outside slightly, + * due to the required padding we have set. + */ + private Rect drawingRect; + + private Rect satValRect; + private Rect hueRect; + private Rect alphaRect; + + private Point startTouchPoint = null; + + private AlphaPatternDrawable alphaPatternDrawable; + private OnColorChangedListener onColorChangedListener; + + public ColorPickerView(Context context) { + this(context, null); + } + + public ColorPickerView(Context context, AttributeSet attrs) { + this(context, attrs, 0); + } + + public ColorPickerView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(context, attrs); + } + + @Override + public Parcelable onSaveInstanceState() { + Bundle state = new Bundle(); + state.putParcelable("instanceState", super.onSaveInstanceState()); + state.putInt("alpha", alpha); + state.putFloat("hue", hue); + state.putFloat("sat", sat); + state.putFloat("val", val); + state.putBoolean("show_alpha", showAlphaPanel); + state.putString("alpha_text", alphaSliderText); + + return state; + } + + @Override + public void onRestoreInstanceState(Parcelable state) { + + if (state instanceof Bundle) { + Bundle bundle = (Bundle) state; + + alpha = bundle.getInt("alpha"); + hue = bundle.getFloat("hue"); + sat = bundle.getFloat("sat"); + val = bundle.getFloat("val"); + showAlphaPanel = bundle.getBoolean("show_alpha"); + alphaSliderText = bundle.getString("alpha_text"); + + state = bundle.getParcelable("instanceState"); + } + super.onRestoreInstanceState(state); + } + + private void init(Context context, AttributeSet attrs) { + //Load those if set in xml resource file. + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPickerView); + showAlphaPanel = a.getBoolean(R.styleable.ColorPickerView_cpv_alphaChannelVisible, false); + alphaSliderText = a.getString(R.styleable.ColorPickerView_cpv_alphaChannelText); + sliderTrackerColor = a.getColor(R.styleable.ColorPickerView_cpv_sliderColor, 0xFFBDBDBD); + borderColor = a.getColor(R.styleable.ColorPickerView_cpv_borderColor, 0xFF6E6E6E); + a.recycle(); + + applyThemeColors(context); + + huePanelWidthPx = DrawingUtils.dpToPx(getContext(), HUE_PANEL_WDITH_DP); + alphaPanelHeightPx = DrawingUtils.dpToPx(getContext(), ALPHA_PANEL_HEIGH_DP); + panelSpacingPx = DrawingUtils.dpToPx(getContext(), PANEL_SPACING_DP); + circleTrackerRadiusPx = DrawingUtils.dpToPx(getContext(), CIRCLE_TRACKER_RADIUS_DP); + sliderTrackerSizePx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_SIZE_DP); + sliderTrackerOffsetPx = DrawingUtils.dpToPx(getContext(), SLIDER_TRACKER_OFFSET_DP); + + mRequiredPadding = getResources().getDimensionPixelSize(R.dimen.cpv_required_padding); + + initPaintTools(); + + //Needed for receiving trackball motion events. + setFocusable(true); + setFocusableInTouchMode(true); + } + + private void applyThemeColors(Context c) { + // If no specific border/slider color has been + // set we take the default secondary text color + // as border/slider color. Thus it will adopt + // to theme changes automatically. + + final TypedValue value = new TypedValue(); + TypedArray a = c.obtainStyledAttributes(value.data, new int[]{android.R.attr.textColorSecondary}); + + if (borderColor == DEFAULT_BORDER_COLOR) { + borderColor = a.getColor(0, DEFAULT_BORDER_COLOR); + } + + if (sliderTrackerColor == DEFAULT_SLIDER_COLOR) { + sliderTrackerColor = a.getColor(0, DEFAULT_SLIDER_COLOR); + } + + a.recycle(); + } + + private void initPaintTools() { + + satValPaint = new Paint(); + satValTrackerPaint = new Paint(); + hueAlphaTrackerPaint = new Paint(); + alphaPaint = new Paint(); + alphaTextPaint = new Paint(); + borderPaint = new Paint(); + + satValTrackerPaint.setStyle(Style.STROKE); + satValTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2)); + satValTrackerPaint.setAntiAlias(true); + + hueAlphaTrackerPaint.setColor(sliderTrackerColor); + hueAlphaTrackerPaint.setStyle(Style.STROKE); + hueAlphaTrackerPaint.setStrokeWidth(DrawingUtils.dpToPx(getContext(), 2)); + hueAlphaTrackerPaint.setAntiAlias(true); + + alphaTextPaint.setColor(0xff1c1c1c); + alphaTextPaint.setTextSize(DrawingUtils.dpToPx(getContext(), 14)); + alphaTextPaint.setAntiAlias(true); + alphaTextPaint.setTextAlign(Align.CENTER); + alphaTextPaint.setFakeBoldText(true); + } + + @Override + protected void onDraw(Canvas canvas) { + if (drawingRect.width() <= 0 || drawingRect.height() <= 0) { + return; + } + + drawSatValPanel(canvas); + drawHuePanel(canvas); + drawAlphaPanel(canvas); + } + + private void drawSatValPanel(Canvas canvas) { + final Rect rect = satValRect; + + if (BORDER_WIDTH_PX > 0) { + borderPaint.setColor(borderColor); + canvas.drawRect(drawingRect.left, drawingRect.top, rect.right + BORDER_WIDTH_PX, rect.bottom + BORDER_WIDTH_PX, + borderPaint); + } + + if (valShader == null) { + //Black gradient has either not been created or the view has been resized. + valShader = + new LinearGradient(rect.left, rect.top, rect.left, rect.bottom, 0xffffffff, 0xff000000, TileMode.CLAMP); + } + + //If the hue has changed we need to recreate the cache. + if (satValBackgroundCache == null || satValBackgroundCache.value != hue) { + + if (satValBackgroundCache == null) { + satValBackgroundCache = new BitmapCache(); + } + + //We create our bitmap in the cache if it doesn't exist. + if (satValBackgroundCache.bitmap == null) { + satValBackgroundCache.bitmap = Bitmap.createBitmap(rect.width(), rect.height(), Config.ARGB_8888); + } + + //We create the canvas once so we can draw on our bitmap and the hold on to it. + if (satValBackgroundCache.canvas == null) { + satValBackgroundCache.canvas = new Canvas(satValBackgroundCache.bitmap); + } + + int rgb = Color.HSVToColor(new float[]{hue, 1f, 1f}); + + satShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, 0xffffffff, rgb, TileMode.CLAMP); + + ComposeShader mShader = new ComposeShader(valShader, satShader, PorterDuff.Mode.MULTIPLY); + satValPaint.setShader(mShader); + + // Finally we draw on our canvas, the result will be + // stored in our bitmap which is already in the cache. + // Since this is drawn on a canvas not rendered on + // screen it will automatically not be using the + // hardware acceleration. And this was the code that + // wasn't supported by hardware acceleration which mean + // there is no need to turn it of anymore. The rest of + // the view will still be hw accelerated. + satValBackgroundCache.canvas.drawRect(0, 0, satValBackgroundCache.bitmap.getWidth(), + satValBackgroundCache.bitmap.getHeight(), satValPaint); + + //We set the hue value in our cache to which hue it was drawn with, + //then we know that if it hasn't changed we can reuse our cached bitmap. + satValBackgroundCache.value = hue; + } + + // We draw our bitmap from the cached, if the hue has changed + // then it was just recreated otherwise the old one will be used. + canvas.drawBitmap(satValBackgroundCache.bitmap, null, rect, null); + + Point p = satValToPoint(sat, val); + + satValTrackerPaint.setColor(0xff000000); + canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx - DrawingUtils.dpToPx(getContext(), 1), satValTrackerPaint); + + satValTrackerPaint.setColor(0xffdddddd); + canvas.drawCircle(p.x, p.y, circleTrackerRadiusPx, satValTrackerPaint); + } + + private void drawHuePanel(Canvas canvas) { + final Rect rect = hueRect; + + if (BORDER_WIDTH_PX > 0) { + borderPaint.setColor(borderColor); + + canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, + rect.bottom + BORDER_WIDTH_PX, borderPaint); + } + + if (hueBackgroundCache == null) { + hueBackgroundCache = new BitmapCache(); + hueBackgroundCache.bitmap = Bitmap.createBitmap(rect.width(), rect.height(), Config.ARGB_8888); + hueBackgroundCache.canvas = new Canvas(hueBackgroundCache.bitmap); + + int[] hueColors = new int[(int) (rect.height() + 0.5f)]; + + // Generate array of all colors, will be drawn as individual lines. + float h = 360f; + for (int i = 0; i < hueColors.length; i++) { + hueColors[i] = Color.HSVToColor(new float[]{h, 1f, 1f}); + h -= 360f / hueColors.length; + } + + // Time to draw the hue color gradient, + // its drawn as individual lines which + // will be quite many when the resolution is high + // and/or the panel is large. + Paint linePaint = new Paint(); + linePaint.setStrokeWidth(0); + for (int i = 0; i < hueColors.length; i++) { + linePaint.setColor(hueColors[i]); + hueBackgroundCache.canvas.drawLine(0, i, hueBackgroundCache.bitmap.getWidth(), i, linePaint); + } + } + + canvas.drawBitmap(hueBackgroundCache.bitmap, null, rect, null); + + Point p = hueToPoint(hue); + + RectF r = new RectF(); + r.left = rect.left - sliderTrackerOffsetPx; + r.right = rect.right + sliderTrackerOffsetPx; + r.top = p.y - (sliderTrackerSizePx / 2); + r.bottom = p.y + (sliderTrackerSizePx / 2); + + canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint); + } + + private void drawAlphaPanel(Canvas canvas) { + /* + * Will be drawn with hw acceleration, very fast. + * Also the AlphaPatternDrawable is backed by a bitmap + * generated only once if the size does not change. + */ + + if (!showAlphaPanel || alphaRect == null || alphaPatternDrawable == null) return; + + final Rect rect = alphaRect; + + if (BORDER_WIDTH_PX > 0) { + borderPaint.setColor(borderColor); + canvas.drawRect(rect.left - BORDER_WIDTH_PX, rect.top - BORDER_WIDTH_PX, rect.right + BORDER_WIDTH_PX, + rect.bottom + BORDER_WIDTH_PX, borderPaint); + } + + alphaPatternDrawable.draw(canvas); + + float[] hsv = new float[]{hue, sat, val}; + int color = Color.HSVToColor(hsv); + int acolor = Color.HSVToColor(0, hsv); + + alphaShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top, color, acolor, TileMode.CLAMP); + + alphaPaint.setShader(alphaShader); + + canvas.drawRect(rect, alphaPaint); + + if (alphaSliderText != null && !alphaSliderText.equals("")) { + canvas.drawText(alphaSliderText, rect.centerX(), rect.centerY() + DrawingUtils.dpToPx(getContext(), 4), + alphaTextPaint); + } + + Point p = alphaToPoint(alpha); + + RectF r = new RectF(); + r.left = p.x - (sliderTrackerSizePx / 2); + r.right = p.x + (sliderTrackerSizePx / 2); + r.top = rect.top - sliderTrackerOffsetPx; + r.bottom = rect.bottom + sliderTrackerOffsetPx; + + canvas.drawRoundRect(r, 2, 2, hueAlphaTrackerPaint); + } + + private Point hueToPoint(float hue) { + + final Rect rect = hueRect; + final float height = rect.height(); + + Point p = new Point(); + + p.y = (int) (height - (hue * height / 360f) + rect.top); + p.x = rect.left; + + return p; + } + + private Point satValToPoint(float sat, float val) { + + final Rect rect = satValRect; + final float height = rect.height(); + final float width = rect.width(); + + Point p = new Point(); + + p.x = (int) (sat * width + rect.left); + p.y = (int) ((1f - val) * height + rect.top); + + return p; + } + + private Point alphaToPoint(int alpha) { + + final Rect rect = alphaRect; + final float width = rect.width(); + + Point p = new Point(); + + p.x = (int) (width - (alpha * width / 0xff) + rect.left); + p.y = rect.top; + + return p; + } + + private float[] pointToSatVal(float x, float y) { + + final Rect rect = satValRect; + float[] result = new float[2]; + + float width = rect.width(); + float height = rect.height(); + + if (x < rect.left) { + x = 0f; + } else if (x > rect.right) { + x = width; + } else { + x = x - rect.left; + } + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + result[0] = 1.f / width * x; + result[1] = 1.f - (1.f / height * y); + + return result; + } + + private float pointToHue(float y) { + + final Rect rect = hueRect; + + float height = rect.height(); + + if (y < rect.top) { + y = 0f; + } else if (y > rect.bottom) { + y = height; + } else { + y = y - rect.top; + } + + float hue = 360f - (y * 360f / height); + + return hue; + } + + private int pointToAlpha(int x) { + + final Rect rect = alphaRect; + final int width = rect.width(); + + if (x < rect.left) { + x = 0; + } else if (x > rect.right) { + x = width; + } else { + x = x - rect.left; + } + + return 0xff - (x * 0xff / width); + } + + @Override + public boolean onTouchEvent(MotionEvent event) { + boolean update = false; + + switch (event.getAction()) { + + case MotionEvent.ACTION_DOWN: + startTouchPoint = new Point((int) event.getX(), (int) event.getY()); + update = moveTrackersIfNeeded(event); + break; + case MotionEvent.ACTION_MOVE: + update = moveTrackersIfNeeded(event); + break; + case MotionEvent.ACTION_UP: + startTouchPoint = null; + update = moveTrackersIfNeeded(event); + break; + } + + if (update) { + if (onColorChangedListener != null) { + onColorChangedListener.onColorChanged(Color.HSVToColor(alpha, new float[]{hue, sat, val})); + } + invalidate(); + return true; + } + + return super.onTouchEvent(event); + } + + private boolean moveTrackersIfNeeded(MotionEvent event) { + if (startTouchPoint == null) { + return false; + } + + boolean update = false; + + int startX = startTouchPoint.x; + int startY = startTouchPoint.y; + + if (hueRect.contains(startX, startY)) { + hue = pointToHue(event.getY()); + + update = true; + } else if (satValRect.contains(startX, startY)) { + float[] result = pointToSatVal(event.getX(), event.getY()); + + sat = result[0]; + val = result[1]; + + update = true; + } else if (alphaRect != null && alphaRect.contains(startX, startY)) { + alpha = pointToAlpha((int) event.getX()); + + update = true; + } + + return update; + } + + @Override + protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int finalWidth; + int finalHeight; + + int widthMode = MeasureSpec.getMode(widthMeasureSpec); + int heightMode = MeasureSpec.getMode(heightMeasureSpec); + + int widthAllowed = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight(); + int heightAllowed = MeasureSpec.getSize(heightMeasureSpec) - getPaddingBottom() - getPaddingTop(); + + if (widthMode == MeasureSpec.EXACTLY || heightMode == MeasureSpec.EXACTLY) { + //A exact value has been set in either direction, we need to stay within this size. + + if (widthMode == MeasureSpec.EXACTLY && heightMode != MeasureSpec.EXACTLY) { + //The with has been specified exactly, we need to adopt the height to fit. + int h = (widthAllowed - panelSpacingPx - huePanelWidthPx); + + if (showAlphaPanel) { + h += panelSpacingPx + alphaPanelHeightPx; + } + + if (h > heightAllowed) { + //We can't fit the view in this container, set the size to whatever was allowed. + finalHeight = heightAllowed; + } else { + finalHeight = h; + } + + finalWidth = widthAllowed; + } else if (heightMode == MeasureSpec.EXACTLY && widthMode != MeasureSpec.EXACTLY) { + //The height has been specified exactly, we need to stay within this height and adopt the width. + + int w = (heightAllowed + panelSpacingPx + huePanelWidthPx); + + if (showAlphaPanel) { + w -= (panelSpacingPx + alphaPanelHeightPx); + } + + if (w > widthAllowed) { + //we can't fit within this container, set the size to whatever was allowed. + finalWidth = widthAllowed; + } else { + finalWidth = w; + } + + finalHeight = heightAllowed; + } else { + //If we get here the dev has set the width and height to exact sizes. For example match_parent or 300dp. + //This will mean that the sat/val panel will not be square but it doesn't matter. It will work anyway. + //In all other senarios our goal is to make that panel square. + + //We set the sizes to exactly what we were told. + finalWidth = widthAllowed; + finalHeight = heightAllowed; + } + } else { + //If no exact size has been set we try to make our view as big as possible + //within the allowed space. + + //Calculate the needed width to layout using max allowed height. + int widthNeeded = (heightAllowed + panelSpacingPx + huePanelWidthPx); + + //Calculate the needed height to layout using max allowed width. + int heightNeeded = (widthAllowed - panelSpacingPx - huePanelWidthPx); + + if (showAlphaPanel) { + widthNeeded -= (panelSpacingPx + alphaPanelHeightPx); + heightNeeded += panelSpacingPx + alphaPanelHeightPx; + } + + boolean widthOk = false; + boolean heightOk = false; + + if (widthNeeded <= widthAllowed) { + widthOk = true; + } + + if (heightNeeded <= heightAllowed) { + heightOk = true; + } + + if (widthOk && heightOk) { + finalWidth = widthAllowed; + finalHeight = heightNeeded; + } else if (!heightOk && widthOk) { + finalHeight = heightAllowed; + finalWidth = widthNeeded; + } else if (!widthOk && heightOk) { + finalHeight = heightNeeded; + finalWidth = widthAllowed; + } else { + finalHeight = heightAllowed; + finalWidth = widthAllowed; + } + } + + setMeasuredDimension(finalWidth + getPaddingLeft() + getPaddingRight(), + finalHeight + getPaddingTop() + getPaddingBottom()); + } + + private int getPreferredWidth() { + //Our preferred width and height is 200dp for the square sat / val rectangle. + int width = DrawingUtils.dpToPx(getContext(), 200); + + return (width + huePanelWidthPx + panelSpacingPx); + } + + private int getPreferredHeight() { + int height = DrawingUtils.dpToPx(getContext(), 200); + + if (showAlphaPanel) { + height += panelSpacingPx + alphaPanelHeightPx; + } + return height; + } + + @Override + public int getPaddingTop() { + return Math.max(super.getPaddingTop(), mRequiredPadding); + } + + @Override + public int getPaddingBottom() { + return Math.max(super.getPaddingBottom(), mRequiredPadding); + } + + @Override + public int getPaddingLeft() { + return Math.max(super.getPaddingLeft(), mRequiredPadding); + } + + @Override + public int getPaddingRight() { + return Math.max(super.getPaddingRight(), mRequiredPadding); + } + + @Override + protected void onSizeChanged(int w, int h, int oldw, int oldh) { + super.onSizeChanged(w, h, oldw, oldh); + + drawingRect = new Rect(); + drawingRect.left = getPaddingLeft(); + drawingRect.right = w - getPaddingRight(); + drawingRect.top = getPaddingTop(); + drawingRect.bottom = h - getPaddingBottom(); + + //The need to be recreated because they depend on the size of the view. + valShader = null; + satShader = null; + alphaShader = null; + + // Clear those bitmap caches since the size may have changed. + satValBackgroundCache = null; + hueBackgroundCache = null; + + setUpSatValRect(); + setUpHueRect(); + setUpAlphaRect(); + } + + private void setUpSatValRect() { + //Calculate the size for the big color rectangle. + final Rect dRect = drawingRect; + + int left = dRect.left + BORDER_WIDTH_PX; + int top = dRect.top + BORDER_WIDTH_PX; + int bottom = dRect.bottom - BORDER_WIDTH_PX; + int right = dRect.right - BORDER_WIDTH_PX - panelSpacingPx - huePanelWidthPx; + + if (showAlphaPanel) { + bottom -= (alphaPanelHeightPx + panelSpacingPx); + } + + satValRect = new Rect(left, top, right, bottom); + } + + private void setUpHueRect() { + //Calculate the size for the hue slider on the left. + final Rect dRect = drawingRect; + + int left = dRect.right - huePanelWidthPx + BORDER_WIDTH_PX; + int top = dRect.top + BORDER_WIDTH_PX; + int bottom = dRect.bottom - BORDER_WIDTH_PX - (showAlphaPanel ? (panelSpacingPx + alphaPanelHeightPx) : 0); + int right = dRect.right - BORDER_WIDTH_PX; + + hueRect = new Rect(left, top, right, bottom); + } + + private void setUpAlphaRect() { + + if (!showAlphaPanel) return; + + final Rect dRect = drawingRect; + + int left = dRect.left + BORDER_WIDTH_PX; + int top = dRect.bottom - alphaPanelHeightPx + BORDER_WIDTH_PX; + int bottom = dRect.bottom - BORDER_WIDTH_PX; + int right = dRect.right - BORDER_WIDTH_PX; + + alphaRect = new Rect(left, top, right, bottom); + + alphaPatternDrawable = new AlphaPatternDrawable(DrawingUtils.dpToPx(getContext(), 4)); + alphaPatternDrawable.setBounds(Math.round(alphaRect.left), Math.round(alphaRect.top), Math.round(alphaRect.right), + Math.round(alphaRect.bottom)); + } + + /** + * Set a OnColorChangedListener to get notified when the color + * selected by the user has changed. + * + * @param listener the listener + */ + public void setOnColorChangedListener(OnColorChangedListener listener) { + onColorChangedListener = listener; + } + + /** + * Get the current color this view is showing. + * + * @return the current color. + */ + public int getColor() { + return Color.HSVToColor(alpha, new float[]{hue, sat, val}); + } + + /** + * Set the color the view should show. + * + * @param color The color that should be selected. #argb + */ + public void setColor(int color) { + setColor(color, false); + } + + /** + * Set the color this view should show. + * + * @param color The color that should be selected. #argb + * @param callback If you want to get a callback to your OnColorChangedListener. + */ + public void setColor(int color, boolean callback) { + + int alpha = Color.alpha(color); + int red = Color.red(color); + int blue = Color.blue(color); + int green = Color.green(color); + + float[] hsv = new float[3]; + + Color.RGBToHSV(red, green, blue, hsv); + + this.alpha = alpha; + hue = hsv[0]; + sat = hsv[1]; + val = hsv[2]; + + if (callback && onColorChangedListener != null) { + onColorChangedListener.onColorChanged(Color.HSVToColor(this.alpha, new float[]{hue, sat, val})); + } + + invalidate(); + } + + /** + * Set if the user is allowed to adjust the alpha panel. Default is false. + * If it is set to false no alpha will be set. + * + * @param visible {@code true} to show the alpha slider + */ + public void setAlphaSliderVisible(boolean visible) { + if (showAlphaPanel != visible) { + showAlphaPanel = visible; + + /* + * Force recreation. + */ + valShader = null; + satShader = null; + alphaShader = null; + hueBackgroundCache = null; + satValBackgroundCache = null; + + requestLayout(); + } + } + + /** + * Get color of the tracker slider on the hue and alpha panel. + * + * @return the color value + */ + public int getSliderTrackerColor() { + return sliderTrackerColor; + } + + /** + * Set the color of the tracker slider on the hue and alpha panel. + * + * @param color a color value + */ + public void setSliderTrackerColor(int color) { + sliderTrackerColor = color; + hueAlphaTrackerPaint.setColor(sliderTrackerColor); + invalidate(); + } + + /** + * Get the color of the border surrounding all panels. + */ + public int getBorderColor() { + return borderColor; + } + + /** + * Set the color of the border surrounding all panels. + * + * @param color a color value + */ + public void setBorderColor(int color) { + borderColor = color; + invalidate(); + } + + /** + * Get the current value of the text + * that will be shown in the alpha + * slider. + * + * @return the slider text + */ + public String getAlphaSliderText() { + return alphaSliderText; + } + + /** + * Set the text that should be shown in the + * alpha slider. Set to null to disable text. + * + * @param res string resource id. + */ + public void setAlphaSliderText(int res) { + String text = getContext().getString(res); + setAlphaSliderText(text); + } + + /** + * Set the text that should be shown in the + * alpha slider. Set to null to disable text. + * + * @param text Text that should be shown. + */ + public void setAlphaSliderText(String text) { + alphaSliderText = text; + invalidate(); + } + + public interface OnColorChangedListener { + + void onColorChanged(int newColor); + } + + private class BitmapCache { + + public Canvas canvas; + public Bitmap bitmap; + public float value; + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreference.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreference.java new file mode 100644 index 00000000..5cb1e40b --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreference.java @@ -0,0 +1,239 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.preference.Preference; +import android.util.AttributeSet; +import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; + +/** + * A Preference to select a color + */ +public class ColorPreference extends Preference implements ColorPickerDialogListener { + + private static final int SIZE_NORMAL = 0; + private static final int SIZE_LARGE = 1; + + private OnShowDialogListener onShowDialogListener; + private int color = Color.BLACK; + private boolean showDialog; + @ColorPickerDialog.DialogType + private int dialogType; + private int colorShape; + private boolean allowPresets; + private boolean allowCustom; + private boolean showAlphaSlider; + private boolean showColorShades; + private int previewSize; + private int[] presets; + private int dialogTitle; + + public ColorPreference(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public ColorPreference(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs); + } + + private void init(AttributeSet attrs) { + setPersistent(true); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPreference); + showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true); + //noinspection WrongConstant + dialogType = a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS); + colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE); + allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true); + allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true); + showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false); + showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true); + previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL); + final int presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0); + dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title); + if (presetsResId != 0) { + presets = getContext().getResources().getIntArray(presetsResId); + } else { + presets = ColorPickerDialog.MATERIAL_COLORS; + } + if (colorShape == ColorShape.CIRCLE) { + setWidgetLayoutResource( + previewSize == SIZE_LARGE ? R.layout.cpv_preference_circle_large : R.layout.cpv_preference_circle); + } else { + setWidgetLayoutResource( + previewSize == SIZE_LARGE ? R.layout.cpv_preference_square_large : R.layout.cpv_preference_square); + } + a.recycle(); + } + + @Override + protected void onClick() { + super.onClick(); + if (onShowDialogListener != null) { + onShowDialogListener.onShowColorPickerDialog((String) getTitle(), color); + } else if (showDialog) { + ColorPickerDialog dialog = ColorPickerDialog.newBuilder() + .setDialogType(dialogType) + .setDialogTitle(dialogTitle) + .setColorShape(colorShape) + .setPresets(presets) + .setAllowPresets(allowPresets) + .setAllowCustom(allowCustom) + .setShowAlphaSlider(showAlphaSlider) + .setShowColorShades(showColorShades) + .setColor(color) + .create(); + dialog.setColorPickerDialogListener(this); + FragmentActivity activity = (FragmentActivity) getContext(); + activity.getSupportFragmentManager() + .beginTransaction() + .add(dialog, getFragmentTag()) + .commitAllowingStateLoss(); + } + } + + @Override + protected void onAttachedToActivity() { + super.onAttachedToActivity(); + + if (showDialog) { + FragmentActivity activity = (FragmentActivity) getContext(); + ColorPickerDialog fragment = + (ColorPickerDialog) activity.getSupportFragmentManager().findFragmentByTag(getFragmentTag()); + if (fragment != null) { + // re-bind preference to fragment + fragment.setColorPickerDialogListener(this); + } + } + } + + @Override + protected void onBindView(View view) { + super.onBindView(view); + ColorPanelView preview = view.findViewById(R.id.cpv_preference_preview_color_panel); + if (preview != null) { + preview.setColor(color); + } + } + + @Override + protected void onSetInitialValue(boolean restorePersistedValue, Object defaultValue) { + if (restorePersistedValue) { + color = getPersistedInt(0xFF000000); + } else { + color = (Integer) defaultValue; + persistInt(color); + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInteger(index, Color.BLACK); + } + + @Override + public void onColorSelected(int dialogId, @ColorInt int color) { + saveValue(color); + } + + @Override + public void onDialogDismissed(int dialogId) { + // no-op + } + + /** + * Set the new color + * + * @param color The newly selected color + */ + public void saveValue(@ColorInt int color) { + this.color = color; + persistInt(this.color); + notifyChanged(); + callChangeListener(color); + } + + /** + * Get the color of the key. This should be one of the entries in {@link #getPresets()}. + * + * @return The color of the key + */ + public int getColor() { + return color; + } + + /** + * Sets the color selected. This should be one of the entries in {@link #getPresets()}. + * + * @param color The color to set for the key + */ + public void setColor(@ColorInt int color) { + this.color = color; + } + + + /** + * Get the colors that will be shown in the {@link ColorPickerDialog}. + * + * @return An array of color ints + */ + public int[] getPresets() { + return presets; + } + + /** + * Set the colors shown in the {@link ColorPickerDialog}. + * + * @param presets An array of color ints + */ + public void setPresets(@NonNull int[] presets) { + this.presets = presets; + } + + /** + * The listener used for showing the {@link ColorPickerDialog}. + * Call {@link #saveValue(int)} after the user chooses a color. + * If this is set then it is up to you to show the dialog. + * + * @param listener The listener to show the dialog + */ + public void setOnShowDialogListener(OnShowDialogListener listener) { + onShowDialogListener = listener; + } + + /** + * The tag used for the {@link ColorPickerDialog}. + * + * @return The tag + */ + public String getFragmentTag() { + return "color_" + getKey(); + } + + public interface OnShowDialogListener { + + void onShowColorPickerDialog(String title, int currentColor); + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreferenceCompat.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreferenceCompat.java new file mode 100644 index 00000000..99130546 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorPreferenceCompat.java @@ -0,0 +1,234 @@ +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.content.ContextWrapper; +import android.content.res.TypedArray; +import android.graphics.Color; +import android.util.AttributeSet; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceViewHolder; + +/** + * A Preference to select a color + */ +public class ColorPreferenceCompat extends Preference implements ColorPickerDialogListener { + + private static final int SIZE_NORMAL = 0; + private static final int SIZE_LARGE = 1; + + private OnShowDialogListener onShowDialogListener; + private int color = Color.BLACK; + private boolean showDialog; + @ColorPickerDialog.DialogType + private int dialogType; + private int colorShape; + private boolean allowPresets; + private boolean allowCustom; + private boolean showAlphaSlider; + private boolean showColorShades; + private int previewSize; + private int[] presets; + private int dialogTitle; + + public ColorPreferenceCompat(Context context, AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + public ColorPreferenceCompat(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + init(attrs); + } + + private void init(AttributeSet attrs) { + setPersistent(true); + TypedArray a = getContext().obtainStyledAttributes(attrs, R.styleable.ColorPreference); + showDialog = a.getBoolean(R.styleable.ColorPreference_cpv_showDialog, true); + //noinspection WrongConstant + dialogType = a.getInt(R.styleable.ColorPreference_cpv_dialogType, ColorPickerDialog.TYPE_PRESETS); + colorShape = a.getInt(R.styleable.ColorPreference_cpv_colorShape, ColorShape.CIRCLE); + allowPresets = a.getBoolean(R.styleable.ColorPreference_cpv_allowPresets, true); + allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true); + showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false); + showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true); + previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL); + final int presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0); + dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title); + if (presetsResId != 0) { + presets = getContext().getResources().getIntArray(presetsResId); + } else { + presets = ColorPickerDialog.MATERIAL_COLORS; + } + if (colorShape == ColorShape.CIRCLE) { + setWidgetLayoutResource( + previewSize == SIZE_LARGE ? R.layout.cpv_preference_circle_large : R.layout.cpv_preference_circle); + } else { + setWidgetLayoutResource( + previewSize == SIZE_LARGE ? R.layout.cpv_preference_square_large : R.layout.cpv_preference_square); + } + a.recycle(); + } + + @Override + protected void onClick() { + super.onClick(); + if (onShowDialogListener != null) { + onShowDialogListener.onShowColorPickerDialog((String) getTitle(), color); + } else if (showDialog) { + ColorPickerDialog dialog = ColorPickerDialog.newBuilder() + .setDialogType(dialogType) + .setDialogTitle(dialogTitle) + .setColorShape(colorShape) + .setPresets(presets) + .setAllowPresets(allowPresets) + .setAllowCustom(allowCustom) + .setShowAlphaSlider(showAlphaSlider) + .setShowColorShades(showColorShades) + .setColor(color) + .create(); + dialog.setColorPickerDialogListener(this); + getActivity().getSupportFragmentManager() + .beginTransaction() + .add(dialog, getFragmentTag()) + .commitAllowingStateLoss(); + } + } + + public FragmentActivity getActivity() { + Context context = getContext(); + if (context instanceof FragmentActivity) { + return (FragmentActivity) context; + } else if (context instanceof ContextWrapper) { + Context baseContext = ((ContextWrapper) context).getBaseContext(); + if (baseContext instanceof FragmentActivity) { + return (FragmentActivity) baseContext; + } + } + throw new IllegalStateException("Error getting activity from context"); + } + + @Override + public void onAttached() { + super.onAttached(); + if (showDialog) { + ColorPickerDialog fragment = + (ColorPickerDialog) getActivity().getSupportFragmentManager().findFragmentByTag(getFragmentTag()); + if (fragment != null) { + // re-bind preference to fragment + fragment.setColorPickerDialogListener(this); + } + } + } + + @Override + public void onBindViewHolder(PreferenceViewHolder holder) { + super.onBindViewHolder(holder); + ColorPanelView preview = holder.itemView.findViewById(R.id.cpv_preference_preview_color_panel); + if (preview != null) { + preview.setColor(color); + } + } + + @Override + protected void onSetInitialValue(Object defaultValue) { + super.onSetInitialValue(defaultValue); + if (defaultValue instanceof Integer) { + color = (Integer) defaultValue; + persistInt(color); + } else { + color = getPersistedInt(0xFF000000); + } + } + + @Override + protected Object onGetDefaultValue(TypedArray a, int index) { + return a.getInteger(index, Color.BLACK); + } + + @Override + public void onColorSelected(int dialogId, @ColorInt int color) { + saveValue(color); + } + + @Override + public void onDialogDismissed(int dialogId) { + // no-op + } + + /** + * Set the new color + * + * @param color The newly selected color + */ + public void saveValue(@ColorInt int color) { + this.color = color; + persistInt(this.color); + notifyChanged(); + callChangeListener(color); + } + + /** + * Get the colors that will be shown in the {@link ColorPickerDialog}. + * + * @return An array of color ints + */ + public int[] getPresets() { + return presets; + } + + /** + * Set the colors shown in the {@link ColorPickerDialog}. + * + * @param presets An array of color ints + */ + public void setPresets(@NonNull int[] presets) { + this.presets = presets; + } + + /** + * The listener used for showing the {@link ColorPickerDialog}. + * Call {@link #saveValue(int)} after the user chooses a color. + * If this is set then it is up to you to show the dialog. + * + * @param listener The listener to show the dialog + */ + public void setOnShowDialogListener(OnShowDialogListener listener) { + onShowDialogListener = listener; + } + + /** + * Get the color of the key. This should be one of the entries in {@link #getPresets()}. + * + * @return The color of the key + */ + public int getColor() { + return color; + } + + /** + * Sets the color selected. This should be one of the entries in {@link #getPresets()}. + * + * @param color The color to set for the key + */ + public void setColor(@ColorInt int color) { + this.color = color; + } + + /** + * The tag used for the {@link ColorPickerDialog}. + * + * @return The tag + */ + public String getFragmentTag() { + return "color_" + getKey(); + } + + public interface OnShowDialogListener { + + void onShowColorPickerDialog(String title, int currentColor); + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorShape.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorShape.java new file mode 100644 index 00000000..4e64dbfe --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/ColorShape.java @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import androidx.annotation.IntDef; + +/** + * The shape of the color preview + */ +@IntDef({ColorShape.SQUARE, ColorShape.CIRCLE}) +public @interface ColorShape { + + int SQUARE = 0; + + int CIRCLE = 1; +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/DrawingUtils.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/DrawingUtils.java new file mode 100644 index 00000000..26a48376 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/DrawingUtils.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.util.DisplayMetrics; +import android.util.TypedValue; + +final class DrawingUtils { + + static int dpToPx(Context c, float dipValue) { + DisplayMetrics metrics = c.getResources().getDisplayMetrics(); + float val = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dipValue, metrics); + int res = (int) (val + 0.5); // Round + // Ensure at least 1 pixel if val was > 0 + return res == 0 && val > 0 ? 1 : res; + } +} diff --git a/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/NestedGridView.java b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/NestedGridView.java new file mode 100644 index 00000000..c3ed13e4 --- /dev/null +++ b/colorPicker/src/main/java/com/jaredrummler/android/colorpicker/NestedGridView.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2017 Jared Rummler + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.jaredrummler.android.colorpicker; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.GridView; + +import androidx.annotation.RestrictTo; + +@RestrictTo(RestrictTo.Scope.LIBRARY) +public class NestedGridView extends GridView { + + public NestedGridView(Context context) { + super(context); + } + + public NestedGridView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public NestedGridView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + @Override + public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { + int expandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); + super.onMeasure(widthMeasureSpec, expandSpec); + } +} diff --git a/colorPicker/src/main/res/drawable-hdpi/cpv_alpha.png b/colorPicker/src/main/res/drawable-hdpi/cpv_alpha.png new file mode 100644 index 00000000..e3fe9c85 Binary files /dev/null and b/colorPicker/src/main/res/drawable-hdpi/cpv_alpha.png differ diff --git a/colorPicker/src/main/res/drawable-xhdpi/cpv_alpha.png b/colorPicker/src/main/res/drawable-xhdpi/cpv_alpha.png new file mode 100644 index 00000000..c51c6120 Binary files /dev/null and b/colorPicker/src/main/res/drawable-xhdpi/cpv_alpha.png differ diff --git a/colorPicker/src/main/res/drawable-xxhdpi/cpv_alpha.png b/colorPicker/src/main/res/drawable-xxhdpi/cpv_alpha.png new file mode 100644 index 00000000..c81fc262 Binary files /dev/null and b/colorPicker/src/main/res/drawable-xxhdpi/cpv_alpha.png differ diff --git a/colorPicker/src/main/res/drawable/cpv_btn_background.xml b/colorPicker/src/main/res/drawable/cpv_btn_background.xml new file mode 100644 index 00000000..406902db --- /dev/null +++ b/colorPicker/src/main/res/drawable/cpv_btn_background.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/drawable/cpv_btn_background_pressed.xml b/colorPicker/src/main/res/drawable/cpv_btn_background_pressed.xml new file mode 100644 index 00000000..3b183c39 --- /dev/null +++ b/colorPicker/src/main/res/drawable/cpv_btn_background_pressed.xml @@ -0,0 +1,14 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/drawable/cpv_ic_arrow_right_black_24dp.xml b/colorPicker/src/main/res/drawable/cpv_ic_arrow_right_black_24dp.xml new file mode 100644 index 00000000..246da445 --- /dev/null +++ b/colorPicker/src/main/res/drawable/cpv_ic_arrow_right_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/colorPicker/src/main/res/drawable/cpv_preset_checked.xml b/colorPicker/src/main/res/drawable/cpv_preset_checked.xml new file mode 100644 index 00000000..4e056df3 --- /dev/null +++ b/colorPicker/src/main/res/drawable/cpv_preset_checked.xml @@ -0,0 +1,9 @@ + + + diff --git a/colorPicker/src/main/res/layout/cpv_color_item_circle.xml b/colorPicker/src/main/res/layout/cpv_color_item_circle.xml new file mode 100644 index 00000000..ce8bde62 --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_color_item_circle.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_color_item_square.xml b/colorPicker/src/main/res/layout/cpv_color_item_square.xml new file mode 100644 index 00000000..8d4ce3fb --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_color_item_square.xml @@ -0,0 +1,23 @@ + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_dialog_color_picker.xml b/colorPicker/src/main/res/layout/cpv_dialog_color_picker.xml new file mode 100644 index 00000000..933d006a --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_dialog_color_picker.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_dialog_presets.xml b/colorPicker/src/main/res/layout/cpv_dialog_presets.xml new file mode 100644 index 00000000..9a420758 --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_dialog_presets.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_preference_circle.xml b/colorPicker/src/main/res/layout/cpv_preference_circle.xml new file mode 100644 index 00000000..88b67d27 --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_preference_circle.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_preference_circle_large.xml b/colorPicker/src/main/res/layout/cpv_preference_circle_large.xml new file mode 100644 index 00000000..f42902d7 --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_preference_circle_large.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_preference_square.xml b/colorPicker/src/main/res/layout/cpv_preference_square.xml new file mode 100644 index 00000000..4ff603cb --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_preference_square.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/colorPicker/src/main/res/layout/cpv_preference_square_large.xml b/colorPicker/src/main/res/layout/cpv_preference_square_large.xml new file mode 100644 index 00000000..07c6ea50 --- /dev/null +++ b/colorPicker/src/main/res/layout/cpv_preference_square_large.xml @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-ar/strings.xml b/colorPicker/src/main/res/values-ar/strings.xml new file mode 100644 index 00000000..8b9e1957 --- /dev/null +++ b/colorPicker/src/main/res/values-ar/strings.xml @@ -0,0 +1,22 @@ + + + + اختر لونًا + ألوان جاهزة + ألوان معدلة + اختر + الشفافية + diff --git a/colorPicker/src/main/res/values-be/strings.xml b/colorPicker/src/main/res/values-be/strings.xml new file mode 100644 index 00000000..7bc2df19 --- /dev/null +++ b/colorPicker/src/main/res/values-be/strings.xml @@ -0,0 +1,22 @@ + + + + Вылучыце колер + Перадусталёўкі + Асаблівы + Выбраць + Празрыстасць + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-cs/strings.xml b/colorPicker/src/main/res/values-cs/strings.xml new file mode 100644 index 00000000..4a10349d --- /dev/null +++ b/colorPicker/src/main/res/values-cs/strings.xml @@ -0,0 +1,22 @@ + + + + Vyberte barvu + Přednastavené + Vlastní + Vybrat + Průhlednost + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-de/strings.xml b/colorPicker/src/main/res/values-de/strings.xml new file mode 100644 index 00000000..2b23b5fe --- /dev/null +++ b/colorPicker/src/main/res/values-de/strings.xml @@ -0,0 +1,22 @@ + + + + Farbe auswählen + Voreinstellungen + Benutzerdefiniert + Auswählen + Transparenz + diff --git a/colorPicker/src/main/res/values-es/strings.xml b/colorPicker/src/main/res/values-es/strings.xml new file mode 100644 index 00000000..8f1ed687 --- /dev/null +++ b/colorPicker/src/main/res/values-es/strings.xml @@ -0,0 +1,9 @@ + + + + Selecciona un color + Predeterminados + Personalizar + Seleccionar + Transparencia + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-fr-rFR/strings.xml b/colorPicker/src/main/res/values-fr-rFR/strings.xml new file mode 100644 index 00000000..a5ba7b11 --- /dev/null +++ b/colorPicker/src/main/res/values-fr-rFR/strings.xml @@ -0,0 +1,19 @@ + + + + Sélectionner une couleur + Présélections + Personnalisée + Valider + Transparence + diff --git a/colorPicker/src/main/res/values-it-rIT/strings.xml b/colorPicker/src/main/res/values-it-rIT/strings.xml new file mode 100644 index 00000000..a05a8506 --- /dev/null +++ b/colorPicker/src/main/res/values-it-rIT/strings.xml @@ -0,0 +1,21 @@ + + + Personalizzato + Seleziona un Colore + Predefiniti + Seleziona + Trasparenza + diff --git a/colorPicker/src/main/res/values-iw/strings.xml b/colorPicker/src/main/res/values-iw/strings.xml new file mode 100644 index 00000000..1eea7d44 --- /dev/null +++ b/colorPicker/src/main/res/values-iw/strings.xml @@ -0,0 +1,8 @@ + + + בחירת צבע + קבוע מראש + מותאם אישית + בחירה + שקיפות + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-ja/strings.xml b/colorPicker/src/main/res/values-ja/strings.xml new file mode 100644 index 00000000..9b6ce502 --- /dev/null +++ b/colorPicker/src/main/res/values-ja/strings.xml @@ -0,0 +1,22 @@ + + + + 色を選択 + プリセット + カスタム + 選択 + 透明度 + diff --git a/colorPicker/src/main/res/values-nl-rNL/strings.xml b/colorPicker/src/main/res/values-nl-rNL/strings.xml new file mode 100644 index 00000000..6a161092 --- /dev/null +++ b/colorPicker/src/main/res/values-nl-rNL/strings.xml @@ -0,0 +1,19 @@ + + + + Selecteer een kleur + Vooraf ingesteld + Aangepast + Kiezen + Transparantie + diff --git a/colorPicker/src/main/res/values-pl/strings.xml b/colorPicker/src/main/res/values-pl/strings.xml new file mode 100644 index 00000000..c4f90907 --- /dev/null +++ b/colorPicker/src/main/res/values-pl/strings.xml @@ -0,0 +1,22 @@ + + + + Wybierz kolor + Kolory domyślne + Dostosuj + Wybierz + Przezroczystość + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-pt/strings.xml b/colorPicker/src/main/res/values-pt/strings.xml new file mode 100644 index 00000000..dcd6bff2 --- /dev/null +++ b/colorPicker/src/main/res/values-pt/strings.xml @@ -0,0 +1,9 @@ + + + + Selecionar uma cor + Predefinidos + Personalizar + Selecionar + Transparência + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-ru/strings.xml b/colorPicker/src/main/res/values-ru/strings.xml new file mode 100644 index 00000000..0e010e62 --- /dev/null +++ b/colorPicker/src/main/res/values-ru/strings.xml @@ -0,0 +1,22 @@ + + + + Выберите цвет + Предустановки + Особый + Выбрать + Прозрачность + diff --git a/colorPicker/src/main/res/values-sk/strings.xml b/colorPicker/src/main/res/values-sk/strings.xml new file mode 100644 index 00000000..75fc8c0a --- /dev/null +++ b/colorPicker/src/main/res/values-sk/strings.xml @@ -0,0 +1,22 @@ + + + + Vyberte farbu + Prednastavené + Vlastné + Vybrať + Priehľadnosť + \ No newline at end of file diff --git a/colorPicker/src/main/res/values-tr/strings.xml b/colorPicker/src/main/res/values-tr/strings.xml new file mode 100644 index 00000000..db8539f5 --- /dev/null +++ b/colorPicker/src/main/res/values-tr/strings.xml @@ -0,0 +1,19 @@ + + + + Renk Seçin + Ön Ayarlar + Özel + Seç + Şeffaflık + diff --git a/colorPicker/src/main/res/values-zh/strings.xml b/colorPicker/src/main/res/values-zh/strings.xml new file mode 100644 index 00000000..058e4c5a --- /dev/null +++ b/colorPicker/src/main/res/values-zh/strings.xml @@ -0,0 +1,22 @@ + + + + 选择颜色 + 预置颜色 + 自定义颜色 + 确认 + 透明度 + \ No newline at end of file diff --git a/colorPicker/src/main/res/values/attrs.xml b/colorPicker/src/main/res/values/attrs.xml new file mode 100644 index 00000000..90da9539 --- /dev/null +++ b/colorPicker/src/main/res/values/attrs.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/values/dimen.xml b/colorPicker/src/main/res/values/dimen.xml new file mode 100644 index 00000000..296eee8d --- /dev/null +++ b/colorPicker/src/main/res/values/dimen.xml @@ -0,0 +1,11 @@ + + + 6dp + 66dp + 34dp + 58dp + 50dp + 8dp + 28dp + 40dp + diff --git a/colorPicker/src/main/res/values/ids.xml b/colorPicker/src/main/res/values/ids.xml new file mode 100644 index 00000000..1ea3355b --- /dev/null +++ b/colorPicker/src/main/res/values/ids.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/colorPicker/src/main/res/values/strings.xml b/colorPicker/src/main/res/values/strings.xml new file mode 100644 index 00000000..bef797cb --- /dev/null +++ b/colorPicker/src/main/res/values/strings.xml @@ -0,0 +1,22 @@ + + + + Select a Color + Presets + Custom + Select + Transparency + \ No newline at end of file diff --git a/colorPicker/src/main/res/values/styles.xml b/colorPicker/src/main/res/values/styles.xml new file mode 100644 index 00000000..31bb8b2b --- /dev/null +++ b/colorPicker/src/main/res/values/styles.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index ece7185d..907710f8 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,3 +5,4 @@ include ':mytransl' include ':ratethisapp' include ':sparkbutton' include ':mathjaxandroid' +include ':colorPicker'