package com.anysoftkeyboard.ui.settings.wordseditor; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.view.inputmethod.EditorInfo; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.TextView; import com.menny.android.anysoftkeyboard.R; import java.util.List; /** * List adapter to be used with the words editor fragment. */ class UserWordsListAdapter extends ArrayAdapter<UserWordsListAdapter.Word> implements View.OnClickListener { public static class Word { @NonNull public final String word; public final int frequency; public Word(@NonNull String word, int frequency) { this.word = word; this.frequency = frequency; } @Override public int hashCode() { return word.hashCode() + frequency; } @Override public String toString() { return word; } @Override public boolean equals(Object o) { if (o instanceof Word) { Word otherWord = (Word) o; return otherWord.frequency == frequency && otherWord.word.equals(word); } else { return false; } } } public static interface AdapterCallbacks { void onWordDeleted(Word word); void onWordUpdated(String oldWord, Word newWord); void performDiscardEdit(); } private final LayoutInflater mInflater; private AdapterCallbacks mCallbacksListener; private final int NONE_POSITION = -1; private int mCurrentlyEditPosition = NONE_POSITION; private final int TYPE_NORMAL = 0; private final int TYPE_EDIT = 1; private final int TYPE_ADD = 2; public UserWordsListAdapter(Context context, List<Word> words, AdapterCallbacks callbacks) { super(context, R.id.word_view, words); mCallbacksListener = callbacks; mInflater = LayoutInflater.from(context); } @Override public int getViewTypeCount() { //one for normal, and the second type is "editing" //it will inflate the same layout on both occasions, but will allow use to stop re-creation and re-use of the EDIT view. //the third one is for the "add new word" return 3; } @Override public boolean hasStableIds() { return true; } @Override public long getItemId(int position) { switch (getItemViewType(position)) { case TYPE_EDIT: return 1; case TYPE_ADD: return 2; default: final Word word = getItem(position); return word.hashCode(); } } @Override public int getCount() { final int baseCount = super.getCount(); if (baseCount == 0 && mCurrentlyEditPosition == NONE_POSITION) return 0;//in the case that there are no words (and not editing the first word), I have a special "empty state" return super.getCount() + 1;//the plus one is for the "Add new"; } @Override public int getItemViewType(int position) { if (mCurrentlyEditPosition == position) return TYPE_EDIT; else if (position == super.getCount())//this is the last item, which is an "Add word" item. return TYPE_ADD; else return TYPE_NORMAL; } @Override public View getView(int position, View convertView, ViewGroup parent) { final int viewType = getItemViewType(position); TextView wordView; if (convertView == null) { switch (viewType) { case TYPE_NORMAL: convertView = inflateNormalWordRow(mInflater, parent); assert convertView != null; final View deleteButton = convertView.findViewById(R.id.delete_user_word); deleteButton.setOnClickListener(this); break; case TYPE_EDIT: convertView = inflateEditedWordRow(mInflater, parent); assert convertView != null; final View approveButton = convertView.findViewById(R.id.approve_user_word); approveButton.setOnClickListener(this); wordView = ((TextView) convertView.findViewById(R.id.word_view)); wordView.setOnKeyListener(mOnEditBoxKeyPressedListener); wordView.addTextChangedListener(mOnEditBoxTextChangedListener); wordView.setOnEditorActionListener(mEditBoxActionListener); break; case TYPE_ADD: convertView = inflateAddWordRow(mInflater, parent); assert convertView != null; break; default: throw new IllegalArgumentException("Unknown view type!"); } } wordView = ((TextView) convertView.findViewById(R.id.word_view)); final Word word; //why to check the position against the super.getCount, and not the view type? //good question! In the state where we adding a new word, the underling array is still one short, //so the view type will be "EDIT", but the count will still be one less. if (position == super.getCount()) { word = null;/*empty word at the "add new word" row*/ } else { word = getItem(position); } convertView.setTag(word); switch (viewType) { case TYPE_NORMAL: updateNormalWordRow(convertView, wordView, word); break; case TYPE_EDIT: updateEditedWordRow(convertView, wordView, word); //I want the text-box to take the focus now. wordView.requestFocus(); break; } return convertView; } protected void updateEditedWordRow(View rootView, TextView wordView, Word word) { wordView.setText(word == null? "" : word.word); } protected void updateNormalWordRow(View rootView, TextView wordView, Word word) { wordView.setText(word.word); } protected View inflateAddWordRow(LayoutInflater inflater, ViewGroup parent) { return inflater.inflate(R.layout.user_dictionary_word_row_add, parent, false); } protected View inflateEditedWordRow(LayoutInflater inflater, ViewGroup parent) { return inflater.inflate(R.layout.user_dictionary_word_row_edit, parent, false); } protected View inflateNormalWordRow(LayoutInflater inflater, ViewGroup parent) { return inflater.inflate(R.layout.user_dictionary_word_row, parent, false); } public void onItemClicked(AdapterView<?> listView, int position) { if (mCurrentlyEditPosition == NONE_POSITION && position >= 0) { //nothing was in edit mode, so we start a new one mCurrentlyEditPosition = position; //see http://stackoverflow.com/a/2680077/1324235 listView.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS); } else { //there was an edit in progress. Clicking out side will cause DISCARD. mCurrentlyEditPosition = NONE_POSITION; //see http://stackoverflow.com/a/2680077/1324235 listView.setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS); listView.requestFocus(); } notifyDataSetChanged(); } @Override public final void onClick(View v) { final Word word = (Word) ((View) v.getParent()).getTag(); switch (v.getId()) { case R.id.delete_user_word: onWordDeleted(word); break; case R.id.approve_user_word: Word newWord = onWordEditApproved(v, word); mCurrentlyEditPosition = NONE_POSITION; if (newWord == null || TextUtils.isEmpty(newWord.word) || newWord.frequency == 0) { //this is weird.. The user wanted the word to be deleted? //why not clicking on the delete icon?! //I'm ignoring. notifyDataSetChanged();//reloading the list. } else { mCallbacksListener.onWordUpdated(word == null? "" : word.word, newWord); } break; } } protected void onWordDeleted(Word word) { mCallbacksListener.onWordDeleted(word); } protected Word onWordEditApproved(View approveButton, @Nullable Word oldWord) { View parent = ((View) approveButton.getParent()); EditText editBox = (EditText) parent.findViewById(R.id.word_view); final String newWord = editBox.getText().toString(); if (TextUtils.isEmpty(newWord)) { return null; } else { return new Word(newWord, oldWord == null? 128 : oldWord.frequency); } } protected final View.OnKeyListener mOnEditBoxKeyPressedListener = new View.OnKeyListener() { @Override public boolean onKey(View v, int keyCode, KeyEvent event) { switch (keyCode) { case KeyEvent.KEYCODE_BACK: //discarded! mCallbacksListener.performDiscardEdit(); return true; case KeyEvent.KEYCODE_ENTER: EditText edit = (EditText)v; if ((edit.getImeOptions() & EditorInfo.IME_ACTION_DONE) == EditorInfo.IME_ACTION_DONE) { View parent = (View) v.getParent(); View approveButton = parent.findViewById(R.id.approve_user_word); onClick(approveButton); } else if ((edit.getImeOptions() & EditorInfo.IME_ACTION_NEXT) == EditorInfo.IME_ACTION_NEXT) { View nextField = edit.focusSearch(View.FOCUS_RIGHT); if (nextField != null) nextField.requestFocus(); } return true; default: return false; } } }; protected final TextView.OnEditorActionListener mEditBoxActionListener = new TextView.OnEditorActionListener() { @Override public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { View parent = (View) v.getParent(); View approveButton = parent.findViewById(R.id.approve_user_word); onClick(approveButton); return true; } }; private final TextWatcher mOnEditBoxTextChangedListener = new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void afterTextChanged(Editable s) { } }; }