null
22 | * parameter
23 | */
24 | void removeOnDrawableClickListener();
25 |
26 | /**
27 | * Add the start drawable to the view, default the LEFT drawable.
28 | * this could be the LEFT or RIGHT drawable based on the user locale if the developer
29 | * has added android:supportsRtl="true"
in his AndroidManifest.xml
30 | * @param csDrawable the CsDrawable object (can be null)
31 | */
32 | void addStartCsDrawable(CsDrawable csDrawable);
33 |
34 | /**
35 | * Add the TOP drawable to the view
36 | * @param csDrawable the CsDrawable object (can be null)
37 | */
38 | void addTopCsDrawable(CsDrawable csDrawable);
39 |
40 | /**
41 | * Add the end drawable to the view, default the RIGHT drawable.
42 | * this could be the LEFT or RIGHT drawable based on the user locale if the developer
43 | * has added android:supportsRtl="true"
in his AndroidManifest.xml
44 | * @param csDrawable the CsDrawable object (can be null)
45 | */
46 | void addEndCsDrawable(CsDrawable csDrawable);
47 |
48 | /**
49 | * Add the BOTTOM drawable to the view
50 | * @param csDrawable the CsDrawable object (can be null)
51 | */
52 | void addBottomCsDrawable(CsDrawable csDrawable);
53 |
54 | /**
55 | * Change the {@link CsDrawable} object visibility attached to the START position
56 | * @param visible true for show it in the view, false to hide
57 | */
58 | void showStartCsDrawable(boolean visible);
59 |
60 | /**
61 | * Change the {@link CsDrawable} object visibility attached to the TOP position
62 | * @param visible true for show it in the view, false to hide
63 | */
64 | void showTopCsDrawable(boolean visible);
65 |
66 | /**
67 | * Change the {@link CsDrawable} object visibility attached to the END position
68 | * @param visible true for show it in the view, false to hide
69 | */
70 | void showEndCsDrawable(boolean visible);
71 |
72 | /**
73 | * Change the {@link CsDrawable} object visibility attached to the BOTTOM position
74 | * @param visible true for show it in the view, false to hide
75 | */
76 | void showBottomCsDrawable(boolean visible);
77 |
78 | /**
79 | * Remove all the {@link CsDrawable} objects to the view
80 | */
81 | void removeAllCsDrawables();
82 |
83 | /**
84 | * Disable focus on the view, simulating a {@link android.view.View#setEnabled(boolean)}
85 | * call with a false
parameter. In this way we can still handle the touch
86 | * inputs on the {@link CsDrawable}/s attached.
87 | * @param preventReFocus true
if the focus leaving the current view should
88 | * not fall on another view inside the parent view. false
89 | * for default behaviour.
90 | * @param closeKeyboard true
if the keyboard should be closed if opened when
91 | * this method is called
92 | */
93 | void disableFocusOnText(boolean preventReFocus, boolean closeKeyboard);
94 |
95 | /**
96 | * Re-enable the focus on the view, canceling a previous call to
97 | * {@link #disableFocusOnText(boolean, boolean)} method.
98 | * @param openKeyboard true
if the keyboard should be opened if closed when this
99 | * method is called
100 | */
101 | void enableFocusOnText(boolean openKeyboard);
102 |
103 | /**
104 | * Helper method to close the keyboard if the IME is currently opened.
105 | * WARNING
106 | *107 | * This is a small utility which aims to provide a basic functionality which depends a lot 108 | * on the keyboard application the user it's using, your view and inputMethod settings. 109 | * So if this method is not working you probably need to provide your custom implementation. 110 | *
111 | */ 112 | void closeKeyboard(); 113 | 114 | /** 115 | * Helper method to open the keyboard on the current view if the IME is currently closed 116 | */ 117 | void openKeyboard(); 118 | 119 | } 120 | -------------------------------------------------------------------------------- /library/src/main/java/com/matpag/clickdrawabletextview/CsDrawable.java: -------------------------------------------------------------------------------- 1 | package com.matpag.clickdrawabletextview; 2 | 3 | import android.content.Context; 4 | import android.graphics.drawable.Drawable; 5 | import android.support.annotation.DrawableRes; 6 | import android.support.annotation.NonNull; 7 | import android.support.annotation.Nullable; 8 | import android.support.v4.content.ContextCompat; 9 | import android.support.v4.graphics.drawable.DrawableCompat; 10 | import android.util.DisplayMetrics; 11 | import android.util.TypedValue; 12 | 13 | /** 14 | * Custom-Sizable drawable 15 | * 16 | * Created by Mattia Pagini on 12/02/2017. 17 | */ 18 | public class CsDrawable { 19 | 20 | /** 21 | * The wrapped drawable 22 | */ 23 | private Drawable drawable; 24 | 25 | /** 26 | * The application context 27 | */ 28 | private Context context; 29 | 30 | /** 31 | * The visibility flag for this {@link CsDrawable} 32 | */ 33 | private boolean visibility; 34 | 35 | /** 36 | * Internal constructor 37 | */ 38 | CsDrawable(@NonNull Context context, @NonNull Drawable drawable){ 39 | this.context = context.getApplicationContext(); 40 | this.drawable = drawable; 41 | this.visibility = true; 42 | setDefaultDrawableIntrinsicBounds(); 43 | } 44 | 45 | public boolean isVisible(){ 46 | return visibility; 47 | } 48 | 49 | void setVisibility(boolean visibility){ 50 | this.visibility = visibility; 51 | } 52 | 53 | public @NonNull Drawable getDrawable(){ 54 | return drawable; 55 | } 56 | 57 | @Nullable Drawable getDrawableIfVisible(){ 58 | return visibility ? drawable : null; 59 | } 60 | 61 | /** 62 | * Validate the dimension params before sizing the drawable 63 | * @param height the requested height of the drawable 64 | * @param width the requested width of the drawable 65 | * @return true if params are correct, false otherwise 66 | */ 67 | private static boolean validateSizeParams(int height, int width){ 68 | if (width < 0 || height < 0){ 69 | throw new IllegalArgumentException("CsDrawable requested height and width must be >= 0"); 70 | } 71 | return true; 72 | } 73 | 74 | void setDrawablePixelSize(int height, int width){ 75 | drawable.setBounds(0, 0, width, height); 76 | } 77 | 78 | void setDrawableDpSize(int heightDp, int widthDp){ 79 | DisplayMetrics metrics = context.getResources().getDisplayMetrics(); 80 | int pixelWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, widthDp, metrics); 81 | int pixelHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, heightDp, metrics); 82 | drawable.setBounds(0, 0, pixelWidth, pixelHeight); 83 | } 84 | 85 | private void setDefaultDrawableIntrinsicBounds(){ 86 | drawable.setBounds(0, 0, 87 | drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight()); 88 | } 89 | 90 | /** 91 | * The builder to create a {@link CsDrawable} object in the easy way 92 | */ 93 | public final static class Builder { 94 | 95 | private CsDrawable csDrawable; 96 | 97 | /** 98 | * Builder to create a {@link CsDrawable} object 99 | * @param context Any context 100 | * @param drawable A {@link Drawable} object, if you want a mutable drawable (to prevent 101 | * state sharing between drawable with the same origin) you should check 102 | * {@link Builder#Builder(Context, Drawable, boolean)} instead. 103 | * Or you can provide an already mutated {@link Drawable} object. 104 | */ 105 | public Builder(@NonNull Context context, Drawable drawable){ 106 | this (context, drawable, false); 107 | } 108 | 109 | /** 110 | * 111 | * @param context Any context 112 | * @param drawableRes A {@link DrawableRes} resourceId pointing to a drawable like a PNG or 113 | * a {@link android.graphics.drawable.VectorDrawable} 114 | * @param mutable If you want make the drawable mutable (to prevent 115 | * state sharing between drawable with the same origin). 116 | * Read here 117 | * for more info. 118 | *This is usefull for tinting or other things which should act only on the 119 | * specific drawable object and not at global level.
120 | */ 121 | public Builder(@NonNull Context context, @DrawableRes int drawableRes, boolean mutable){ 122 | this(context, ContextCompat.getDrawable(context, drawableRes), mutable); 123 | } 124 | 125 | /** 126 | * 127 | * @param context Any context 128 | * @param drawable A {@link Drawable} object 129 | * @param mutable Passtrue
if you want make the drawable mutable (to prevent
130 | * state sharing between drawable with the same origin).
131 | * Read here
132 | * for more info.
133 | * This is usefull for tinting or other things which should act only on the
134 | * specific drawable object and not at global level.
135 | */
136 | public Builder(@NonNull Context context, Drawable drawable, boolean mutable){
137 | if (drawable == null){
138 | throw new IllegalArgumentException("drawable can't be null");
139 | }
140 | if (mutable) {
141 | Drawable wrappedDrawable = DrawableCompat.wrap(drawable);
142 | drawable = wrappedDrawable.mutate();
143 | }
144 | csDrawable = new CsDrawable(context, drawable);
145 | }
146 |
147 | /**
148 | * set the drawable size in pixels
149 | * @param pixelHeight target height in pixel
150 | * @param pixelWidth target width in pixel
151 | *
152 | * @return current instance
153 | */
154 | public Builder setDrawablePixelSize(int pixelHeight, int pixelWidth){
155 | if (validateSizeParams(pixelHeight, pixelWidth)) {
156 | csDrawable.setDrawablePixelSize(pixelHeight, pixelWidth);
157 | }
158 | return this;
159 | }
160 |
161 | /**
162 | * set the drawable size in DP
163 | * @param dpHeight target height in DP
164 | * @param dpWidth target width in DP
165 | *
166 | * @return current instance
167 | */
168 | public Builder setDrawableDpSize(int dpHeight, int dpWidth){
169 | if (validateSizeParams(dpHeight, dpWidth)) {
170 | csDrawable.setDrawableDpSize(dpHeight, dpWidth);
171 | }
172 | return this;
173 | }
174 |
175 | /**
176 | * set the initial visibility of the drawable
177 | * @param visible default false
178 | *
179 | * @return current instance
180 | */
181 | public Builder setVisibility(boolean visible){
182 | csDrawable.setVisibility(visible);
183 | return this;
184 | }
185 |
186 | public CsDrawable build(){
187 | return csDrawable;
188 | }
189 |
190 | }
191 |
192 | }
193 |
--------------------------------------------------------------------------------
/library/src/main/java/com/matpag/clickdrawabletextview/CsDrawableSettings.java:
--------------------------------------------------------------------------------
1 | package com.matpag.clickdrawabletextview;
2 |
3 | import android.content.Context;
4 | import android.content.pm.ApplicationInfo;
5 | import android.content.pm.PackageManager;
6 | import android.os.Build;
7 |
8 | /**
9 | * Global configuration to handle correctly some user specific choices
10 | *
11 | * Created by Mattia Pagini on 17/05/2017.
12 | */
13 | public class CsDrawableSettings {
14 |
15 | private boolean rtlSupportEnabled;
16 |
17 | private static CsDrawableSettings mSettings;
18 |
19 | private CsDrawableSettings(boolean rtlSupportEnabled){
20 | this.rtlSupportEnabled = rtlSupportEnabled;
21 | }
22 |
23 | /**
24 | * Init method to call before every custom view initialization, this should preferably be
25 | * called in the custom {@link android.app.Application} app class. But you can use it in the
26 | * activity too before calling {@link android.app.Activity#setContentView(int)} or calling
27 | * this per Activity if you need to support RTL in some activities and not in others
28 | * @param context the app context
29 | * @param packageName the packageName of the app, BuildConfig.APPLICATION_ID
should
30 | * be the proper choice in the most cases
31 | */
32 | public static void init(Context context, String packageName){
33 | // prior to API 17 RTL is not supported, so we create settings instance with default support
34 | // for RTL set to false
35 | boolean rtlSupport = false;
36 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1){
37 | PackageManager pManager = context.getApplicationContext().getPackageManager();
38 | try {
39 | ApplicationInfo appInfo = pManager.getApplicationInfo(packageName, 0);
40 | //read the Application android:supportsRtl xml properties (if present)
41 | rtlSupport = (appInfo.flags & ApplicationInfo.FLAG_SUPPORTS_RTL) != 0;
42 | } catch (PackageManager.NameNotFoundException nfe){
43 | throw new IllegalArgumentException("Unable to get info for the provided " +
44 | "packageName, are you sure is it correct? BuildConfig.APPLICATION_ID " +
45 | "should be fine in most cases");
46 | }
47 | }
48 | mSettings = new CsDrawableSettings(rtlSupport);
49 | }
50 |
51 | /**
52 | * Expose the RTL support flag
53 | * @return true if the developer added android:supportsRtl="true"
to the
54 | * application manifest, false otherwise
55 | */
56 | static boolean isRtlSupportEnabled(){
57 | if (mSettings == null){
58 | throw new NullPointerException("You need to call CsDrawableSettings.init() in your " +
59 | "custom Application class or Activity before using the library");
60 | }
61 | return mSettings.rtlSupportEnabled;
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/library/src/main/java/com/matpag/clickdrawabletextview/CsDrawableTouchUtils.java:
--------------------------------------------------------------------------------
1 | package com.matpag.clickdrawabletextview;
2 |
3 | import android.graphics.Canvas;
4 | import android.view.MotionEvent;
5 | import android.widget.TextView;
6 |
7 | /**
8 | * Class to handle touch event on the view and calculate if the touch are happening inside
9 | * the drawables we have defined
10 | *
11 | * Created by Mattia Pagini on 29/04/2017.
12 | */
13 | final class CsDrawableTouchUtils {
14 |
15 | /**
16 | * vSpace = vertical space available in the TextView
17 | *hSpace = horizontal space available in the TextView
18 | */ 19 | private int vSpace, hSpace; 20 | 21 | /** 22 | *hHeight = half height of the drawable bounds
23 | *hWidth = half width of the drawable bounds
24 | */ 25 | private int hHeight, hWidth = -1; 26 | 27 | /** 28 | *centerY = the Y coordinates where the {@link TextView#onDraw(Canvas)} wants start to draw 29 | * the drawable from
30 | *centerX = the X coordinates where the {@link TextView#onDraw(Canvas)} wants start to draw 31 | * the drawable from the center
32 | */ 33 | private int centerY, centerX = -1; 34 | 35 | /** 36 | * The offset for the X and Y axis (if the view is inside a ScrollView or similar) 37 | */ 38 | private int scrollX, scrollY; 39 | 40 | /** 41 | * The current touch event 42 | */ 43 | private MotionEvent event; 44 | 45 | /** 46 | * The current touched view 47 | */ 48 | private TextView view; 49 | 50 | /** 51 | * Support for RTL layout or not 52 | */ 53 | private boolean isLayoutRTL; 54 | 55 | CsDrawableTouchUtils(MotionEvent event, TextView view, boolean isLayoutRTL){ 56 | this.event = event; 57 | this.view = view; 58 | this.isLayoutRTL = isLayoutRTL; 59 | vSpace = view.getHeight() - view.getCompoundPaddingBottom() - view.getCompoundPaddingTop(); 60 | hSpace = view.getWidth() - view.getCompoundPaddingRight() - view.getCompoundPaddingLeft(); 61 | //if the drawable is extremely large (pushing the edges of the drawable itself 62 | //or of the other drawables out of the current view bounds, will not be possible 63 | //to calculate the correct touch position 64 | if (hSpace < 0 || vSpace < 0){ 65 | throw new IllegalArgumentException("The size of one of your drawable is exceeding the" + 66 | " calculated width or height of the view. In this case you should provide" + 67 | "a smaller drawable or provide a smaller dimension in XML or with the builder"); 68 | } 69 | scrollX = view.getScrollX(); 70 | scrollY = view.getScrollY(); 71 | } 72 | 73 | private boolean isClickInsideDrawableBounds(){ 74 | return (event.getX() >= centerX - hWidth) && (event.getX() <= centerX + hWidth) 75 | && (event.getY() >= centerY - hHeight) && (event.getY() <= centerY + hHeight); 76 | } 77 | 78 | boolean isStartDrawableTouched(CsDrawable drawable){ 79 | if (isLayoutRTL){ 80 | return isRightDrawableTouched(drawable); 81 | } else { 82 | return isLeftDrawableTouched(drawable); 83 | } 84 | } 85 | 86 | boolean isEndDrawableTouched(CsDrawable drawable){ 87 | if (isLayoutRTL){ 88 | return isLeftDrawableTouched(drawable); 89 | } else { 90 | return isRightDrawableTouched(drawable); 91 | } 92 | } 93 | 94 | boolean isTopDrawableTouched(CsDrawable drawable){ 95 | hHeight = drawable.getDrawable().getBounds().height() / 2; 96 | hWidth = drawable.getDrawable().getBounds().width() / 2; 97 | centerX = scrollX + view.getCompoundPaddingLeft() + hSpace / 2; 98 | centerY = scrollY + view.getPaddingTop() + hHeight; 99 | return isClickInsideDrawableBounds(); 100 | } 101 | 102 | boolean isBottomDrawableTouched(CsDrawable drawable){ 103 | hHeight = drawable.getDrawable().getBounds().height() / 2; 104 | hWidth = drawable.getDrawable().getBounds().width() / 2; 105 | centerX = scrollX + view.getCompoundPaddingLeft() + hSpace / 2; 106 | centerY = scrollY + view.getHeight() - view.getPaddingBottom() - hHeight; 107 | return isClickInsideDrawableBounds(); 108 | } 109 | 110 | private boolean isLeftDrawableTouched(CsDrawable drawable){ 111 | hHeight = drawable.getDrawable().getBounds().height() / 2; 112 | hWidth = drawable.getDrawable().getBounds().width() / 2; 113 | centerX = scrollX + view.getPaddingLeft() + hWidth; 114 | centerY = scrollY + view.getCompoundPaddingTop() + vSpace / 2; 115 | return isClickInsideDrawableBounds(); 116 | } 117 | 118 | private boolean isRightDrawableTouched(CsDrawable drawable){ 119 | hHeight = drawable.getDrawable().getBounds().height() / 2; 120 | hWidth = drawable.getDrawable().getBounds().width() / 2; 121 | centerX = scrollX + view.getWidth() - view.getPaddingRight() - hWidth; 122 | centerY = scrollY + view.getCompoundPaddingTop() + vSpace / 2; 123 | return isClickInsideDrawableBounds(); 124 | } 125 | 126 | 127 | 128 | } 129 | -------------------------------------------------------------------------------- /library/src/main/java/com/matpag/clickdrawabletextview/CsDrawableViewManager.java: -------------------------------------------------------------------------------- 1 | package com.matpag.clickdrawabletextview; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.annotation.TargetApi; 5 | import android.content.Context; 6 | import android.content.res.ColorStateList; 7 | import android.content.res.Configuration; 8 | import android.content.res.TypedArray; 9 | import android.graphics.PorterDuff; 10 | import android.graphics.drawable.Drawable; 11 | import android.os.Build; 12 | import android.support.v4.graphics.drawable.DrawableCompat; 13 | import android.text.TextWatcher; 14 | import android.util.AttributeSet; 15 | import android.util.DisplayMetrics; 16 | import android.view.MotionEvent; 17 | import android.view.View; 18 | import android.view.ViewConfiguration; 19 | import android.view.ViewGroup; 20 | import android.view.inputmethod.InputMethodManager; 21 | import android.widget.TextView; 22 | 23 | import com.matpag.clickdrawabletextview.interfaces.OnDrawableClickListener; 24 | 25 | /** 26 | * Wrapper manager for all the ClickDrawableViews which contains the shared logic 27 | * 28 | * Created by Mattia Pagini on 23/04/2017. 29 | */ 30 | 31 | final class CsDrawableViewManager implements ClickableDrawable { 32 | 33 | /** 34 | * This is the reference to the current {@link ClickableDrawable} subclass, one of the view 35 | * between {@link ClickDrawableTextView}, {@link ClickDrawableEditText} or 36 | * {@link ClickDrawableAutoCompleteTextView} 37 | */ 38 | private TextView view; 39 | 40 | private Context mContext; 41 | 42 | //the 4 drawables a view can setup around itself 43 | private CsDrawable mStartDrawable; 44 | private CsDrawable mTopDrawable; 45 | private CsDrawable mEndDrawable; 46 | private CsDrawable mBottomDrawable; 47 | 48 | //the position of the last valid touch performed on one of the drawable 49 | private DrawablePosition mTouchedPosition; 50 | 51 | private static DisplayMetrics mMetrics; 52 | 53 | //default true 54 | private boolean enableTouchOnText = true; 55 | 56 | private Configuration mConfig; 57 | 58 | private TextWatcher mViewTextWatcher; 59 | 60 | /** 61 | * Max allowed duration for a "click", in milliseconds. 62 | * 63 | * I've played a bit with the default android value for recognize a touch 64 | * at {@link ViewConfiguration#getTapTimeout()} but it seemed to me a little to small 65 | * for a normal touch, so i decided to double the amount 66 | */ 67 | private static final int MAX_CLICK_DURATION = ViewConfiguration.getTapTimeout() * 2; 68 | 69 | /** 70 | * Max allowed distance to move during a "click", in DP. 71 | */ 72 | private static final int MAX_CLICK_DISTANCE = 15; 73 | 74 | private float pressedX; 75 | private float pressedY; 76 | private boolean stayedWithinClickDistance; 77 | 78 | /** 79 | * Inteface to listen for drawable click 80 | */ 81 | private OnDrawableClickListener mOnDrawableClickListener; 82 | 83 | CsDrawableViewManager(TextView view){ 84 | this.view = view; 85 | } 86 | 87 | /** 88 | * Init method to call in every {@link TextView} subclass constructor 89 | * @param context the context 90 | * @param attrs the attributes passed from XML 91 | */ 92 | void init(Context context, AttributeSet attrs) { 93 | mContext = context; 94 | mMetrics = context.getResources().getDisplayMetrics(); 95 | mConfig = context.getResources().getConfiguration(); 96 | 97 | if (attrs != null) { 98 | TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.CsDrawableViewManager); 99 | 100 | Drawable startUnwrappedDrawable = a.getDrawable( 101 | R.styleable.CsDrawableViewManager_csStartDrawable); 102 | 103 | if (startUnwrappedDrawable != null) { 104 | Drawable startDrawable = DrawableCompat.wrap(startUnwrappedDrawable); 105 | startDrawable = startDrawable.mutate(); 106 | mStartDrawable = new CsDrawable(context, startDrawable); 107 | int height = a.getDimensionPixelSize( 108 | R.styleable.CsDrawableViewManager_csStartDrawableHeight, -1); 109 | int width = a.getDimensionPixelSize( 110 | R.styleable.CsDrawableViewManager_csStartDrawableWidth, -1); 111 | if (height > -1 && width > -1) { 112 | mStartDrawable.setDrawablePixelSize(height, width); 113 | } 114 | boolean visibility = a.getBoolean( 115 | R.styleable.CsDrawableViewManager_csStartDrawableVisible, true); 116 | mStartDrawable.setVisibility(visibility); 117 | //handle tint and tintMode 118 | ColorStateList tintColor = a.getColorStateList( 119 | R.styleable.CsDrawableViewManager_csStartDrawableTint); 120 | if (tintColor != null){ 121 | DrawableCompat.setTintList(mStartDrawable.getDrawable(), tintColor); 122 | } 123 | PorterDuff.Mode tintMode = parseTintMode(a.getInt( 124 | R.styleable.CsDrawableViewManager_csStartDrawableTintMode, -1)); 125 | if (tintMode != null){ 126 | DrawableCompat.setTintMode(mStartDrawable.getDrawable(), tintMode); 127 | } 128 | } 129 | 130 | Drawable topUnwrappedDrawable = a.getDrawable( 131 | R.styleable.CsDrawableViewManager_csTopDrawable); 132 | if (topUnwrappedDrawable != null) { 133 | Drawable topDrawable = DrawableCompat.wrap(topUnwrappedDrawable); 134 | topDrawable = topDrawable.mutate(); 135 | mTopDrawable = new CsDrawable(context, topDrawable); 136 | int height = a.getDimensionPixelSize( 137 | R.styleable.CsDrawableViewManager_csTopDrawableHeight, -1); 138 | int width = a.getDimensionPixelSize( 139 | R.styleable.CsDrawableViewManager_csTopDrawableWidth, -1); 140 | if (height > -1 && width > -1) { 141 | mTopDrawable.setDrawablePixelSize(height, width); 142 | } 143 | boolean visibility = a.getBoolean( 144 | R.styleable.CsDrawableViewManager_csTopDrawableVisible, true); 145 | mTopDrawable.setVisibility(visibility); 146 | //handle tint and tintMode 147 | ColorStateList tintColor = a.getColorStateList( 148 | R.styleable.CsDrawableViewManager_csTopDrawableTint); 149 | if (tintColor != null){ 150 | DrawableCompat.setTintList(mTopDrawable.getDrawable(), tintColor); 151 | } 152 | PorterDuff.Mode tintMode = parseTintMode(a.getInt( 153 | R.styleable.CsDrawableViewManager_csTopDrawableTintMode, -1)); 154 | if (tintMode != null){ 155 | DrawableCompat.setTintMode(mTopDrawable.getDrawable(), tintMode); 156 | } 157 | } 158 | 159 | Drawable endUnwrappedDrawable = a.getDrawable( 160 | R.styleable.CsDrawableViewManager_csEndDrawable); 161 | if (endUnwrappedDrawable != null) { 162 | Drawable endDrawable = DrawableCompat.wrap(endUnwrappedDrawable); 163 | endDrawable = endDrawable.mutate(); 164 | mEndDrawable = new CsDrawable(context, endDrawable); 165 | int height = a.getDimensionPixelSize( 166 | R.styleable.CsDrawableViewManager_csEndDrawableHeight, -1); 167 | int width = a.getDimensionPixelSize( 168 | R.styleable.CsDrawableViewManager_csEndDrawableWidth, -1); 169 | if (height > -1 && width > -1) { 170 | mEndDrawable.setDrawablePixelSize(height, width); 171 | } 172 | boolean visibility = a.getBoolean( 173 | R.styleable.CsDrawableViewManager_csEndDrawableVisible, true); 174 | mEndDrawable.setVisibility(visibility); 175 | //handle tint and tintMode 176 | ColorStateList tintColor = a.getColorStateList( 177 | R.styleable.CsDrawableViewManager_csEndDrawableTint); 178 | if (tintColor != null){ 179 | DrawableCompat.setTintList(mEndDrawable.getDrawable(), tintColor); 180 | } 181 | PorterDuff.Mode tintMode = parseTintMode(a.getInt( 182 | R.styleable.CsDrawableViewManager_csEndDrawableTintMode, -1)); 183 | if (tintMode != null){ 184 | DrawableCompat.setTintMode(mEndDrawable.getDrawable(), tintMode); 185 | } 186 | } 187 | 188 | Drawable bottomUnwrappedDrawable = a.getDrawable( 189 | R.styleable.CsDrawableViewManager_csBottomDrawable); 190 | if (bottomUnwrappedDrawable != null) { 191 | Drawable bottomDrawable = DrawableCompat.wrap(bottomUnwrappedDrawable); 192 | bottomDrawable = bottomDrawable.mutate(); 193 | mBottomDrawable = new CsDrawable(context, bottomDrawable); 194 | int height = a.getDimensionPixelSize( 195 | R.styleable.CsDrawableViewManager_csBottomDrawableHeight, -1); 196 | int width = a.getDimensionPixelSize( 197 | R.styleable.CsDrawableViewManager_csBottomDrawableWidth, -1); 198 | if (height > -1 && width > -1) { 199 | mBottomDrawable.setDrawablePixelSize(height, width); 200 | } 201 | boolean visibility = a.getBoolean( 202 | R.styleable.CsDrawableViewManager_csBottomDrawableVisible, true); 203 | mBottomDrawable.setVisibility(visibility); 204 | //handle tint and tintMode 205 | ColorStateList tintColor = a.getColorStateList( 206 | R.styleable.CsDrawableViewManager_csBottomDrawableTint); 207 | if (tintColor != null){ 208 | DrawableCompat.setTintList(mBottomDrawable.getDrawable(), tintColor); 209 | } 210 | PorterDuff.Mode tintMode = parseTintMode(a.getInt( 211 | R.styleable.CsDrawableViewManager_csBottomDrawableTintMode, -1)); 212 | if (tintMode != null){ 213 | DrawableCompat.setTintMode(mBottomDrawable.getDrawable(), tintMode); 214 | } 215 | } 216 | 217 | a.recycle(); 218 | } 219 | 220 | invalidateDrawables(); 221 | 222 | setViewOnTouchListener(); 223 | } 224 | 225 | /** 226 | * Check if the current Locale is in RTL mode and if the user has enabled the view to support 227 | * it in theAndroidManifest.xml
of his app
228 | *
229 | * NOTE: We should use ViewCompat.getLayoutDirection(view) ==
230 | * ViewCompat.LAYOUT_DIRECTION_RTL
but when you use developer option : Force RTL Layout
231 | * this is not working correctly (read
232 | * here), checking the configuration is the only rieliable way
233 | * to get the current direction without adding an extra utility to check if the app is running
234 | * in the emulator
235 | *
236 | * @return true if in RTL, false otherwise
237 | */
238 | private boolean isLayoutRTL(){
239 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
240 | return CsDrawableSettings.isRtlSupportEnabled() &&
241 | mConfig.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL;
242 | } else {
243 | return CsDrawableSettings.isRtlSupportEnabled();
244 | }
245 | }
246 |
247 | /**
248 | * Add the drawables to the view
249 | */
250 | private void invalidateDrawables(){
251 | if (isLayoutRTL()) {
252 | addCompoundDrawablesRelative();
253 | } else {
254 | addCompoundDrawables();
255 | }
256 | }
257 |
258 | /**
259 | * Add the compound drawables to the view, using the default method which
260 | * not take into account RTL support
261 | */
262 | private void addCompoundDrawables(){
263 | view.setCompoundDrawables(
264 | mStartDrawable != null ? mStartDrawable.getDrawableIfVisible() : null,
265 | mTopDrawable != null ? mTopDrawable.getDrawableIfVisible() : null,
266 | mEndDrawable != null ? mEndDrawable.getDrawableIfVisible() : null,
267 | mBottomDrawable != null ? mBottomDrawable.getDrawableIfVisible() : null
268 | );
269 | }
270 |
271 | /**
272 | * Add the compound drawables to the view, if the Locale of the user is in
273 | * RTL mode the drawables will be added in the proper position
274 | */
275 | @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
276 | private void addCompoundDrawablesRelative(){
277 | view.setCompoundDrawablesRelative(
278 | mStartDrawable != null ? mStartDrawable.getDrawableIfVisible() : null,
279 | mTopDrawable != null ? mTopDrawable.getDrawableIfVisible() : null,
280 | mEndDrawable != null ? mEndDrawable.getDrawableIfVisible() : null,
281 | mBottomDrawable != null ? mBottomDrawable.getDrawableIfVisible() : null
282 | );
283 | }
284 |
285 | /**
286 | * Handle all the touch events of the current {@link #view} object
287 | */
288 | @SuppressLint("ClickableViewAccessibility")
289 | private void setViewOnTouchListener(){
290 | view.setOnTouchListener((v, e) -> {
291 | switch (e.getAction()) {
292 | case MotionEvent.ACTION_DOWN: {
293 | //if the user clicked on one of the drawables, save some datas about it
294 | if (isOneDrawableTouched(e)) {
295 | pressedX = e.getX();
296 | pressedY = e.getY();
297 | stayedWithinClickDistance = true;
298 | return true;
299 | } else if (!enableTouchOnText){
300 | return true;
301 | }
302 | break;
303 | }
304 | case MotionEvent.ACTION_MOVE: {
305 | //if the user moved the finger to much far from the initial tap point,
306 | //we cancel the touch on the drawable
307 | if (stayedWithinClickDistance &&
308 | distance(pressedX, pressedY, e.getX(), e.getY()) > MAX_CLICK_DISTANCE) {
309 | stayedWithinClickDistance = false;
310 | }
311 | break;
312 | }
313 | case MotionEvent.ACTION_UP: {
314 | // proceed with the drawable touch logic
315 | long eventDuration = e.getEventTime() - e.getDownTime();
316 | if (isOneDrawableTouching()){
317 | if ((eventDuration < MAX_CLICK_DURATION) && stayedWithinClickDistance) {
318 | //dispatch accessibility events
319 | view.performClick();
320 | dispatchDrawableClickEvent();
321 | }
322 | resetTouchedDrawable();
323 | return true;
324 | }
325 | }
326 | }
327 | return view.onTouchEvent(e);
328 | });
329 | }
330 |
331 | /**
332 | * Using the coordinates X and Y of the touch event, we check if they are inside
333 | * the bounds of one of the showing drawables, if true set {@link #mTouchedPosition}
334 | * with one of correct values of {@link DrawablePosition}
335 | */
336 | private boolean isOneDrawableTouched(MotionEvent event){
337 | CsDrawableTouchUtils cdu = new CsDrawableTouchUtils(event, view, isLayoutRTL());
338 | if (mEndDrawable != null && mEndDrawable.isVisible()
339 | && cdu.isEndDrawableTouched(mEndDrawable)){
340 | mTouchedPosition = DrawablePosition.END;
341 | } else if (mStartDrawable != null && mStartDrawable.isVisible()
342 | && cdu.isStartDrawableTouched(mStartDrawable)){
343 | mTouchedPosition = DrawablePosition.START;
344 | } else if (mTopDrawable != null && mTopDrawable.isVisible()
345 | && cdu.isTopDrawableTouched(mTopDrawable)){
346 | mTouchedPosition = DrawablePosition.TOP;
347 | } else if (mBottomDrawable != null && mBottomDrawable.isVisible()
348 | && cdu.isBottomDrawableTouched(mBottomDrawable)){
349 | mTouchedPosition = DrawablePosition.BOTTOM;
350 | }
351 | return mTouchedPosition != null;
352 | }
353 |
354 | /**
355 | * Dispatch click event if the listener has been attached
356 | */
357 | private void dispatchDrawableClickEvent(){
358 | if (mOnDrawableClickListener != null && mTouchedPosition != null){
359 | mOnDrawableClickListener.onClick(view, mTouchedPosition);
360 | }
361 | }
362 |
363 | private void resetTouchedDrawable(){
364 | mTouchedPosition = null;
365 | }
366 |
367 | private boolean isOneDrawableTouching(){
368 | return mTouchedPosition != null;
369 | }
370 |
371 |
372 | @Override
373 | public void setOnDrawableClickListener(OnDrawableClickListener listener){
374 | mOnDrawableClickListener = listener;
375 | }
376 |
377 | @Override
378 | public void removeOnDrawableClickListener(){
379 | mOnDrawableClickListener = null;
380 | }
381 |
382 | @Override
383 | public void addStartCsDrawable(CsDrawable csDrawable) {
384 | mStartDrawable = csDrawable;
385 | invalidateDrawables();
386 | }
387 |
388 | @Override
389 | public void addTopCsDrawable(CsDrawable csDrawable) {
390 | mTopDrawable = csDrawable;
391 | invalidateDrawables();
392 | }
393 |
394 | @Override
395 | public void addEndCsDrawable(CsDrawable csDrawable) {
396 | mEndDrawable = csDrawable;
397 | invalidateDrawables();
398 | }
399 |
400 | @Override
401 | public void addBottomCsDrawable(CsDrawable csDrawable) {
402 | mBottomDrawable = csDrawable;
403 | invalidateDrawables();
404 | }
405 |
406 | @Override
407 | public void showStartCsDrawable(boolean visible) {
408 | if (mStartDrawable.isVisible() != visible){
409 | mStartDrawable.setVisibility(visible);
410 | invalidateDrawables();
411 | }
412 | }
413 |
414 | @Override
415 | public void showTopCsDrawable(boolean visible) {
416 | if (mTopDrawable.isVisible() != visible) {
417 | mTopDrawable.setVisibility(visible);
418 | invalidateDrawables();
419 | }
420 | }
421 |
422 | @Override
423 | public void showEndCsDrawable(boolean visible) {
424 | if (mEndDrawable.isVisible() != visible) {
425 | mEndDrawable.setVisibility(visible);
426 | invalidateDrawables();
427 | }
428 | }
429 |
430 | @Override
431 | public void showBottomCsDrawable(boolean visible) {
432 | if (mBottomDrawable.isVisible() != visible) {
433 | mBottomDrawable.setVisibility(visible);
434 | invalidateDrawables();
435 | }
436 | }
437 |
438 | @Override
439 | public void disableFocusOnText(boolean preventReFocus, boolean closeKeyboard) {
440 | //block refocus on others EditText on the same ViewGroup
441 | if (preventReFocus) {
442 | ViewGroup rootView = (ViewGroup) view.getRootView();
443 | int dfValue = rootView.getDescendantFocusability();
444 | rootView.setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
445 | view.clearFocus();
446 | rootView.setDescendantFocusability(dfValue);
447 | } else {
448 | view.clearFocus();
449 | }
450 | if (closeKeyboard){
451 | //workaround a problem with some keyboard implementations like SwiftKey, where they
452 | //don't remove the underline from text even when the keyboard is closed
453 | if (mViewTextWatcher == null){
454 | view.setText(view.getText());
455 | } else {
456 | //if a TextWatcher is present remove it before calling setText or it could result
457 | //in false positive changes in the user code
458 | view.removeTextChangedListener(mViewTextWatcher);
459 | view.setText(view.getText());
460 | view.addTextChangedListener(mViewTextWatcher);
461 | }
462 | //hide the keyboard if opened
463 | setImeVisibility(false);
464 | }
465 | enableTouchOnText = false;
466 | }
467 |
468 | @Override
469 | public void enableFocusOnText(boolean openKeyboard) {
470 | enableTouchOnText = true;
471 | if (openKeyboard) {
472 | setImeVisibility(true);
473 | view.requestFocus();
474 | }
475 | }
476 |
477 | @Override
478 | public void openKeyboard() {
479 | setImeVisibility(true);
480 | }
481 |
482 | @Override
483 | public void closeKeyboard() {
484 | setImeVisibility(false);
485 | }
486 |
487 |
488 | void addTextWatcher(TextWatcher textWatcher){
489 | mViewTextWatcher = textWatcher;
490 | }
491 |
492 | void removeTextWatcher(){
493 | mViewTextWatcher = null;
494 | }
495 |
496 | /**
497 | * Open the keyboard with some Google trick
498 | * Link here
499 | * @param visible true for open the keyboard, false to close it
500 | */
501 | private void setImeVisibility(final boolean visible) {
502 | if (visible) {
503 | view.post(mShowImeRunnable);
504 | } else {
505 | view.removeCallbacks(mShowImeRunnable);
506 | InputMethodManager imm = (InputMethodManager) mContext
507 | .getSystemService(Context.INPUT_METHOD_SERVICE);
508 | if (imm != null) {
509 | imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
510 | }
511 | }
512 | }
513 |
514 | private Runnable mShowImeRunnable = new Runnable() {
515 | public void run() {
516 | InputMethodManager imm = (InputMethodManager) mContext
517 | .getSystemService(Context.INPUT_METHOD_SERVICE);
518 | if (imm != null) {
519 | imm.showSoftInput(view, 0);
520 | }
521 | }
522 | };
523 |
524 | /**
525 | * Remove all the custom drawables
526 | */
527 | @Override
528 | public void removeAllCsDrawables() {
529 | mStartDrawable = null;
530 | mTopDrawable = null;
531 | mEndDrawable = null;
532 | mBottomDrawable = null;
533 | invalidateDrawables();
534 | }
535 |
536 | /**
537 | * Calculate the distance from 2 point in DP
538 | */
539 | private static float distance(float x1, float y1, float x2, float y2) {
540 | float dx = x1 - x2;
541 | float dy = y1 - y2;
542 | float distanceInPx = (float) Math.sqrt(dx * dx + dy * dy);
543 | return pxToDp(distanceInPx);
544 | }
545 |
546 | private static float pxToDp(float px) {
547 | return px / mMetrics.density;
548 | }
549 |
550 | /**
551 | * Parses a {@link android.graphics.PorterDuff.Mode} from a tintMode
552 | * attribute's enum value.
553 | *
554 | * Copied from the AOSP source in the {@link Drawable} class
555 | * here
556 | */
557 | private static PorterDuff.Mode parseTintMode(int value) {
558 | switch (value) {
559 | case 3: return PorterDuff.Mode.SRC_OVER;
560 | case 5: return PorterDuff.Mode.SRC_IN;
561 | case 9: return PorterDuff.Mode.SRC_ATOP;
562 | case 14: return PorterDuff.Mode.MULTIPLY;
563 | case 15: return PorterDuff.Mode.SCREEN;
564 | case 16: return PorterDuff.Mode.ADD;
565 | default: return null;
566 | }
567 | }
568 |
569 | }
570 |
--------------------------------------------------------------------------------
/library/src/main/java/com/matpag/clickdrawabletextview/DrawablePosition.java:
--------------------------------------------------------------------------------
1 | package com.matpag.clickdrawabletextview;
2 |
3 | /**
4 | * Enum position
5 | * Created by Mattia Pagini on 13/02/2017.
6 | */
7 |
8 | public enum DrawablePosition {
9 | START, //LEFT if not in RTL mode
10 | TOP,
11 | END, //RIGHT if not in RTL mode
12 | BOTTOM
13 | }
--------------------------------------------------------------------------------
/library/src/main/java/com/matpag/clickdrawabletextview/interfaces/OnDrawableClickListener.java:
--------------------------------------------------------------------------------
1 | package com.matpag.clickdrawabletextview.interfaces;
2 |
3 | import android.view.View;
4 |
5 | import com.matpag.clickdrawabletextview.DrawablePosition;
6 |
7 | /**
8 | *
9 | * Interface for handling drawable touch events
10 | *
11 | * Created by Mattia Pagini on 03/03/2017.
12 | */
13 | public interface OnDrawableClickListener {
14 |
15 | /**
16 | * Drawable click event
17 | * @param view one of the subclasses of the {@link android.widget.TextView} widget,
18 | * which received the touch input
19 | * @param position the position of the clicked drawable
20 | */
21 | void onClick(View view, DrawablePosition position);
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/library/src/main/res/values/attrs.xml:
--------------------------------------------------------------------------------
1 |
2 |