1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16package android.databinding.adapters;
17
18import com.android.databinding.library.baseAdapters.R;
19
20import android.databinding.BindingAdapter;
21import android.databinding.BindingMethod;
22import android.databinding.BindingMethods;
23import android.databinding.InverseBindingAdapter;
24import android.databinding.InverseBindingListener;
25import android.graphics.drawable.Drawable;
26import android.os.Build;
27import android.text.Editable;
28import android.text.InputFilter;
29import android.text.InputType;
30import android.text.Spanned;
31import android.text.SpannableString;
32import android.text.SpannableStringBuilder;
33import android.text.TextWatcher;
34import android.text.method.DialerKeyListener;
35import android.text.method.DigitsKeyListener;
36import android.text.method.KeyListener;
37import android.text.method.PasswordTransformationMethod;
38import android.text.method.TextKeyListener;
39import android.util.Log;
40import android.util.TypedValue;
41import android.widget.TextView;
42
43@BindingMethods({
44        @BindingMethod(type = TextView.class, attribute = "android:autoLink", method = "setAutoLinkMask"),
45        @BindingMethod(type = TextView.class, attribute = "android:drawablePadding", method = "setCompoundDrawablePadding"),
46        @BindingMethod(type = TextView.class, attribute = "android:editorExtras", method = "setInputExtras"),
47        @BindingMethod(type = TextView.class, attribute = "android:inputType", method = "setRawInputType"),
48        @BindingMethod(type = TextView.class, attribute = "android:scrollHorizontally", method = "setHorizontallyScrolling"),
49        @BindingMethod(type = TextView.class, attribute = "android:textAllCaps", method = "setAllCaps"),
50        @BindingMethod(type = TextView.class, attribute = "android:textColorHighlight", method = "setHighlightColor"),
51        @BindingMethod(type = TextView.class, attribute = "android:textColorHint", method = "setHintTextColor"),
52        @BindingMethod(type = TextView.class, attribute = "android:textColorLink", method = "setLinkTextColor"),
53        @BindingMethod(type = TextView.class, attribute = "android:onEditorAction", method = "setOnEditorActionListener"),
54})
55public class TextViewBindingAdapter {
56
57    private static final String TAG = "TextViewBindingAdapters";
58    public static final int INTEGER = 0x01;
59    public static final int SIGNED = 0x03;
60    public static final int DECIMAL = 0x05;
61
62    @BindingAdapter("android:text")
63    public static void setText(TextView view, CharSequence text) {
64        final CharSequence oldText = view.getText();
65        if (text == oldText || (text == null && oldText.length() == 0)) {
66            return;
67        }
68        if (text instanceof Spanned) {
69            if (text.equals(oldText)) {
70                return; // No change in the spans, so don't set anything.
71            }
72        } else if (!haveContentsChanged(text, oldText)) {
73            return; // No content changes, so don't set anything.
74        }
75        view.setText(text);
76    }
77
78    @InverseBindingAdapter(attribute = "android:text", event = "android:textAttrChanged")
79    public static String getTextString(TextView view) {
80        return view.getText().toString();
81    }
82
83    @BindingAdapter({"android:autoText"})
84    public static void setAutoText(TextView view, boolean autoText) {
85        KeyListener listener = view.getKeyListener();
86
87        TextKeyListener.Capitalize capitalize = TextKeyListener.Capitalize.NONE;
88
89        int inputType = listener != null ? listener.getInputType() : 0;
90        if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
91            capitalize = TextKeyListener.Capitalize.CHARACTERS;
92        } else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
93            capitalize = TextKeyListener.Capitalize.WORDS;
94        } else if ((inputType & InputType.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
95            capitalize = TextKeyListener.Capitalize.SENTENCES;
96        }
97        view.setKeyListener(TextKeyListener.getInstance(autoText, capitalize));
98    }
99
100    @BindingAdapter({"android:capitalize"})
101    public static void setCapitalize(TextView view, TextKeyListener.Capitalize capitalize) {
102        KeyListener listener = view.getKeyListener();
103
104        int inputType = listener.getInputType();
105        boolean autoText = (inputType & InputType.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
106        view.setKeyListener(TextKeyListener.getInstance(autoText, capitalize));
107    }
108
109    @BindingAdapter({"android:bufferType"})
110    public static void setBufferType(TextView view, TextView.BufferType bufferType) {
111        view.setText(view.getText(), bufferType);
112    }
113
114    @BindingAdapter({"android:digits"})
115    public static void setDigits(TextView view, CharSequence digits) {
116        if (digits != null) {
117            view.setKeyListener(DigitsKeyListener.getInstance(digits.toString()));
118        } else if (view.getKeyListener() instanceof DigitsKeyListener) {
119            view.setKeyListener(null);
120        }
121    }
122
123    @BindingAdapter({"android:numeric"})
124    public static void setNumeric(TextView view, int numeric) {
125        view.setKeyListener(DigitsKeyListener.getInstance((numeric & SIGNED) != 0,
126                (numeric & DECIMAL) != 0));
127    }
128
129    @BindingAdapter({"android:phoneNumber"})
130    public static void setPhoneNumber(TextView view, boolean phoneNumber) {
131        if (phoneNumber) {
132            view.setKeyListener(DialerKeyListener.getInstance());
133        } else if (view.getKeyListener() instanceof DialerKeyListener) {
134            view.setKeyListener(null);
135        }
136    }
137
138    private static void setIntrinsicBounds(Drawable drawable) {
139        if (drawable != null) {
140            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
141        }
142    }
143
144    @BindingAdapter({"android:drawableBottom"})
145    public static void setDrawableBottom(TextView view, Drawable drawable) {
146        setIntrinsicBounds(drawable);
147        Drawable[] drawables = view.getCompoundDrawables();
148        view.setCompoundDrawables(drawables[0], drawables[1], drawables[2], drawable);
149    }
150
151    @BindingAdapter({"android:drawableLeft"})
152    public static void setDrawableLeft(TextView view, Drawable drawable) {
153        setIntrinsicBounds(drawable);
154        Drawable[] drawables = view.getCompoundDrawables();
155        view.setCompoundDrawables(drawable, drawables[1], drawables[2], drawables[3]);
156    }
157
158    @BindingAdapter({"android:drawableRight"})
159    public static void setDrawableRight(TextView view, Drawable drawable) {
160        setIntrinsicBounds(drawable);
161        Drawable[] drawables = view.getCompoundDrawables();
162        view.setCompoundDrawables(drawables[0], drawables[1], drawable,
163                drawables[3]);
164    }
165
166    @BindingAdapter({"android:drawableTop"})
167    public static void setDrawableTop(TextView view, Drawable drawable) {
168        setIntrinsicBounds(drawable);
169        Drawable[] drawables = view.getCompoundDrawables();
170        view.setCompoundDrawables(drawables[0], drawable, drawables[2],
171                drawables[3]);
172    }
173
174    @BindingAdapter({"android:drawableStart"})
175    public static void setDrawableStart(TextView view, Drawable drawable) {
176        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
177            setDrawableLeft(view, drawable);
178        } else {
179            setIntrinsicBounds(drawable);
180            Drawable[] drawables = view.getCompoundDrawablesRelative();
181            view.setCompoundDrawablesRelative(drawable, drawables[1], drawables[2], drawables[3]);
182        }
183    }
184
185    @BindingAdapter({"android:drawableEnd"})
186    public static void setDrawableEnd(TextView view, Drawable drawable) {
187        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
188            setDrawableRight(view, drawable);
189        } else {
190            setIntrinsicBounds(drawable);
191            Drawable[] drawables = view.getCompoundDrawablesRelative();
192            view.setCompoundDrawablesRelative(drawables[0], drawables[1], drawable, drawables[3]);
193        }
194    }
195
196    @BindingAdapter({"android:imeActionLabel"})
197    public static void setImeActionLabel(TextView view, CharSequence value) {
198        view.setImeActionLabel(value, view.getImeActionId());
199    }
200
201    @BindingAdapter({"android:imeActionId"})
202    public static void setImeActionLabel(TextView view, int value) {
203        view.setImeActionLabel(view.getImeActionLabel(), value);
204    }
205
206    @BindingAdapter({"android:inputMethod"})
207    public static void setInputMethod(TextView view, CharSequence inputMethod) {
208        try {
209            Class<?> c = Class.forName(inputMethod.toString());
210            view.setKeyListener((KeyListener) c.newInstance());
211        } catch (ClassNotFoundException e) {
212            Log.e(TAG, "Could not create input method: " + inputMethod, e);
213        } catch (InstantiationException e) {
214            Log.e(TAG, "Could not create input method: " + inputMethod, e);
215        } catch (IllegalAccessException e) {
216            Log.e(TAG, "Could not create input method: " + inputMethod, e);
217        }
218    }
219
220    @BindingAdapter({"android:lineSpacingExtra"})
221    public static void setLineSpacingExtra(TextView view, float value) {
222        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
223            view.setLineSpacing(value, view.getLineSpacingMultiplier());
224        } else {
225            view.setLineSpacing(value, 1);
226        }
227    }
228
229    @BindingAdapter({"android:lineSpacingMultiplier"})
230    public static void setLineSpacingMultiplier(TextView view, float value) {
231        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
232            view.setLineSpacing(view.getLineSpacingExtra(), value);
233        } else {
234            view.setLineSpacing(0, value);
235        }
236    }
237
238    @BindingAdapter({"android:maxLength"})
239    public static void setMaxLength(TextView view, int value) {
240        InputFilter[] filters = view.getFilters();
241        if (filters == null) {
242            filters = new InputFilter[]{
243                    new InputFilter.LengthFilter(value)
244            };
245        } else {
246            boolean foundMaxLength = false;
247            for (int i = 0; i < filters.length; i++) {
248                InputFilter filter = filters[i];
249                if (filter instanceof InputFilter.LengthFilter) {
250                    foundMaxLength = true;
251                    boolean replace = true;
252                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
253                        replace = ((InputFilter.LengthFilter) filter).getMax() != value;
254                    }
255                    if (replace) {
256                        filters[i] = new InputFilter.LengthFilter(value);
257                    }
258                    break;
259                }
260            }
261            if (!foundMaxLength) {
262                // can't use Arrays.copyOf -- it shows up in API 9
263                InputFilter[] oldFilters = filters;
264                filters = new InputFilter[oldFilters.length + 1];
265                System.arraycopy(oldFilters, 0, filters, 0, oldFilters.length);
266                filters[filters.length - 1] = new InputFilter.LengthFilter(value);
267            }
268        }
269        view.setFilters(filters);
270    }
271
272    @BindingAdapter({"android:password"})
273    public static void setPassword(TextView view, boolean password) {
274        if (password) {
275            view.setTransformationMethod(PasswordTransformationMethod.getInstance());
276        } else if (view.getTransformationMethod() instanceof PasswordTransformationMethod) {
277            view.setTransformationMethod(null);
278        }
279    }
280
281    @BindingAdapter({"android:shadowColor"})
282    public static void setShadowColor(TextView view, int color) {
283        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
284            float dx = view.getShadowDx();
285            float dy = view.getShadowDy();
286            float r = view.getShadowRadius();
287            view.setShadowLayer(r, dx, dy, color);
288        }
289    }
290
291    @BindingAdapter({"android:shadowDx"})
292    public static void setShadowDx(TextView view, float dx) {
293        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
294            int color = view.getShadowColor();
295            float dy = view.getShadowDy();
296            float r = view.getShadowRadius();
297            view.setShadowLayer(r, dx, dy, color);
298        }
299    }
300
301    @BindingAdapter({"android:shadowDy"})
302    public static void setShadowDy(TextView view, float dy) {
303        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
304            int color = view.getShadowColor();
305            float dx = view.getShadowDx();
306            float r = view.getShadowRadius();
307            view.setShadowLayer(r, dx, dy, color);
308        }
309    }
310
311    @BindingAdapter({"android:shadowRadius"})
312    public static void setShadowRadius(TextView view, float r) {
313        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
314            int color = view.getShadowColor();
315            float dx = view.getShadowDx();
316            float dy = view.getShadowDy();
317            view.setShadowLayer(r, dx, dy, color);
318        }
319    }
320
321    @BindingAdapter({"android:textSize"})
322    public static void setTextSize(TextView view, float size) {
323        view.setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
324    }
325
326    private static boolean haveContentsChanged(CharSequence str1, CharSequence str2) {
327        if ((str1 == null) != (str2 == null)) {
328            return true;
329        } else if (str1 == null) {
330            return false;
331        }
332        final int length = str1.length();
333        if (length != str2.length()) {
334            return true;
335        }
336        for (int i = 0; i < length; i++) {
337            if (str1.charAt(i) != str2.charAt(i)) {
338                return true;
339            }
340        }
341        return false;
342    }
343
344    @BindingAdapter(value = {"android:beforeTextChanged", "android:onTextChanged",
345            "android:afterTextChanged", "android:textAttrChanged"}, requireAll = false)
346    public static void setTextWatcher(TextView view, final BeforeTextChanged before,
347            final OnTextChanged on, final AfterTextChanged after,
348            final InverseBindingListener textAttrChanged) {
349        final TextWatcher newValue;
350        if (before == null && after == null && on == null && textAttrChanged == null) {
351            newValue = null;
352        } else {
353            newValue = new TextWatcher() {
354                @Override
355                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
356                    if (before != null) {
357                        before.beforeTextChanged(s, start, count, after);
358                    }
359                }
360
361                @Override
362                public void onTextChanged(CharSequence s, int start, int before, int count) {
363                    if (on != null) {
364                        on.onTextChanged(s, start, before, count);
365                    }
366                    if (textAttrChanged != null) {
367                        textAttrChanged.onChange();
368                    }
369                }
370
371                @Override
372                public void afterTextChanged(Editable s) {
373                    if (after != null) {
374                        after.afterTextChanged(s);
375                    }
376                }
377            };
378        }
379        final TextWatcher oldValue = ListenerUtil.trackListener(view, newValue, R.id.textWatcher);
380        if (oldValue != null) {
381            view.removeTextChangedListener(oldValue);
382        }
383        if (newValue != null) {
384            view.addTextChangedListener(newValue);
385        }
386    }
387
388    public interface AfterTextChanged {
389        void afterTextChanged(Editable s);
390    }
391
392    public interface BeforeTextChanged {
393        void beforeTextChanged(CharSequence s, int start, int count, int after);
394    }
395
396    public interface OnTextChanged {
397        void onTextChanged(CharSequence s, int start, int before, int count);
398    }
399}
400