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