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