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下次开始绘制的时候执行的。