Как обрабатывать клики по строкам ListView, в то время как может быть вызван notifyDataSetChanged?

#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. Могу ли я отредактировать ваш ответ, чтобы включить предложенное вами решение, которое также включает то, что я написал, чего не хватает в вашей идее?