#android #android-listview #onclicklistener #android-selector
#Android #android-listview #onclicklistener #android-селектор
Вопрос:
Фон
У меня есть сложный адаптер для ListView.
Каждая строка имеет некоторые внутренние представления, которые должны быть интерактивными (и обрабатывать клики), и у них также есть селекторы (для отображения эффекта касания).
в некоторых случаях notifyDataSetChanged() необходимо вызывать довольно часто (например, один / два раза в секунду), чтобы показать некоторые изменения в элементах ListView.
В качестве примера рассмотрим просмотр списка загружаемых файлов, где вы показываете пользователю ход загрузки каждого файла.
Проблема
Каждый раз, когда вызывается notifyDataSetChanged, событие касания теряется в режиме просмотра касания, поэтому пользователь может пропустить нажатие на него, и особенно пропустить длительное нажатие на него.
Не только это, но и селектор также теряет свое состояние, поэтому, если вы прикоснетесь к нему и увидите эффект, когда вызывается notifyDataSetChanged, представление теряет свое состояние, и вы видите его так, как будто к нему не прикасались.
Это происходит даже для представлений, в которых ничего не обновляется (то есть я просто возвращаю convertView для них).
Пример кода
Приведенный ниже код демонстрирует проблему. Это не исходный код, а очень короткий пример, чтобы было понятно, о чем я говорю.
Опять же, это не исходный код, поэтому я удалил использование ViewHolder и позаботился о том, чтобы щелкнуть, чтобы выполнить некоторые операции, чтобы сделать его простым для чтения и понимания. Но это все та же логика.
Вот код:
MainActivity.java
public class MainActivity extends ActionBarActivity {
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
final ListView listView = (ListView) findViewById(R.id.listView);
final BaseAdapter adapter = new BaseAdapter() {
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
TextView tv = (TextView) convertView;
if (tv == null) {
tv = new TextView(MainActivity.this);
tv.setBackgroundDrawable(getResources().getDrawable(R.drawable.item_background_selector));
tv.setOnClickListener(new OnClickListener() {
@Override
public void onClick(final View v) {
android.util.Log.d("AppLog", "click");
}
});
}
//NOTE: putting the setOnClickListener here won't help either.
final int itemViewType = getItemViewType(position);
tv.setText((itemViewType == 0 ? "A " : "B ") System.currentTimeMillis());
return tv;
}
@Override
public int getItemViewType(final int position) {
return position % 2;
}
@Override
public long getItemId(final int position) {
return position;
}
@Override
public Object getItem(final int position) {
return null;
}
@Override
public int getCount() {
return 100;
}
@Override
public boolean areAllItemsEnabled() {
return false;
}
@Override
public boolean isEnabled(final int position) {
return false;
}
};
listView.setAdapter(adapter);
final Handler handler = new Handler();
handler.postDelayed(new Runnable() {
@Override
public void run() {
// fake notifying
adapter.notifyDataSetChanged();
android.util.Log.d("AppLog", "notifyDataSetChanged");
handler.postDelayed(this, 1000);
}
}, 1000);
}
}
item_background_selector.xml
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true"><shape>
<solid android:color="@android:color/holo_blue_light" />
</shape></item>
<item android:state_focused="true"><shape>
<solid android:color="@android:color/holo_blue_light" />
</shape></item>
<item android:drawable="@android:color/transparent"/>
</selector>
activity_main.xml
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@ id/container"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.test.MainActivity"
tools:ignore="MergeRootFrame" >
<ListView
android:id="@ id/listView"
android:layout_width="match_parent"
android:layout_height="match_parent" >
</ListView>
</FrameLayout>
Частичное решение
Можно обновить только необходимые представления, найдя представление и затем вызвав getView для него, но это обходной путь. Кроме того, это не будет работать в случае добавления / удаления элементов из ListView, для которого необходимо вызвать notifyDataSetChanged. Кроме того, это также приводит к тому, что обновленный вид теряет свое состояние касания.
РЕДАКТИРОВАТЬ: даже частичное решение не работает. Возможно, это вызывает компоновку всего ListView, из-за чего другие представления теряют свои состояния.
Вопрос
Как я могу позволить представлениям оставаться «синхронизированными» с событиями касания после вызова notifyDataSetChanged() ?
Комментарии:
1. Что именно происходит при касании? Если вы сохраняете состояние и вызываете notifydatasetchanged() в конце прослушивателя кликов, это должно сработать
2. в некоторых случаях notifyDataSetChanged() необходимо вызывать довольно часто (например, один / два раза в секунду), чтобы показать некоторые изменения в элементах ListView. — Вам следует переосмыслить это. Почему это требование?
3. Вы пробовали использовать
onTouchEvent
? Возможно, он запускает команду, выполняя вACTION_UP
событии4. @MadhurAhuja Мне нужно обновить то, что отображается на экране. Иногда у вас нет полностью статического ListView. Например, когда вы показываете список загружаемых файлов, вы хотели бы показать их прогресс. Вы пробовали образец, который я написал? Что вы подразумеваете под «поддержанием состояния»?
5. @mapo хорошая идея, но я уже попробовал, и она улавливает только первое событие (касание вниз). больше ничего … если я возвращаю «true», я получаю большинство событий (и даже ACTION_CANCEL) , но я также теряю селектор.
Ответ №1:
Вы не обновляете прослушиватель кликов для «переработанных» просмотров.
Поместите tv.setOnClickListener()
из if (tv == null)
проверки.
Кроме того, свойства, которые вы хотите «синхронизировать», должны быть в модели, поддерживающей ListView. Никогда не доверяйте представлениям для хранения важных данных, они должны отражать только данные из модели.
class Item{
String name;
boolean enabled;
boolean checked
}
class ItemAdapter extends ArrayAdapter<Item>{
@Override
public View getView(final int position, final View convertView, final ViewGroup parent) {
if(convertView == null){
// create new instance
}
// remove all event listeners
Item item = getItem(position);
// set view properties from item (some times, old event listeners will fire when changing view properties , so we have cleared event listeners above)
// setup new event listeners to update properties of view and item
}
}
Комментарии:
1. Нет, это не поможет (и я это уже проверил). одному и тому же представлению не нужно снова и снова получать новый OnClickListener. На самом деле, то, что я написал, является оптимизированным способом избежать все большего количества GCS .
2. Что касается «оставаться синхронизированным», каково решение? Я хочу, чтобы оба щелкали по внутренним представлениям ListView, а также могли обновлять элементы ListView.
3. @androiddeveloper Только если все представления выполнили одно и то же действие (как в вашем примере), в реальных случаях представлениям необходимо будет вызывать события щелчка для этого конкретного индекса / идентификатора элемента, тогда этот подход не сработает.
4. Я не понимаю. Как я уже писал, приведенный выше код не является реальным кодом, который я использую. Это более сложно, и оно будет корректно обрабатывать просмотр щелчка (когда он получает событие). Пожалуйста, посмотрите лекцию Google «Мир ListView». Там объясняется использование ViewHolder, что может вам очень помочь в обработке представлений в ListView . Может быть, мне следует написать больше примечаний в вопросе, чтобы прояснить.
5. @androiddeveloper обновил ответ. Вы можете поддерживать все важные свойства в классе Item, чтобы они сохранялись, даже если представления перерабатываются.
Ответ №2:
Как уже упоминалось, вы можете использовать View.setOnTouchListener()
и перехватывать ACTION_DOWN
и ACTION_UP
событие.
Для анимации выбора вы можете использовать свою пользовательскую цветовую анимацию. Вот пример изменения backgroundColor с анимацией.
Integer colorFrom = getResources().getColor(R.color.red);
Integer colorTo = getResources().getColor(R.color.blue);
ValueAnimator colorAnimation = ValueAnimator.ofObject(new ArgbEvaluator(), colorFrom, colorTo);
colorAnimation.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animator) {
view.setBackgroundColor((Integer)animator.getAnimatedValue());
}
});
colorAnimation.start();
Альтернативное решение
РЕДАКТИРОВАТЬ (с помощью OP, что означает создателя потока): альтернативное решение, основанное на вышесказанном, заключается в использовании TouchListener для установки состояния фона представления.
Недостатком этого (хотя в большинстве случаев это должно быть нормально) является то, что если в списке становится больше / меньше элементов, касание теряется (с помощью ACTION_CANCEL) , поэтому, если вы хотите обработать и это, вы могли бы использовать это событие и обработать его самостоятельно, используя свою собственную логику.
Хотя решение немного странное и не обрабатывает все возможные состояния, оно работает нормально.
Вот пример кода:
public static abstract class StateTouchListener implements OnTouchListener,OnClickListener
{
@Override
public boolean onTouch(final View v,final MotionEvent event)
{
final Drawable background=v.getBackground();
// TODO use GestureDetectorCompat if needed
switch(event.getAction())
{
case MotionEvent.ACTION_CANCEL:
background.setState(new int[] {});
v.invalidate();
break;
case MotionEvent.ACTION_DOWN:
background.setState(new int[] {android.R.attr.state_pressed});
v.invalidate();
break;
case MotionEvent.ACTION_MOVE:
break;
case MotionEvent.ACTION_UP:
background.setState(new int[] {});
v.invalidate();
v.performClick();
onClick(v);
break;
}
return true;
}
}
и исправление в моем коде:
tv.setOnTouchListener(new StateTouchListener()
{
@Override
public void onClick(final View v)
{
android.util.Log.d("Applog","click!");
};
});
Это должно заменить setOnClickListener, который я использовал.
Комментарии:
1. Прошу прощения. Я не понимаю, зачем мне нужно добавлять анимацию и куда следует добавить этот код .
2. Извините, я думал, вы хотите стандартную анимацию клика. В противном случае вы просто можете использовать
setBackgroundColor
для изменения цвета фона для вашего представления при нажатии на представление и изменить его обратно через некоторое время3. Я не знал, что существует стандартная анимация щелчка. но если он существует, зачем мне нужно добавлять пользовательский вместо того, что уже доступно? В любом случае, как мне установить состояние представления? Я знаю, что, вероятно, можно использовать «v.getBackground().setState(…)», но иногда представления внутри или снаружи должны соответствующим образом изменять свое состояние (например, используя «duplicateParentState»)…
4. Я заметил, что это работает, только если я обновляю представление без notifyDataSetChanged, но, как я уже писал, это немного проблематично, поскольку элементы могут быть удалены и вставлены. Но, возможно, это нормально, поскольку порядок может быть изменен, а элемент может быть перемещен в другие места (или удален).
5. Могу ли я отредактировать ваш ответ, чтобы включить предложенное вами решение, которое также включает то, что я написал, чего не хватает в вашей идее?