概述
在 Android 应用程序中,ListView 是一种常用的控件,用于显示可滚动列表数据。然而,当在鼠标操作模式下使用 ListView 时,可能会遇到一个问题:点击列表项时,列表会回滚到指定位置,这可能会导致用户体验不佳。
分析
通过测试发现, 回滚的位置与列表的选中项位置有关系.
在启动activity后, 不执行其他操作时, 通过滚轮直接滚动到列表最下方, 再点击按键, 调用了adapter.notifyDataSetChanged
列表回滚;
dumpsys
dumpsys activity com.android.tester/.cases.ListViewBackground
出现回滚时
View Hierarchy:DecorView@4ef06ab[ListViewBackground]com.android.internal.widget.ActionBarOverlayLayout{1102108 V.E...... ........ 0,0-1920,1080 #1020230 android:id/decor_content_parent}android.widget.FrameLayout{21eb0a1 V.E...... ........ 0,96-1920,1080 #1020002 android:id/content}android.widget.LinearLayout{e6872c6 V.E...... ........ 0,0-1920,984}android.widget.ListView{956287 VFED.VC.. .F...... 0,0-1920,984 #7f030002 app:id/lvRight}android.widget.LinearLayout{f0a45b4 V.E...... ..S..... 0,0-1920,173}android.widget.TextView{5d4b5dd V.ED..... ..S..... 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{e8f552 VFED..C.. ..S..... 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{37d149e V.E...... ........ 0,175-1920,348}android.widget.TextView{8be1b7f V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{88c974c VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{d5ba023 V.E...... ........ 0,350-1920,523}android.widget.TextView{8e00920 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{8ebd2d9 VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{abdc395 V.E...... ........ 0,525-1920,698}android.widget.TextView{ea1dcaa V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{1a2f09b VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{a91dc38 V.E...... ........ 0,700-1920,873}android.widget.TextView{c840411 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{3991976 VFED..C.. ....H... 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{535fb77 V.E...... ........ 0,875-1920,1048}android.widget.TextView{fa483e4 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{f93d04d VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}com.android.internal.widget.ActionBarContainer{3f55702 V.ED..... ........ 0,0-1920,96 #102018e android:id/action_bar_container}android.widget.Toolbar{d84d813 V.E...... ........ 0,0-1920,96 #102018d android:id/action_bar}android.widget.TextView{d17fa50 V.ED..... ........ 36,27-122,68}com.android.internal.widget.ActionBarContextView{b5a2449 G.E...... ......I. 0,0-0,0 #1020192 android:id/action_context_bar}
在启动activity后, 点击列表项(不是BTN所在区域), 通过滚轮直接滚动到列表最下方, 再点击按键, 调用了adapter.notifyDataSetChanged
列表不回滚;
不会滚
View Hierarchy:DecorView@4ef06ab[ListViewBackground]com.android.internal.widget.ActionBarOverlayLayout{1102108 V.E...... ........ 0,0-1920,1080 #1020230 android:id/decor_content_parent}android.widget.FrameLayout{21eb0a1 V.E...... ........ 0,96-1920,1080 #1020002 android:id/content}android.widget.LinearLayout{e6872c6 V.E...... ........ 0,0-1920,984}android.widget.ListView{956287 VFED.VC.. .F..H... 0,0-1920,984 #7f030002 app:id/lvRight}android.widget.LinearLayout{f0a45b4 V.E...... ........ 0,0-1920,173}android.widget.TextView{5d4b5dd V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{e8f552 VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{d5ba023 V.E...... ........ 0,175-1920,348}android.widget.TextView{8e00920 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{8ebd2d9 VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{37d149e V.E...... ........ 0,350-1920,523}android.widget.TextView{8be1b7f V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{88c974c VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{abdc395 V.E...... ........ 0,525-1920,698}android.widget.TextView{ea1dcaa V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{1a2f09b VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{a91dc38 V.E...... ........ 0,700-1920,873}android.widget.TextView{c840411 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{3991976 VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}android.widget.LinearLayout{535fb77 V.E...... ........ 0,875-1920,1048}android.widget.TextView{fa483e4 V.ED..... ........ 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{f93d04d VFED..C.. ........ 1735,0-1920,173 #7f030000 app:id/btn}com.android.internal.widget.ActionBarContainer{3f55702 V.ED..... ........ 0,0-1920,96 #102018e android:id/action_bar_container}android.widget.Toolbar{d84d813 V.E...... ........ 0,0-1920,96 #102018d android:id/action_bar}android.widget.TextView{d17fa50 V.ED..... ........ 36,27-122,68}com.android.internal.widget.ActionBarContextView{b5a2449 G.E...... ......I. 0,0-0,0 #1020192 android:id/action_context_bar}
View状态输出的源码:
frameworks/base/core/java/android/view/View.java
public String toString() {StringBuilder out = new StringBuilder(128);out.append(getClass().getName());out.append('{');out.append(Integer.toHexString(System.identityHashCode(this)));out.append(' ');switch (mViewFlags&VISIBILITY_MASK) {case VISIBLE: out.append('V'); break;case INVISIBLE: out.append('I'); break;case GONE: out.append('G'); break;default: out.append('.'); break;}out.append((mViewFlags&FOCUSABLE_MASK) == FOCUSABLE ? 'F' : '.');out.append((mViewFlags&ENABLED_MASK) == ENABLED ? 'E' : '.');out.append((mViewFlags&DRAW_MASK) == WILL_NOT_DRAW ? '.' : 'D');out.append((mViewFlags&SCROLLBARS_HORIZONTAL) != 0 ? 'H' : '.');out.append((mViewFlags&SCROLLBARS_VERTICAL) != 0 ? 'V' : '.');out.append((mViewFlags&CLICKABLE) != 0 ? 'C' : '.');out.append((mViewFlags&LONG_CLICKABLE) != 0 ? 'L' : '.');out.append((mViewFlags&CONTEXT_CLICKABLE) != 0 ? 'X' : '.');out.append(' ');out.append((mPrivateFlags&PFLAG_IS_ROOT_NAMESPACE) != 0 ? 'R' : '.');out.append((mPrivateFlags&PFLAG_FOCUSED) != 0 ? 'F' : '.');out.append((mPrivateFlags&PFLAG_SELECTED) != 0 ? 'S' : '.');if ((mPrivateFlags&PFLAG_PREPRESSED) != 0) {out.append('p');} else {out.append((mPrivateFlags&PFLAG_PRESSED) != 0 ? 'P' : '.');}out.append((mPrivateFlags&PFLAG_HOVERED) != 0 ? 'H' : '.');out.append((mPrivateFlags&PFLAG_ACTIVATED) != 0 ? 'A' : '.');out.append((mPrivateFlags&PFLAG_INVALIDATED) != 0 ? 'I' : '.');out.append((mPrivateFlags&PFLAG_DIRTY_MASK) != 0 ? 'D' : '.');out.append(' ');out.append(mLeft);out.append(',');out.append(mTop);out.append('-');out.append(mRight);out.append(',');out.append(mBottom);}
从上面的代码可以看出, S代表了View的Selected状态. 通过操作按键的上 下 键改变列表的选中项, 可以看出列表的S项跟随选中项. 而回滚的位置就是选中的位置, 回滚的效果与 setSelection(int pos)相同;
代码中打印ListView.getSelectedItemPosition的返回值, 默认是 0(回滚), 当点击列表项后变为 -1(不回滚)
默认选中从何而来?
打印出setSelected的堆栈信息:
java.lang.Exception: setSelectedat com.android.tester.widgets.XTV.setSelected(XTV.java:23)at android.view.ViewGroup.dispatchSetSelected(ViewGroup.java:4426)at android.view.View.setSelected(View.java:22276)at android.widget.ListView.setupChild(ListView.java:2102)at android.widget.ListView.makeAndAddView(ListView.java:2055)at android.widget.ListView.fillDown(ListView.java:786)at android.widget.ListView.fillFromTop(ListView.java:847)at android.widget.ListView.layoutChildren(ListView.java:1826)at android.widget.AbsListView.onLayout(AbsListView.java:2165)at android.view.View.layout(View.java:20672)at android.view.ViewGroup.layout(ViewGroup.java:6194)at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1812)at android.widget.LinearLayout.layoutHorizontal(LinearLayout.java:1801)at android.widget.LinearLayout.onLayout(LinearLayout.java:1567)at android.view.View.layout(View.java:20672)at android.view.ViewGroup.layout(ViewGroup.java:6194)at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)at android.widget.FrameLayout.onLayout(FrameLayout.java:261)at android.view.View.layout(View.java:20672)at android.view.ViewGroup.layout(ViewGroup.java:6194)at com.android.internal.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:508)at android.view.View.layout(View.java:20672)at android.view.ViewGroup.layout(ViewGroup.java:6194)at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)at android.widget.FrameLayout.onLayout(FrameLayout.java:261)at com.android.internal.policy.DecorView.onLayout(DecorView.java:753)at android.view.View.layout(View.java:20672)at android.view.ViewGroup.layout(ViewGroup.java:6194)at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2796)at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2323)at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1462)at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:7187)at android.view.Choreographer$CallbackRecord.run(Choreographer.java:949)at android.view.Choreographer.doCallbacks(Choreographer.java:761)at android.view.Choreographer.doFrame(Choreographer.java:696)at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:935)at android.os.Handler.handleCallback(Handler.java:873)at android.os.Handler.dispatchMessage(Handler.java:99)at android.os.Looper.loop(Looper.java:193)at android.app.ActivityThread.main(ActivityThread.java:6669)at java.lang.reflect.Method.invoke(Native Method)at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:951)
源码中:
frameworks/base/core/java/android/widget/ListView.java
/*** Fills the list from pos down to the end of the list view.** @param pos The first position to put in the list** @param nextTop The location where the top of the item associated with pos* should be drawn** @return The view that is currently selected, if it happens to be in the* range that we draw.*/private View fillDown(int pos, int nextTop) {View selectedView = null;int end = (mBottom - mTop);if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {end -= mListPadding.bottom;}while (nextTop < end && pos < mItemCount) {// is this the selected item?boolean selected = pos == mSelectedPosition;View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);nextTop = child.getBottom() + mDividerHeight;if (selected) {selectedView = child;}pos++;}setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);return selectedView;}
重点看下: mSelectedPosition
/*** Find a position that can be selected (i.e., is not a separator).** @param position The starting position to look at.* @param lookDown Whether to look down for other positions.* @return The next selectable position starting at position and then searching either up or* down. Returns {@link #INVALID_POSITION} if nothing can be found.*/@Overrideint lookForSelectablePosition(int position, boolean lookDown) {final ListAdapter adapter = mAdapter;if (adapter == null || isInTouchMode()) {return INVALID_POSITION;}final int count = adapter.getCount();if (!mAreAllItemsSelectable) {if (lookDown) {position = Math.max(0, position);while (position < count && !adapter.isEnabled(position)) {position++;}} else {position = Math.min(position, count - 1);while (position >= 0 && !adapter.isEnabled(position)) {position--;}}}if (position < 0 || position >= count) {return INVALID_POSITION;}return position;}
鼠标的操作模式中, isInTouchMode 返回的是false, mSelectedPosition = 0;
当点击了列表项后, isInTouchMode 则返回了true, mSelectedPosition = INVALID_POSITION (-1)
解决回滚
一个取巧的办法: 在鼠标输入的模式下, 会导致View判断isInTouchMode返回false, 那么, 重写该判断方法即可.
package com.android.tester.widgets;import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;public class XLV extends ListView {public XLV(Context context) {super(context);}public XLV(Context context, AttributeSet attrs) {super(context, attrs);}public XLV(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overridepublic boolean isInTouchMode() {return true;}
}
设置列表项的背景: android:listSelector 和 android:background 的效果并不相同!
<?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="@color/red"/></shape></item><item android:state_focused="true"><shape><solid android:color="@color/yellow"/></shape></item><item android:state_checked="true"><shape><solid android:color="@color/cyan"/></shape></item><item android:state_selected="true"><shape><solid android:color="@color/blue"/></shape></item><item android:state_activated="true"><shape><solid android:color="@color/purple"/></shape></item><item><shape><solid android:color="@color/trans"/></shape></item>
</selector>
设置列表项的方法有两种, 实际效果却不相同:
android:listSelector 有自己的想法, 测试的代码中, 似乎它对 state_focused (黄色)相当执着, 即使列表项在View的状态已经是Selected, 它依然不变初衷.
从dumpsys可以看到:
android.widget.LinearLayout{9af619f V.E...... ..S..... 0,0-1920,173}com.android.tester.widgets.XTV{f6532ec V.ED..... ..S..... 0,0-1735,173 #7f030004 app:id/tv}android.widget.Button{222c6b5 VFED..C.. ..S..... 1735,0-1920,173 #7f030000 app:id/btn}
显示效果:
为列表项单独设置android:background会更灵活一点
//会残留黄色背景android:listSelector="@null"//背景干净.android:listSelector="@color/trans"
修改列表项背景:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:descendantFocusability="blocksDescendants"android:background="@drawable/selector_item_bg"android:layout_width="match_parent"android:layout_height="match_parent"><com.android.tester.widgets.XTV android:id="@+id/tv"android:layout_width="0dp"android:layout_weight="1"android:textColor="#FF987654"android:gravity="center_vertical"android:layout_height="match_parent"/><Button android:id="@+id/btn"android:text="BTN"android:padding="48dp"android:layout_width="wrap_content"android:layout_height="wrap_content"/>
</LinearLayout>
参考代码
layout
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:layout_width="match_parent"android:layout_height="match_parent"><!--<TextView android:id="@+id/tvLeft"android:background="@drawable/selector_border_bg"android:layout_width="0dp"android:layout_weight="0.3"android:layout_height="wrap_content"/>--><com.android.tester.widgets.XLVandroid:id="@+id/lvRight"android:layout_width="match_parent"android:layout_height="match_parent"android:drawSelectorOnTop="false"android:cacheColorHint="@color/trans"android:listSelector="@null"/>
</LinearLayout>
layout item
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:orientation="horizontal"android:descendantFocusability="blocksDescendants"android:layout_width="match_parent"android:layout_height="match_parent"><com.android.tester.widgets.XTV android:id="@+id/tv"android:layout_width="0dp"android:layout_weight="1"android:textColor="#FF987654"android:gravity="center_vertical"android:layout_height="match_parent"/><Button android:id="@+id/btn"android:text="BTN"android:padding="48dp"android:layout_width="wrap_content"android:layout_height="wrap_content"/>
</LinearLayout>
Activity
package com.android.tester.cases;import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.Button;
import android.widget.ListView;
import android.widget.TextView;import com.android.tester.R;import java.util.ArrayList;public class ListViewBackground extends Activity {final String TAG = "ListViewBackground";//TextView tvLeft;ListView lvRight;LVAdapter adapter;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.listview_background);/*tvLeft = (TextView) findViewById(R.id.tvLeft);tvLeft.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {lvRight.setSelection(0);}});*/lvRight = (ListView)findViewById(R.id.lvRight);lvRight.setChoiceMode(AbsListView.CHOICE_MODE_NONE);adapter = new LVAdapter();for(int i = 0; i < 50; i ++){adapter.addData("ITEM " + i);}lvRight.setAdapter(adapter);lvRight.setOnItemClickListener(new AdapterView.OnItemClickListener() {@Overridepublic void onItemClick(AdapterView<?> parent, View view, int position, long id) {/*tvLeft.setText(adapter.content.get(position));tvLeft.setSelected(position%3 == 0);tvLeft.setPressed(position%2 == 0);*///lvRight.setSelection(position);}});}class LVAdapter extends BaseAdapter{ArrayList<Data> content = new ArrayList<>();void addData(String str){int pos = content.size();content.add(new Data());content.get(pos).content = str;content.get(pos).pos = pos;}@Overridepublic int getCount() {return content.size();}@Overridepublic Object getItem(int position) {return content.get(position);}@Overridepublic long getItemId(int position) {return position;}@Overridepublic View getView(int position, View convertView, ViewGroup parent) {TAG tag = null;if(convertView != null && convertView.getTag() != null){tag = (TAG)convertView.getTag();}else{tag = new TAG();convertView = getLayoutInflater().inflate(R.layout.layout_item_lv_background, null, false);tag.tv = convertView.findViewById(R.id.tv);tag.btn = convertView.findViewById(R.id.btn);tag.btn.setOnClickListener(btnClick);convertView.setTag(tag);}tag.tv.setText(content.get(position).content);tag.btn.setTag(position);//convertView.setSelected(content.get(position).selected);return convertView;}View.OnClickListener btnClick = new View.OnClickListener() {@Overridepublic void onClick(View v) {int pos = (int)v.getTag();//tvLeft.setText("Click Button " + pos);Log.d(TAG, "onClick isItemChecked=" + lvRight.isItemChecked(pos));Log.d(TAG, "onClick selected item pos=" + lvRight.getSelectedItemPosition());//lvRight.setSelection(pos);//lvRight.setItemChecked(pos, true);//content.get(pos).selected = true;notifyDataSetChanged();}};class TAG{TextView tv;Button btn;}class Data{int pos;boolean selected;String content;}}
}
XLV.java
package com.android.tester.widgets;import android.content.Context;
import android.util.AttributeSet;
import android.widget.ListView;public class XLV extends ListView {public XLV(Context context) {super(context);}public XLV(Context context, AttributeSet attrs) {super(context, attrs);}public XLV(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}@Overridepublic boolean isInTouchMode() {return true;}
}
参考
ListView的多选单选模式
listview的属性listselector使用解析