如何处理listView行的点击,而可能会调用notifyDataSetChanged?

时间:2014-07-02 08:05:14

标签: android android-listview onclicklistener android-selector

背景

我有一个用于listView的复杂适配器。

每一行都有一些内部视图需要点击(并且点击次数),并且它们还有选择器(以显示触摸的效果)。

在某些情况下,需要非常频繁地调用notifyDataSetChanged()(例如每秒一次/两次),以显示listView项目的一些更改。

例如,考虑查看下载文件列表,向用户显示正在下载的每个文件的进度。

问题

每次调用notifyDataSetChanged时,触摸事件在触摸的视图上都会丢失,因此用户可能会错过点击它,特别是错过了长按。

不仅如此,选择器也会失去其状态,所以如果你触摸它并看到效果,当调用notifyDataSetChanged时,视图会丢失它的状态,你会看到它好像没有被触及。 / p>

即使对于没有更新任何内容的视图(这意味着我只是为它们返回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添加/删除项目的情况,listView需要调用notifyDataSetChangedbeing。此外,它还使更新的视图失去了触摸状态。

编辑:即使是部分解决方案也不起作用。也许它导致整个listView的布局,导致其他视图失去其状态。

问题

在调用notifyDataSetChanged()之后,如何让视图与触摸事件保持“同步”?

2 个答案:

答案 0 :(得分:2)

您没有更新&#34; Recycled&#34;的点击监听器。视图。

tv.setOnClickListener()放在if (tv == null)支票之外。

此外,您希望&#34;保持同步的属性&#34;应该在支持ListView的模型中。永远不要相信Views能够保存重要数据,它们只应反映模型中的数据。

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 :(得分:0)

如上所述,您可以使用View.setOnTouchListener()并抓住ACTION_DOWNACTION_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。