TextFieldsEditorView.java revision da5bf1cf60beef3de5e651a569fa544293683926
1/*
2 * Copyright (C) 2010 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 */
16
17package com.android.contacts.editor;
18
19import com.android.contacts.ContactsUtils;
20import com.android.contacts.R;
21import com.android.contacts.model.AccountType.EditField;
22import com.android.contacts.model.DataKind;
23import com.android.contacts.model.EntityDelta;
24import com.android.contacts.model.EntityDelta.ValuesDelta;
25import com.android.contacts.util.NameConverter;
26
27import android.content.Context;
28import android.content.Entity;
29import android.graphics.Rect;
30import android.graphics.Typeface;
31import android.os.Parcel;
32import android.os.Parcelable;
33import android.provider.ContactsContract.CommonDataKinds.StructuredName;
34import android.telephony.PhoneNumberFormattingTextWatcher;
35import android.text.Editable;
36import android.text.InputType;
37import android.text.Spannable;
38import android.text.TextUtils;
39import android.text.TextWatcher;
40import android.text.style.StyleSpan;
41import android.util.AttributeSet;
42import android.view.Gravity;
43import android.view.View;
44import android.view.ViewGroup;
45import android.widget.EditText;
46import android.widget.ImageView;
47import android.widget.LinearLayout;
48
49import java.util.Map;
50
51/**
52 * Simple editor that handles labels and any {@link EditField} defined for the
53 * entry. Uses {@link ValuesDelta} to read any existing {@link Entity} values,
54 * and to correctly write any changes values.
55 */
56public class TextFieldsEditorView extends LabeledEditorView {
57    private EditText[] mFieldEditTexts = null;
58    private ViewGroup mFields = null;
59    private View mExpansionViewContainer;
60    private ImageView mExpansionView;
61    private boolean mHideOptional = true;
62    private boolean mHasShortAndLongForms;
63    private int mEditorTextSize = 0;
64
65    public TextFieldsEditorView(Context context) {
66        super(context);
67    }
68
69    public TextFieldsEditorView(Context context, AttributeSet attrs) {
70        super(context, attrs);
71    }
72
73    public TextFieldsEditorView(Context context, AttributeSet attrs, int defStyle) {
74        super(context, attrs, defStyle);
75    }
76
77    /** {@inheritDoc} */
78    @Override
79    protected void onFinishInflate() {
80        super.onFinishInflate();
81
82        setDrawingCacheEnabled(true);
83        setAlwaysDrawnWithCacheEnabled(true);
84
85        mFields = (ViewGroup) findViewById(R.id.editors);
86        mExpansionView = (ImageView) findViewById(R.id.expansion_view);
87        mExpansionViewContainer = findViewById(R.id.expansion_view_container);
88        mExpansionViewContainer.setOnClickListener(new OnClickListener() {
89            @Override
90            public void onClick(View v) {
91                // Save focus
92                final View focusedChild = getFocusedChild();
93                final int focusedViewId = focusedChild == null ? -1 : focusedChild.getId();
94
95                // Reconfigure GUI
96                mHideOptional = !mHideOptional;
97                onOptionalFieldVisibilityChange();
98                rebuildValues();
99
100                // Restore focus
101                View newFocusView = findViewById(focusedViewId);
102                if (newFocusView == null || newFocusView.getVisibility() == GONE) {
103                    // find first visible child
104                    newFocusView = TextFieldsEditorView.this;
105                }
106                newFocusView.requestFocus();
107            }
108        });
109    }
110
111    /**
112     * Set the text size of the value of all fields in this class, which will override the default
113     * text appearance style for the associated {@link DataKind}.
114     */
115    public void setEditorTextSize(int textSize) {
116        mEditorTextSize = textSize;
117    }
118
119    @Override
120    public void setEnabled(boolean enabled) {
121        super.setEnabled(enabled);
122
123        if (mFieldEditTexts != null) {
124            for (int index = 0; index < mFieldEditTexts.length; index++) {
125                mFieldEditTexts[index].setEnabled(!isReadOnly() && enabled);
126            }
127        }
128        mExpansionView.setEnabled(!isReadOnly() && enabled);
129    }
130
131    /**
132     * Creates or removes the type/label button. Doesn't do anything if already correctly configured
133     */
134    private void setupExpansionView(boolean shouldExist, boolean collapsed) {
135        if (shouldExist) {
136            mExpansionViewContainer.setVisibility(View.VISIBLE);
137            mExpansionView.setImageResource(collapsed
138                    ? R.drawable.ic_menu_expander_minimized_holo_light
139                    : R.drawable.ic_menu_expander_maximized_holo_light);
140        } else {
141            mExpansionViewContainer.setVisibility(View.GONE);
142        }
143    }
144
145    @Override
146    protected void requestFocusForFirstEditField() {
147        if (mFieldEditTexts != null && mFieldEditTexts.length != 0) {
148            EditText firstField = null;
149            boolean anyFieldHasFocus = false;
150            for (EditText editText : mFieldEditTexts) {
151                if (firstField == null && editText.getVisibility() == View.VISIBLE) {
152                    firstField = editText;
153                }
154                if (editText.hasFocus()) {
155                    anyFieldHasFocus = true;
156                    break;
157                }
158            }
159            if (!anyFieldHasFocus && firstField != null) {
160                firstField.requestFocus();
161            }
162        }
163    }
164
165    @Override
166    public void setValues(DataKind kind, ValuesDelta entry, EntityDelta state, boolean readOnly,
167            ViewIdGenerator vig) {
168        super.setValues(kind, entry, state, readOnly, vig);
169        // Remove edit texts that we currently have
170        if (mFieldEditTexts != null) {
171            for (EditText fieldEditText : mFieldEditTexts) {
172                mFields.removeView(fieldEditText);
173            }
174        }
175        boolean hidePossible = false;
176
177        int fieldCount = kind.fieldList.size();
178        mFieldEditTexts = new EditText[fieldCount];
179        for (int index = 0; index < fieldCount; index++) {
180            final EditField field = kind.fieldList.get(index);
181            final EditText fieldView = new EditText(mContext);
182            fieldView.setLayoutParams(new LinearLayout.LayoutParams(LayoutParams.MATCH_PARENT,
183                    LayoutParams.WRAP_CONTENT));
184            fieldView.setTextAppearance(getContext(), kind.textAppearanceResourceId);
185            if (mEditorTextSize != 0) {
186                fieldView.setTextSize(mEditorTextSize);
187            }
188            fieldView.setGravity(Gravity.TOP);
189            mFieldEditTexts[index] = fieldView;
190            fieldView.setId(vig.getId(state, kind, entry, index));
191            if (field.titleRes > 0) {
192                fieldView.setHint(field.titleRes);
193            }
194            int inputType = field.inputType;
195            fieldView.setInputType(inputType);
196            if (field.isFullName) {
197                fieldView.addTextChangedListener(new NameFormattingTextWatcher());
198            }
199            if (inputType == InputType.TYPE_CLASS_PHONE) {
200                fieldView.addTextChangedListener(new PhoneNumberFormattingTextWatcher(
201                        ContactsUtils.getCurrentCountryIso(mContext)));
202            }
203            fieldView.setMinLines(field.minLines);
204
205            // Read current value from state
206            final String column = field.column;
207            final String value = entry.getAsString(column);
208            fieldView.setText(value);
209
210            // Prepare listener for writing changes
211            fieldView.addTextChangedListener(new TextWatcher() {
212                @Override
213                public void afterTextChanged(Editable s) {
214                    // Trigger event for newly changed value
215                    onFieldChanged(column, s.toString());
216                }
217
218                @Override
219                public void beforeTextChanged(CharSequence s, int start, int count, int after) {
220                }
221
222                @Override
223                public void onTextChanged(CharSequence s, int start, int before, int count) {
224                }
225            });
226
227            fieldView.setEnabled(isEnabled() && !readOnly);
228
229            if (field.shortForm) {
230                hidePossible = true;
231                mHasShortAndLongForms = true;
232                fieldView.setVisibility(mHideOptional ? View.VISIBLE : View.GONE);
233            } else if (field.longForm) {
234                hidePossible = true;
235                mHasShortAndLongForms = true;
236                fieldView.setVisibility(mHideOptional ? View.GONE : View.VISIBLE);
237            } else {
238                // Hide field when empty and optional value
239                final boolean couldHide = (!ContactsUtils.isGraphic(value) && field.optional);
240                final boolean willHide = (mHideOptional && couldHide);
241                fieldView.setVisibility(willHide ? View.GONE : View.VISIBLE);
242                hidePossible = hidePossible || couldHide;
243            }
244
245            mFields.addView(fieldView);
246        }
247
248        // When hiding fields, place expandable
249        setupExpansionView(hidePossible, mHideOptional);
250        mExpansionView.setEnabled(!readOnly && isEnabled());
251    }
252
253    @Override
254    public boolean isEmpty() {
255        for (int i = 0; i < mFields.getChildCount(); i++) {
256            EditText editText = (EditText) mFields.getChildAt(i);
257            if (!TextUtils.isEmpty(editText.getText())) {
258                return false;
259            }
260        }
261        return true;
262    }
263
264    /**
265     * Returns true if the editor is currently configured to show optional fields.
266     */
267    public boolean areOptionalFieldsVisible() {
268        return !mHideOptional;
269    }
270
271    public boolean hasShortAndLongForms() {
272        return mHasShortAndLongForms;
273    }
274
275    /**
276     * Populates the bound rectangle with the bounds of the last editor field inside this view.
277     */
278    public void acquireEditorBounds(Rect bounds) {
279        if (mFieldEditTexts != null) {
280            for (int i = mFieldEditTexts.length; --i >= 0;) {
281                EditText editText = mFieldEditTexts[i];
282                if (editText.getVisibility() == View.VISIBLE) {
283                    bounds.set(editText.getLeft(), editText.getTop(), editText.getRight(),
284                            editText.getBottom());
285                    return;
286                }
287            }
288        }
289    }
290
291    /**
292     * Saves the visibility of the child EditTexts, and mHideOptional.
293     */
294    @Override
295    protected Parcelable onSaveInstanceState() {
296        Parcelable superState = super.onSaveInstanceState();
297        SavedState ss = new SavedState(superState);
298
299        ss.mHideOptional = mHideOptional;
300
301        final int numChildren = mFieldEditTexts == null ? 0 : mFieldEditTexts.length;
302        ss.mVisibilities = new int[numChildren];
303        for (int i = 0; i < numChildren; i++) {
304            ss.mVisibilities[i] = mFieldEditTexts[i].getVisibility();
305        }
306
307        return ss;
308    }
309
310    /**
311     * Restores the visibility of the child EditTexts, and mHideOptional.
312     */
313    @Override
314    protected void onRestoreInstanceState(Parcelable state) {
315        SavedState ss = (SavedState) state;
316        super.onRestoreInstanceState(ss.getSuperState());
317
318        mHideOptional = ss.mHideOptional;
319
320        int numChildren = Math.min(mFieldEditTexts.length, ss.mVisibilities.length);
321        for (int i = 0; i < numChildren; i++) {
322            mFieldEditTexts[i].setVisibility(ss.mVisibilities[i]);
323        }
324    }
325
326    private static class SavedState extends BaseSavedState {
327        public boolean mHideOptional;
328        public int[] mVisibilities;
329
330        SavedState(Parcelable superState) {
331            super(superState);
332        }
333
334        private SavedState(Parcel in) {
335            super(in);
336            mVisibilities = new int[in.readInt()];
337            in.readIntArray(mVisibilities);
338        }
339
340        @Override
341        public void writeToParcel(Parcel out, int flags) {
342            super.writeToParcel(out, flags);
343            out.writeInt(mVisibilities.length);
344            out.writeIntArray(mVisibilities);
345        }
346
347        @SuppressWarnings({"unused", "hiding" })
348        public static final Parcelable.Creator<SavedState> CREATOR
349                = new Parcelable.Creator<SavedState>() {
350            @Override
351            public SavedState createFromParcel(Parcel in) {
352                return new SavedState(in);
353            }
354
355            @Override
356            public SavedState[] newArray(int size) {
357                return new SavedState[size];
358            }
359        };
360    }
361
362    private class NameFormattingTextWatcher implements TextWatcher {
363
364
365        @Override
366        public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
367
368        @Override
369        public void onTextChanged(CharSequence s, int start, int before, int count) {}
370
371        @Override
372        public void afterTextChanged(Editable s) {
373            String displayName = s.toString();
374            Map<String, String> structuredName = NameConverter.displayNameToStructuredName(
375                    getContext(), displayName);
376            String givenName = structuredName.get(StructuredName.GIVEN_NAME);
377            if (!TextUtils.isEmpty(givenName)) {
378                int spanStart = -1;
379                int spanEnd = -1;
380                if (displayName.startsWith(givenName + " ")) {
381                    spanStart = 0;
382                    spanEnd = givenName.length();
383                } else {
384                    spanStart = displayName.lastIndexOf(" " + givenName);
385                    if (spanStart > -1) {
386                        spanStart++;
387                        spanEnd = spanStart + givenName.length();
388                    }
389                }
390
391                // If the requested range is already bolded, don't make any changes.
392                if (spanStart > -1) {
393                    StyleSpan[] existingSpans = s.getSpans(0, s.length(), StyleSpan.class);
394                    for (StyleSpan span : existingSpans) {
395                        if (span.getStyle() == Typeface.BOLD
396                                && s.getSpanStart(span.getUnderlying()) == spanStart
397                                && s.getSpanEnd(span.getUnderlying()) == spanEnd) {
398                            // Nothing to do - the correct portion is already bolded.
399                            return;
400                        }
401                    }
402
403                    // Clear any existing bold style spans.
404                    for (StyleSpan span : existingSpans) {
405                        if (span.getStyle() == Typeface.BOLD) {
406                            s.removeSpan(span);
407                        }
408                    }
409
410                    // Set the new bold span.
411                    s.setSpan(new StyleSpan(Typeface.BOLD), spanStart, spanEnd,
412                            Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
413                }
414            }
415        }
416    }
417}
418