Таким образом, у меня возникли проблемы с расширением MultiAutoCompleteTextView
и поддержкой его с помощью CursorLoader
, одновременно используя пользовательский Tokenizer
. Проблема возникает именно с вызовом mAdapter.setCursorToStringConverter();
. Метод convertToString()
, который имеет курсор в качестве аргумента, имеет действительный и незамкнутый курсор при первом вызове этого метода. Однако последующие вызовы вызывают либо нулевой курсор, либо замкнутый курсор. Я предполагаю, что это имеет какое-то отношение к тому, как LoaderManager
управляет CursorLoader
.
Если я прокомментирую метод setCursorToStringConverter()
, тогда я вижу список доступных вариантов, основанный на тексте, который я ввел в это представление. Однако, поскольку метод convertToString()
не реализован, тогда метод terminateToken()
пользовательского Tokenizer
не получает строку, которую я предполагаю, а скорее представляющая строка объекта курсора, поскольку курсор имеет не используется для получения текущего строкового значения нужного столбца в результате запроса.
Кто-нибудь смог реализовать комбинацию из трех классов (CursorLoader/LoaderManger
, MultiAutoCompleteTextView
и Tokenizer
)?
Я иду в правильном направлении с этим, или это просто невозможно?
Я смог реализовать пользовательский MultiAutoCompleteTextView
, поддерживаемый SimpleCursorAdapter
, а также пользовательский Tokenizer
. Мне просто интересно, возможно ли реализовать это с помощью CursorLoader
, так как Strict Mode жалуется на то, что курсор в MultiAutoCompleteTextView
не закрыт явно.
Любая помощь будет принята с благодарностью.
public class CustomMultiAutoCompleteTextView extends MultiAutoCompleteTextView
implements LoaderManager.LoaderCallbacks<Cursor> {
private final String DEBUG_TAG = getClass().getSimpleName().toString();
private Messenger2 mContext;
private RecipientsCursorAdapter mAdapter;
private ContentResolver mContentResolver;
private final char delimiter = ' ';
private CustomMultiAutoCompleteTextView mView;
// If non-null, this is the current filter the user has provided.
private String mCurFilter;
// These are the Contacts rows that we will retrieve.
final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME };
public CustomMultiAutoCompleteTextView(Context c) {
super(c);
init(c);
}
public CustomMultiAutoCompleteTextView(Context c, AttributeSet attrs) {
super(c, attrs);
init(c);
}
private void init(Context context) {
mContext = (Messenger2) context;
mContentResolver = mContext.getContentResolver();
mView = this;
mAdapter = new RecipientsCursorAdapter(mContext, 0, null, new String[0], new int[0], mContext);
mAdapter.setCursorToStringConverter(new CursorToStringConverter() {
@Override
public CharSequence convertToString(Cursor c) {
String contactName = c.getString(c.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME));
return contactName;
}
});
addTextChangedListener(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) {
Log.d(DEBUG_TAG, "onTextChanged()");
if (!s.equals(""))
mCurFilter = s.toString();
else
mCurFilter = "";
mContext.getLoaderManager().restartLoader(0, null, mView);
}
@Override
public void afterTextChanged(Editable s) {
}
});
setAdapter(mAdapter);
setTokenizer(new SpaceTokenizer());
mContext.getLoaderManager().initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created. This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Log.d(DEBUG_TAG, "onCreateLoader()");
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_FILTER_URI,Uri.encode(mCurFilter));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME
+ " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";
return new CursorLoader(mContext, baseUri, CONTACTS_SUMMARY_PROJECTION,
selection, null, sortOrder);
}
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing
// the old cursor once we return.)
Log.d(DEBUG_TAG, "onLoadFinished()");
mAdapter.swapCursor(data);
}
public void onLoaderReset(Loader<Cursor> loader) {
// This is called when the last Cursor provided to onLoadFinished()
// above is about to be closed. We need to make sure we are no
// longer using it.
Log.d(DEBUG_TAG, "onLoaderReset()");
mAdapter.swapCursor(null);
}
private class SpaceTokenizer implements Tokenizer {
public int findTokenStart(CharSequence text, int cursor) {
int i = cursor;
while (i > 0 && text.charAt(i - 1) != delimiter) {
i--;
}
while (i < cursor && text.charAt(i) == delimiter) {
i++;
}
return i;
}
public int findTokenEnd(CharSequence text, int cursor) {
int i = cursor;
int len = text.length();
while (i < len) {
if (text.charAt(i) == delimiter) {
return i;
} else {
i++;
}
}
return len;
}
public CharSequence terminateToken(CharSequence text) {
Log.d(DEBUG_TAG, "terminateToken()");
int i = text.length();
while (i > 0 && text.charAt(i - 1) == delimiter) {
i--;
}
if (i > 0 && text.charAt(i - 1) == delimiter) {
return text;
} else {
CharSequence contactName = createContactBubble(text);
return contactName;
}
}
}
}
ОБНОВЛЕНИЕ 1
Теперь я вызываю метод setStringConversionColumn()
вместо setCursorToStringConverter()
, как предлагал @Olaf. Я установил это в onLoadFinished()
, так как это единственный раз, когда доступен Cursor
, поскольку он реализует LoaderManger
.
public void onLoadFinished(Loader<Cursor> loader, Cursor data) {
// Swap the new cursor in. (The framework will take care of closing
// the old cursor once we return.)
Log.d(DEBUG_TAG, "onLoadFinished()");
mAdapter.setStringConversionColumn(data.getColumnIndexOrThrow(ContactsContract.Contacts.DISPLAY_NAME));
mAdapter.swapCursor(data);
}
Это работает при выборе одного элемента для MultiAutoCompleteTextView
, но не позволяет выбрать несколько элементов в MultiAutoCompleteTextView
.
Я предполагаю, что есть некоторая проблема с методом onTextChanged()
, так как она вызывает restartLoader()
. Это работает для первой записи в этом представлении, но не для последующих записей. Я не слишком уверен в этом, что не так.
ОБНОВЛЕНИЕ 2
Итак, я определил проблему. Проблема заключается в методе TextWatcher onTextChanged()
. Сделав выделение для завершения первого токена (предположим, что токен был "Джо Джонсон" ), затем введите в этот MultiAutoCompleteTextView
(например, al
) больше символов, которые передаются в onTextChanged()
теперь содержит не только добавочные символы, но и символы из токена, который ранее был прерван (значение s
в этой точке равно Joe Johnson al
). Теперь значение mCursor
устанавливается в Joe Johnson al
, которое впоследствии передается в запрос в onCreateLoader()
, который, очевидно, не возвращает никаких результатов. Есть ли какие-либо пути в этой ситуации? Я открыт для любых предложений.
ОБНОВЛЕНИЕ 3
Когда я реализовал пользовательский MultiAutoCompleteTextView
, поддерживаемый SimpleCursorAdapter
, а также пользовательский Tokenizer
, я установил FilterQueryProvider
следующим образом:
mAdapter.setFilterQueryProvider(new FilterQueryProvider() {
@Override
public Cursor runQuery(CharSequence constraint) {
Log.d(DEBUG_TAG, "runQuery() : constraint " + constraint);
Uri baseUri;
if (constraint != null) {
baseUri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_FILTER_URI,
Uri.encode(constraint.toString()));
} else {
baseUri = ContactsContract.Contacts.CONTENT_URI;
}
String selection = "((" + ContactsContract.Contacts.DISPLAY_NAME
+ " NOTNULL) AND ("
+ ContactsContract.Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ ContactsContract.Contacts.DISPLAY_NAME + " != '' ))";
final String[] CONTACTS_SUMMARY_PROJECTION = new String[] {
ContactsContract.Contacts._ID,
ContactsContract.Contacts.DISPLAY_NAME};
String sortOrder = ContactsContract.Contacts.DISPLAY_NAME
+ " COLLATE LOCALIZED ASC";
Cursor c = mContentResolver.query(baseUri,
CONTACTS_SUMMARY_PROJECTION, selection, null, sortOrder);
return c;
}
});
И по какой-либо причине метод runQuery()
дважды вызывается из метода TextWatcher onTextChanged()
:
public void onTextChanged(CharSequence s, int start, int before,
int count) {
Log.d(DEBUG_TAG, "onTextChanged() : s " + s);
mAdapter.getFilterQueryProvider().runQuery(s);
}
Итак, в моем предыдущем примере переменная constraint
, которая впервые передается методу runQuery()
, равна Joe Johnson al
. Тогда второй раз runQuery()
метод называется значением переменной constraint
al
. Я не знаю, почему метод runQuery()
выполняется дважды, когда его только один раз вызывает метод onTextChanged()
.