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