如何判断ListView的某个条目是否滑出了屏幕
Posted lxn_李小牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何判断ListView的某个条目是否滑出了屏幕相关的知识,希望对你有一定的参考价值。
public class MainActivity extends AppCompatActivity
private List<String> data = new ArrayList<>();
private ListView listView;
private int mPosition;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.listview);
initData();
ListAdapter adapter = new ListAdapter(this, data);
listView.setAdapter(adapter);
public void initData()
for (char i = 'A'; i < 'Z'; i++)
data.add((Character) (i) + "");
public void update(View view)
updateItemWithPosition(3);
//更新指定位置的条目
public void updateItemWithPosition(int position)
mPosition = position;
class ListAdapter extends BaseAdapter
private Context mContext;
private List<String> mData;
ListAdapter(Context context, List<String> data)
this.mContext = context;
this.mData = data;
@Override
public int getCount()
return mData.size();
@Override
public String getItem(int position)
return mData.get(position);
@Override
public long getItemId(int position)
return position;
@Override
public View getView(int position, View convertView, ViewGroup parent)
ViewHolder holder;
if (convertView == null)
holder = new ViewHolder();
convertView = View.inflate(mContext, R.layout.list_item_layout, parent);
holder.textView = convertView.findViewById(R.id.textview);
convertView.setTag(holder);
else
holder = (ViewHolder) convertView.getTag();
holder.textView.setText(getItem(position));
return convertView;
class ViewHolder
private TextView textView;
运行上面的代码,我们会得到下面的崩溃信息
android.view.InflateException: Binary XML file line #11: addView(View, LayoutParams) is not supported in AdapterView
at android.view.LayoutInflater.inflate(LayoutInflater.java:539)
at android.view.LayoutInflater.inflate(LayoutInflater.java:423)
at android.view.LayoutInflater.inflate(LayoutInflater.java:374)
原因就是我们填充布局时出现了错误
convertView = View.inflate(mContext, R.layout.list_item_layout, parent);
那么正确的方式怎么写呢,
convertView = mInflater.inflate(R.layout.list_item_layout,parent,false);
假如我们的parent传入了null,那么你在条目跟布局上设置的layoutParam属性就不管用了,我们可以从源码的角度来看看
if (root != null)
if (DEBUG)
System.out.println("Creating params from root: " +
root);
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot)
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
这里先判断root是否为null,不为null的话,先构造layoutParam属性,然后判断attachToRoot的值,attachToRoot为true代表将这个布局添加到root布局,并返回root布局,为false代表不会添加到root布局,返回resource指定的布局。
完整代码
public class MainActivity extends AppCompatActivity
private List<String> data = new ArrayList<>();
private ListView listView;
private int mPosition;
private int mLastVisiablePosition;
private int mFirstVisiablePosition;
@Override
protected void onCreate(Bundle savedInstanceState)
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
listView = findViewById(R.id.listview);
initData();
ListAdapter adapter = new ListAdapter(this, data);
listView.setAdapter(adapter);
listView.setOnScrollListener(new AbsListView.OnScrollListener()
@Override
public void onScrollStateChanged(AbsListView view, int scrollState)
@Override
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount)
mFirstVisiablePosition = firstVisibleItem;
listView.post(new Runnable()
@Override
public void run()
//防止listview没初始化好时getLastVisiblePosition返回1
mLastVisiablePosition = listView.getLastVisiblePosition();
);
System.out.println("listview: " + firstVisibleItem+"--"+mLastVisiablePosition+"--"+ mPosition);
//判断点击的Item是否在屏幕内,判断的条件是点击Item的position大于屏幕内
//第一个可见的position并且小于最后一个可见的position
if (firstVisibleItem <= mPosition && mPosition <= mLastVisiablePosition)
System.out.println("listview: 屏幕内" );
);
listView.setOnItemClickListener(new AdapterView.OnItemClickListener()
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id)
Toast.makeText(getBaseContext(),""+position,Toast.LENGTH_SHORT).show();
);
public void initData()
for (char i = 'A'; i < 'Z'; i++)
data.add((Character) (i) + "");
//更新指定位置的条目
public void updateItemWithPosition(int position)
mPosition = position;
//因为getChildAt中的position指的是可见区域的第几个元素,这里要减去屏幕上第一个可见元素的位置
// listView.getChildCount()得到的是可见区域内元素个数
View itemView = listView.getChildAt(position - mFirstVisiablePosition);
ListAdapter.ViewHolder holder = (ListAdapter.ViewHolder) itemView.getTag();
holder.button.setText("已更新");
holder.button.setEnabled(false);
//点击button的时候,与当前的position进行关联
holder.button.setTag(position);
class ListAdapter extends BaseAdapter
private List<String> mData;
private LayoutInflater mInflater;
ListAdapter(Context context, List<String> data)
this.mData = data;
mInflater = LayoutInflater.from(context);
@Override
public int getCount()
return mData.size();
@Override
public String getItem(int position)
return mData.get(position);
@Override
public long getItemId(int position)
return position;
@Override
public View getView(final int position, View convertView, ViewGroup parent)
ViewHolder holder;
if (convertView == null)
holder = new ViewHolder();
convertView = mInflater.inflate(R.layout.list_item_layout, parent, false);
holder.textView = convertView.findViewById(R.id.textview);
holder.button = convertView.findViewById(R.id.button);
convertView.setTag(holder);
else
holder = (ViewHolder) convertView.getTag();
holder.textView.setText(getItem(position));
//防止复用导致的显示错误
if (holder.button.getTag() != null && holder.button.getTag().equals(position))
holder.button.setEnabled(false);
else
holder.button.setEnabled(true);
holder.button.setText("更新");
holder.button.setOnClickListener(new View.OnClickListener()
@Override
public void onClick(View v)
updateItemWithPosition(position);
);
return convertView;
class ViewHolder
private TextView textView;
private Button button;
我们可以看到ListView的Item布局中有个Button,如果我们不进行处理的话会导致ListView的item点击事件失效,解决办法是
在ListView的Item的跟布局加上,意思是
android:descendantFocusability="blocksDescendants"
意思是ViewGroup会覆盖子类而直接获得焦点
View的post问题
上面的代码中我们为了防止一开始获取ListView的最后一个可见条目位置不正确,我们调用了它的post方法,接下来我们看看post方法是如何做到的,post方法是定义在View中的
public boolean post(Runnable action)
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null)
return attachInfo.mHandler.post(action);
// Postpone the runnable until we know on which thread it needs to run.
// Assume that the runnable will be successfully placed after attach.
getRunQueue().post(action);
return true;
首先判断attachInfo是否为null,不为null,然后调用Handler的post方法,这个没什么说的,我们主要看看attachInfo是何时初始化的
public ViewRootImpl(Context context, Display display)
mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this);
通过查看源码,我们发现是在ViewRootImpl初始化的时候被创建的,接下来我们看看ViewRootImpl是何时初始化的,同样在源码中可以找到答案,在ActivityThread的handleResumeActivity方法中,这个方法主要是回调Activity的onResume方法,在这个方法中有这样的代码
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
if (a.mVisibleFromClient)
a.mWindowAdded = true;
wm.addView(decor, l);
最后调用了wm.addView方法,ViewManager是一个接口,主要用来为Activity添加和删除View,定义如下
public interface ViewManager
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
它的实现类是WindowManager,而WindowManager的实现类是WindowManagerImpl,上面其实是调用了WindowManagerImpl的addView方法
@Override
public void addView(View view, ViewGroup.LayoutParams params)
mGlobal.addView(view, params, mDisplay, mParentWindow);
mGlobal是WindowManagerGlobal的对象,最终我们发现了
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try
root.setView(view, wparams, panelParentView);
综上所述,attachInfo是在handleResume方法中被创建的,也就是说在Activity的onResume执行之前,attachInfo还没初始化,然后我们回到View的post方法,接着调用了
getRunQueue().post(action);
private HandlerActionQueue getRunQueue()
if (mRunQueue == null)
mRunQueue = new HandlerActionQueue();
return mRunQueue;
这里我们获取到了一个HandlerActionQueue方法,这是一个队列,这个队列其实保存了当前View的需要执行的runnable任务,主要用处是当当前View的handler对象没有关联上的时候,先把任务保存起来,然后延迟执行,那么这个执行的时机是什么呢,接下来我们可以看到答案,我们先看HandlerActionQueue的具体实现
public class HandlerActionQueue
private HandlerAction[] mActions;//任务数组
private int mCount;//任务总个数
public void post(Runnable action)
postDelayed(action, 0);
public void postDelayed(Runnable action, long delayMillis)
final HandlerAction handlerAction = new HandlerAction(action, delayMillis);
synchronized (this)
if (mActions == null)
mActions = new HandlerAction[4];
mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction);//将提交的任务保存到任务数组里
mCount++;
public void removeCallbacks(Runnable action)
synchronized (this)
final int count = mCount;
int j = 0;
final HandlerAction[] actions = mActions;
for (int i = 0; i < count; i++)
if (actions[i].matches(action))
// Remove this action by overwriting it within
// this loop or nulling it out later.
continue;
if (j != i)
// At least one previous entry was removed, so
// this one needs to move to the "new" list.
actions[j] = actions[i];
j++;
// The "new" list only has j entries.
mCount = j;
// Null out any remaining entries.
for (; j < count; j++)
actions[j] = null;
//执行任务数组里的任务
public void executeActions(Handler handler)
synchronized (this)
final HandlerAction[] actions = mActions;
for (int i = 0, count = mCount; i < count; i++)
final HandlerAction handlerAction = actions[i];
handler.postDelayed(handlerAction.action, handlerAction.delay);
mActions = null;
mCount = 0;
public int size()
return mCount;
public Runnable getRunnable(int index)
if (index >= mCount)
throw new IndexOutOfBoundsException();
return mActions[index].action;
public long getDelay(int index)
if (index >= mCount)
throw new IndexOutOfBoundsException();
return mActions[index].delay;
//人物对象
private static class HandlerAction
final Runnable action;
final long delay;
public HandlerAction(Runnable action, long delay)
this.action = action;
this.delay = delay;
public boolean matches(Runnable otherAction)
return otherAction == null && action == null
|| action != null && action.equals(otherAction);
我们通过post提交的任务保存在了mActions数组里,接下来我们重点看看这些任务是在何时执行的,也就是说executeActions方法是在哪里调用的,在ViewRootImpl的performTraversals里我们找到了
private void performTraversals()
// cache mView since it is used so much below...
final View host = mView;
host.dispatchAttachedToWindow(mAttachInfo, 0);
这个mView就是当前的View,我们看看它具体是在哪里赋值的
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView)
synchronized (this)
if (mView == null)
mView = view;
在setView方法里进行了初始化,setView调用的时机我们从上面的代码可以看出,是在WindowManagerGlobal的addView方法中调用的。然后我们看看View的dispatchAttachedToWindow方法
void dispatchAttachedToWindow(AttachInfo info, int visibility)
mAttachInfo = info;
...。
// Transfer all pending runnables.
if (mRunQueue != null) //执行通过post方法保存到任务队列中的任务
mRunQueue.executeActions(info.mHandler);
mRunQueue = null;
综上所属,我们通过post方法提交单任务队列中的任务,是在performTraversals方法中执行任务的,performTraversals中开始了View的绘制,所以说,View的post方法提交的任务是在View下次开始绘制的时候执行的。
以上是关于如何判断ListView的某个条目是否滑出了屏幕的主要内容,如果未能解决你的问题,请参考以下文章
flutter listview item滚出屏幕不重置状态
如何知道 ListView 中修改了哪个 Xamarin 表单条目?