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

Задний план

У меня есть сложный адаптер для 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() ?


person android developer    schedule 02.07.2014    source источник
comment
Что именно происходит при касании? Если вы поддерживаете состояние и вызываете notifydatasetchanged() в конце прослушивателя кликов, это должно работать   -  person Madhur Ahuja    schedule 02.07.2014
comment
в некоторых случаях notifyDataSetChanged() необходимо вызывать довольно часто (например, один раз/два раза в секунду), чтобы показать некоторые изменения в элементах listView. - Вы должны переосмыслить это. Почему это требование?   -  person Madhur Ahuja    schedule 02.07.2014
comment
Вы пробовали использовать onTouchEvent ? Возможно, он запускает команду, выполняя событие ACTION_UP   -  person mapodev    schedule 02.07.2014
comment
@MadhurAhuja Мне нужно обновить то, что показано на экране. Иногда у вас нет полностью статического listView. Например, когда вы показываете список загружаемых файлов, вы хотите показать их ход. Вы пробовали образец, который я написал? Что вы подразумеваете под сохранением состояния?   -  person android developer    schedule 02.07.2014
comment
@mapo хорошая идея, но я попробовал, и она ловит только первое событие (касание). больше ничего... если я верну true , я получу большинство событий (и даже ACTION_CANCEL) , но также потеряю селектор.   -  person android developer    schedule 02.07.2014
comment
так что это не решает вашу проблему, когда вы просто ловите состояние для ACTION_DOWN. Событие длинного клика будет потеряно, но у вас есть хотя бы событие клика.   -  person mapodev    schedule 02.07.2014
comment
@mapo почти, так как пользователь может щелкнуть крошечный момент перед вызовом notifyDataSetChanged, что потеряет эффект щелчка. Кроме того, щелчок не обрабатывается на ACTION_DOWN. Это обрабатывается на ACTION_UP, чтобы различать длинные щелчки, прокрутку, щелчки и т. д. На данный момент это лучшее решение, которое я могу найти, но оно тоже не очень хорошее... :(   -  person android developer    schedule 02.07.2014
comment
@mapo На самом деле, теперь, когда я попробовал, кажется, что это должно работать (при возврате true для сенсорного прослушивателя). Действия, которые я получаю, точно соответствуют тому, что я должен обрабатывать. Вопрос в том, как я могу позволить селектору представления обрабатывать текущее состояние? Я имею в виду, как сделать так, чтобы эффект прикосновения был виден пользователю?   -  person android developer    schedule 02.07.2014
comment
@mapo Я мог бы изменить состояние фона, отображаемого в представлении, но является ли это законной операцией? Может ли это вызвать какие-либо проблемы с представлением и тем, как оно отображается?   -  person android developer    schedule 02.07.2014
comment
я думаю, вы могли бы сделать свою собственную анимацию представления. поэтому, когда вы касаетесь его, вы можете изменить цвет фона представления с помощью перехода, так что да, я думаю, что это законная операция.   -  person mapodev    schedule 02.07.2014


Ответы (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, который я использовал.

person mapodev    schedule 02.07.2014
comment
Мне жаль. Я не понимаю, зачем мне добавлять анимацию, и куда этот код нужно добавить в . - person android developer; 02.07.2014
comment
Извините, я думал, вам нужна стандартная анимация кликов. В противном случае вы просто можете использовать setBackgroundColor для изменения цвета фона для вашего представления при нажатии на представление и изменить его обратно через некоторое время. - person mapodev; 02.07.2014
comment
Я не знал, что есть стандартная анимация щелчка. но если он существует, зачем мне добавлять пользовательский вместо того, что уже доступно? В любом случае, как мне установить состояние представления? Я знаю, что, вероятно, можно использовать v.getBackground().setState(...) , но иногда представлениям внутри или снаружи необходимо соответствующим образом изменить свое состояние (например, с помощью DubaiParentState)... - person android developer; 02.07.2014
comment
Я заметил, что это работает, только если я обновляю представление без notifyDataSetChanged, но, как я уже писал, это немного проблематично, поскольку элементы можно удалять и вставлять. Но, может быть, это нормально, так как порядок может быть изменен, а предмет может быть перемещен в другие места (или удален). - person android developer; 02.07.2014
comment
Могу ли я отредактировать ваш ответ, включив в него предложенное вами решение, которое также включает то, что я написал, чего не хватает в вашей идее? - person android developer; 02.07.2014
comment
хорошо, я отредактировал это. теперь все должно быть хорошо. если у вас есть какие-либо предложения по его улучшению, пожалуйста, напишите об этом. - person android developer; 04.07.2014

Вы не обновляете прослушиватель кликов «переработанных» представлений.

Выложите 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
 }

}
person S.D.    schedule 02.07.2014
comment
Нет, это не поможет (и я это уже проверил). одному и тому же представлению не нужно снова и снова получать новый onClickListener. На самом деле то, что я написал, — это оптимизированный способ избежать все большего количества сборщиков мусора. - person android developer; 02.07.2014
comment
Насчет синхронизации, какое решение? Я хочу, чтобы как щелкнуть внутренние представления listView, так и иметь возможность обновлять элементы listView. - person android developer; 02.07.2014
comment
@androiddeveloper Только в том случае, если все представления выполняли одно и то же действие (как в вашем примере). В реальных случаях представления должны будут вызывать события щелчка для этого конкретного индекса / идентификатора элемента, тогда этот подход не будет работать. - person S.D.; 02.07.2014
comment
Я не понимаю. Как я уже писал, приведенный выше код не является реальным кодом, который я использую. Это более сложно, и он будет правильно обрабатывать щелкнутое представление (когда он получит событие). Пожалуйста, посмотрите лекцию о мире listView от Google. Там объясняется использование ViewHolder, что может очень помочь вам с обработкой представлений в listView. Может быть, мне следует написать больше примечаний в вопросе, чтобы было ясно. - person android developer; 02.07.2014
comment
@androiddeveloper обновил ответ. Вы можете поддерживать все важные свойства в классе Item, чтобы они сохранялись, даже если представления перерабатываются. - person S.D.; 02.07.2014
comment
Ваше решение не работает. Я уже тестировал это. Также он не оптимизирован для использования ViewHolder и элемента, отображаемого в данный момент. Я предлагаю вам посмотреть лекцию о мире listView. Удаление сенсорных событий также не имеет смысла, так как вы уже установили его снова сразу после него. Конечно, то, что вы написали, в общем работает, но не отвечает на вопрос. - person android developer; 02.07.2014
comment
Представления @androiddeveloper переработаны, переработанное представление, которое ранее было под номером 10, теперь отображается под номером 15. Если вы не перенастроите прослушиватели событий, он запустит старые прослушиватели событий, которые по-прежнему будут видеть позицию 10, как это было раньше. когда старые обработчики событий создавались как анонимные внутренние классы. Это не имеет ничего общего с шаблоном View Holder. Здесь вы также можете использовать тот же шаблон. Конкретный ответ можно дать только для исходного кода. Надеюсь это поможет. - person S.D.; 02.07.2014
comment
Это не имеет значения. Ваш код не отвечает на вопрос. Я использовал оба метода. Кроме того, я уже знаю о переработанных представлениях, но если вы вызываете notifyDataSetChanged, используются точно такие же представления, и я уже делаю проверки для проверки данных, но опять же, это не проблема, и не тот вопрос, который я здесь задаю. Теперь я обновил код, чтобы показать, что использование вашего кода не поможет (размещение onClickListener после блока if). - person android developer; 02.07.2014
comment
Пожалуйста, попробуйте свой код, прежде чем утверждать, что это является причиной проблемы. Я уже знаю, как обращаться с listViews и адаптерами. Это только пример, чтобы показать проблему. Конечно, я не буду приводить вам сотни ненужных строк кода... - person android developer; 02.07.2014