如何判断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 表单条目?

在android中listview中怎么获取条目中某个控件的宽度

Flutter ListView 在构建时滚动到底部

ListView 在滚动时不会更改元素

如何根据ListView中的数据打开某个TabPage?