深度剖析 Android GridView 使用原理:从源码到实战
一、引言
在 Android 开发的广袤领域中,用户界面(UI)的设计与实现始终占据着关键地位。而 GridView
作为 Android 系统中一个极为重要且实用的 UI 组件,宛如一颗璀璨的明珠,为开发者提供了强大的功能和丰富的交互可能性。
GridView
本质上是一种可滚动的二维网格视图,它能够以网格的形式展示一系列的数据项。这种展示方式在众多应用场景中都发挥着至关重要的作用,例如图片展示应用,我们可以使用 GridView
以网格形式整齐地排列多张图片,让用户能够直观地浏览和选择;在应用程序的主界面,也可以使用 GridView
展示各种功能图标,方便用户快速访问不同的功能模块。
本文将深入探究 GridView
的使用原理,从源码层面进行细致入微的分析。通过对 GridView
源码的深入解读,我们不仅能够清晰地了解其内部的工作机制,还能掌握如何灵活运用 GridView
来实现各种复杂的界面需求。同时,我们也会探讨在实际开发中可能遇到的问题以及相应的解决方案,为开发者在使用 GridView
时提供全面而深入的指导。
二、GridView 概述
2.1 GridView 的定义与作用
GridView
是 Android 提供的一个视图类,它继承自 AbsListView
。AbsListView
是一个抽象类,为列表视图提供了基本的功能和框架,而 GridView
在此基础上进行了扩展,实现了以二维网格形式展示数据的功能。
GridView
的主要作用是将一组数据以网格的形式展示给用户,用户可以通过滚动操作查看所有的数据项。每个数据项在 GridView
中都以一个独立的视图呈现,这些视图可以是简单的文本视图,也可以是复杂的自定义视图。
2.2 GridView 的应用场景
GridView
在实际开发中有广泛的应用场景,以下是一些常见的例子:
- 图片展示 :如相册应用,使用
GridView
可以将多张图片以网格形式排列,用户可以快速浏览和选择图片。 - 图标展示 :在应用的主界面,使用
GridView
展示各种功能图标,方便用户快速访问不同的功能模块。 - 商品展示 :电商应用中,使用
GridView
展示商品列表,用户可以直观地查看商品的缩略图和基本信息。
2.3 GridView 的基本使用示例
以下是一个简单的 GridView
使用示例,展示了如何在布局文件中添加 GridView
并在代码中设置数据适配器:
布局文件 activity_main.xml
xml
<!-- 引入 Android 命名空间 -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<!-- 定义一个 GridView 组件 -->
<GridView
android:id="@+id/gridView"
android:layout_width="match_parent"
android:layout_height="match_parent"
<!-- 设置列数为 3 -->
android:numColumns="3"/>
</LinearLayout>
主活动 MainActivity.java
java
import android.os.Bundle;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.BaseAdapter;
import android.widget.GridView;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private GridView gridView;
// 定义一个字符串数组作为数据源
private String[] data = {"Item 1", "Item 2", "Item 3", "Item 4", "Item 5", "Item 6"};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 设置布局文件
setContentView(R.layout.activity_main);
// 通过 ID 找到 GridView 组件
gridView = findViewById(R.id.gridView);
// 创建一个自定义的适配器
MyAdapter adapter = new MyAdapter();
// 为 GridView 设置适配器
gridView.setAdapter(adapter);
// 为 GridView 设置点击事件监听器
gridView.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
// 处理点击事件
}
});
}
// 自定义适配器类,继承自 BaseAdapter
private class MyAdapter extends BaseAdapter {
@Override
public int getCount() {
// 返回数据源的数量
return data.length;
}
@Override
public Object getItem(int position) {
// 返回指定位置的数据项
return data[position];
}
@Override
public long getItemId(int position) {
// 返回指定位置的数据项的 ID
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
TextView textView;
if (convertView == null) {
// 如果 convertView 为空,创建一个新的 TextView
textView = new TextView(MainActivity.this);
// 设置 TextView 的内边距
textView.setPadding(8, 8, 8, 8);
} else {
// 如果 convertView 不为空,复用该视图
textView = (TextView) convertView;
}
// 设置 TextView 的文本内容
textView.setText(data[position]);
return textView;
}
}
}
在上述示例中,我们首先在布局文件 activity_main.xml
中定义了一个 GridView
组件,并设置了列数为 3。然后在 MainActivity.java
中,我们通过 findViewById
方法找到 GridView
组件,并创建了一个自定义的适配器 MyAdapter
。适配器负责管理数据源和视图的绑定,我们重写了 getCount
、getItem
、getItemId
和 getView
方法。最后,我们为 GridView
设置了适配器和点击事件监听器。
三、GridView 的源码结构
3.1 GridView 的继承关系
GridView
继承自 AbsListView
,AbsListView
又继承自 AdapterView<ListAdapter>
。AdapterView
是一个抽象类,它定义了一个基于适配器的视图的基本框架,而 AbsListView
则为列表视图提供了一些基本的功能和实现。
以下是 GridView
的继承关系图:
plaintext
Object
└── View
└── ViewGroup
└── AdapterView<ListAdapter>
└── AbsListView
└── GridView
3.2 GridView 的主要成员变量
GridView
中有许多重要的成员变量,这些变量在 GridView
的工作过程中起着关键的作用。以下是一些主要的成员变量及其作用:
mNumColumns
:表示GridView
的列数。mColumnWidth
:表示每列的宽度。mHorizontalSpacing
:表示列与列之间的水平间距。mVerticalSpacing
:表示行与行之间的垂直间距。mAdapter
:表示GridView
的数据适配器,用于管理数据源和视图的绑定。mSelector
:表示GridView
的选择器,用于处理选中项的背景效果。
3.3 GridView 的构造函数
GridView
有多个构造函数,以下是其中一个常见的构造函数:
java
// 构造函数,接收上下文和属性集合作为参数
public GridView(Context context, AttributeSet attrs) {
// 调用父类的构造函数
this(context, attrs, com.android.internal.R.attr.gridViewStyle);
}
// 构造函数,接收上下文、属性集合和默认样式作为参数
public GridView(Context context, AttributeSet attrs, int defStyleAttr) {
// 调用父类的构造函数
super(context, attrs, defStyleAttr);
// 初始化 GridView 的属性
initGridView(context, attrs, defStyleAttr, 0);
}
// 构造函数,接收上下文、属性集合、默认样式和资源 ID 作为参数
public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// 调用父类的构造函数
super(context, attrs, defStyleAttr, defStyleRes);
// 初始化 GridView 的属性
initGridView(context, attrs, defStyleAttr, defStyleRes);
}
在上述构造函数中,最终都会调用 initGridView
方法来初始化 GridView
的属性。
3.4 GridView 的初始化方法 initGridView
java
// 初始化 GridView 的方法
private void initGridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
// 获取属性集合
TypedArray a = context.obtainStyledAttributes(attrs,
com.android.internal.R.styleable.GridView, defStyleAttr, defStyleRes);
// 获取列数属性,如果没有设置则使用默认值
mNumColumns = a.getInt(com.android.internal.R.styleable.GridView_numColumns, AUTO_FIT);
// 获取列宽属性,如果没有设置则使用默认值
mColumnWidth = a.getDimensionPixelSize(com.android.internal.R.styleable.GridView_columnWidth, -1);
// 获取水平间距属性,如果没有设置则使用默认值
mHorizontalSpacing = a.getDimensionPixelSize(com.android.internal.R.styleable.GridView_horizontalSpacing, 0);
// 获取垂直间距属性,如果没有设置则使用默认值
mVerticalSpacing = a.getDimensionPixelSize(com.android.internal.R.styleable.GridView_verticalSpacing, 0);
// 回收属性集合
a.recycle();
// 设置滚动条样式
setVerticalScrollBarEnabled(true);
setHorizontalScrollBarEnabled(false);
// 设置选择器
setSelector(com.android.internal.R.drawable.list_selector_background);
}
在 initGridView
方法中,首先通过 TypedArray
获取布局文件中设置的属性,如列数、列宽、水平间距和垂直间距等。然后回收 TypedArray
以释放资源。接着设置滚动条样式和选择器。
四、GridView 的测量机制
4.1 测量的基本概念
在 Android 中,视图的测量是一个重要的过程,它决定了视图的大小。测量过程主要涉及两个方法:onMeasure
和 measure
。measure
方法是 View
类提供的一个公共方法,用于触发测量过程;onMeasure
方法是一个受保护的方法,需要在自定义视图中重写,用于实现具体的测量逻辑。
4.2 GridView 的 onMeasure
方法
java
// 重写 onMeasure 方法,实现 GridView 的测量逻辑
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 调用父类的 onMeasure 方法进行初步测量
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
// 获取宽度测量规格的模式
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
// 获取宽度测量规格的大小
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
// 获取高度测量规格的模式
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 获取高度测量规格的大小
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 获取适配器
ListAdapter adapter = getAdapter();
if (adapter == null) {
// 如果适配器为空,将宽度和高度都设置为 0
setMeasuredDimension(0, 0);
return;
}
if (mNumColumns == AUTO_FIT) {
// 如果列数为自动适应模式
if (widthMode == MeasureSpec.EXACTLY) {
// 如果宽度测量规格为精确模式
if (mColumnWidth > 0) {
// 如果列宽大于 0
// 计算列数
mNumColumns = widthSize / (mColumnWidth + mHorizontalSpacing);
if (mNumColumns > 0) {
// 如果列数大于 0,计算剩余的宽度
int spaceLeft = widthSize - mNumColumns * (mColumnWidth + mHorizontalSpacing) + mHorizontalSpacing;
if (spaceLeft >= mColumnWidth) {
// 如果剩余宽度大于等于列宽,增加一列
mNumColumns++;
}
}
} else {
// 如果列宽小于等于 0,列数设置为 1
mNumColumns = 1;
}
} else {
// 如果宽度测量规格不是精确模式,列数设置为 1
mNumColumns = 1;
}
}
// 计算所需的行数
int numItems = adapter.getCount();
int numRows = (numItems + mNumColumns - 1) / mNumColumns;
// 计算子视图的高度
int childHeight = 0;
if (numItems > 0) {
// 获取第一个子视图
View child = obtainView(0, null);
// 测量子视图
measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
// 获取子视图的测量高度
childHeight = child.getMeasuredHeight();
}
// 计算 GridView 的高度
int gridHeight = numRows * (childHeight + mVerticalSpacing) - mVerticalSpacing;
if (heightMode == MeasureSpec.EXACTLY) {
// 如果高度测量规格为精确模式,使用测量规格的高度
gridHeight = heightSize;
} else if (heightMode == MeasureSpec.AT_MOST) {
// 如果高度测量规格为最多模式,取测量规格的高度和计算高度的最小值
gridHeight = Math.min(gridHeight, heightSize);
}
// 设置测量的宽度和高度
setMeasuredDimension(widthSize, gridHeight);
}
在 onMeasure
方法中,首先调用父类的 onMeasure
方法进行初步测量。然后获取宽度和高度的测量规格,包括模式和大小。接着检查适配器是否为空,如果为空则将宽度和高度都设置为 0。
如果列数为自动适应模式,根据宽度测量规格和列宽计算列数。然后计算所需的行数和子视图的高度,进而计算 GridView
的高度。最后根据高度测量规格的模式,确定最终的高度,并调用 setMeasuredDimension
方法设置测量的宽度和高度。
4.3 测量子视图的方法 measureChildWithMargins
java
// 测量子视图的方法,考虑了边距
protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
int parentHeightMeasureSpec, int heightUsed) {
// 获取子视图的布局参数
final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 计算子视图的宽度测量规格
final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+ widthUsed, lp.width);
// 计算子视图的高度测量规格
final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+ heightUsed, lp.height);
// 调用子视图的 measure 方法进行测量
child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
}
在 measureChildWithMargins
方法中,首先获取子视图的布局参数,然后根据父视图的测量规格和子视图的边距,计算子视图的宽度和高度测量规格。最后调用子视图的 measure
方法进行测量。
4.4 获取子视图测量规格的方法 getChildMeasureSpec
java
// 获取子视图测量规格的方法
public static int getChildMeasureSpec(int spec, int padding, int childDimension) {
// 获取父视图的测量规格模式
int specMode = MeasureSpec.getMode(spec);
// 获取父视图的测量规格大小
int specSize = MeasureSpec.getSize(spec);
// 计算可用的大小
int size = Math.max(0, specSize - padding);
int resultSize = 0;
int resultMode = 0;
switch (specMode) {
case MeasureSpec.EXACTLY:
if (childDimension >= 0) {
// 如果子视图的尺寸大于等于 0,子视图的尺寸为固定值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果子视图的尺寸为 MATCH_PARENT,子视图的尺寸为父视图的可用尺寸
resultSize = size;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子视图的尺寸为 WRAP_CONTENT,子视图的尺寸最大为父视图的可用尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.AT_MOST:
if (childDimension >= 0) {
// 如果子视图的尺寸大于等于 0,子视图的尺寸为固定值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果子视图的尺寸为 MATCH_PARENT,子视图的尺寸最大为父视图的可用尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子视图的尺寸为 WRAP_CONTENT,子视图的尺寸最大为父视图的可用尺寸
resultSize = size;
resultMode = MeasureSpec.AT_MOST;
}
break;
case MeasureSpec.UNSPECIFIED:
if (childDimension >= 0) {
// 如果子视图的尺寸大于等于 0,子视图的尺寸为固定值
resultSize = childDimension;
resultMode = MeasureSpec.EXACTLY;
} else if (childDimension == LayoutParams.MATCH_PARENT) {
// 如果子视图的尺寸为 MATCH_PARENT,子视图的尺寸为 0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
} else if (childDimension == LayoutParams.WRAP_CONTENT) {
// 如果子视图的尺寸为 WRAP_CONTENT,子视图的尺寸为 0
resultSize = 0;
resultMode = MeasureSpec.UNSPECIFIED;
}
break;
}
// 根据计算结果创建子视图的测量规格
return MeasureSpec.makeMeasureSpec(resultSize, resultMode);
}
在 getChildMeasureSpec
方法中,根据父视图的测量规格模式和子视图的布局参数,计算子视图的测量规格。具体来说,根据父视图的测量规格模式(EXACTLY
、AT_MOST
或 UNSPECIFIED
)和子视图的尺寸(固定值、MATCH_PARENT
或 WRAP_CONTENT
),确定子视图的测量规格大小和模式,最后使用 MeasureSpec.makeMeasureSpec
方法创建子视图的测量规格。
五、GridView 的布局机制
5.1 布局的基本概念
布局是指将视图放置在其父视图中的过程。在 Android 中,布局过程主要涉及两个方法:onLayout
和 layout
。layout
方法是 View
类提供的一个公共方法,用于触发布局过程;onLayout
方法是一个受保护的方法,需要在自定义视图中重写,用于实现具体的布局逻辑。
5.2 GridView 的 onLayout
方法
java
// 重写 onLayout 方法,实现 GridView 的布局逻辑
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
// 调用父类的 onLayout 方法
super.onLayout(changed, l, t, r, b);
// 获取适配器
ListAdapter adapter = getAdapter();
if (adapter == null) {
// 如果适配器为空,直接返回
return;
}
// 获取子视图的数量
int childCount = getChildCount();
if (childCount == 0) {
// 如果子视图数量为 0,直接返回
return;
}
// 获取左内边距
int paddingLeft = getPaddingLeft();
// 获取上内边距
int paddingTop = getPaddingTop();
// 当前列索引
int column = 0;
// 当前行索引
int row = 0;
// 遍历所有子视图
for (int i = 0; i < childCount; i++) {
// 获取当前子视图
View child = getChildAt(i);
if (child.getVisibility() == GONE) {
// 如果子视图不可见,跳过该子视图
continue;
}
// 获取子视图的布局参数
MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
// 计算子视图的左边界
int left = paddingLeft + column * (mColumnWidth + mHorizontalSpacing) + lp.leftMargin;
// 计算子视图的上边界
int top = paddingTop + row * (child.getMeasuredHeight() + mVerticalSpacing) + lp.topMargin;
// 计算子视图的右边界
int right = left + child.getMeasuredWidth();
// 计算子视图的下边界
int bottom = top + child.getMeasuredHeight();
// 调用子视图的 layout 方法进行布局
child.layout(left, top, right, bottom);
// 列索引加 1
column++;
if (column >= mNumColumns) {
// 如果列索引达到列数,列索引重置为 0,行索引加 1
column = 0;
row++;
}
}
}
在 onLayout
方法中,首先调用父类的 onLayout
方法。然后检查适配器是否为空,如果为空则直接返回。接着获取子视图的数量,如果数量为 0 也直接返回。
获取左内边距和上内边距,初始化列索引和行索引。遍历所有子视图,对于可见的子视图,计算其左、上、右、下边界,然后调用子视图的 layout
方法进行布局。最后更新列索引和行索引。
5.3 子视图的布局方法 layout
java
// 子视图的布局方法
public void layout(int l, int t, int r, int b) {
// 检查布局参数是否发生变化
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
if (changed || (mPrivateFlags & PFLAG_LAYOUT_REQUIRED) == PFLAG_LAYOUT_REQUIRED) {
// 如果布局参数发生变化或需要重新布局
onLayout(changed, l, t, r, b);
mPrivateFlags &= ~PFLAG_LAYOUT_REQUIRED;
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnLayoutChangeListeners != null) {
// 通知布局变化监听器
ArrayList<OnLayoutChangeListener> listenersCopy =
(ArrayList<OnLayoutChangeListener>)li.mOnLayoutChangeListeners.clone();
int numListeners = listenersCopy.size();
for (int i = 0; i < numListeners; ++i) {
listenersCopy.get(i).onLayoutChange(this, l, t, r, b, oldL, oldT, oldR, oldB);
}
}
}
mPrivateFlags &= ~PFLAG_FORCE_LAYOUT;
mPrivateFlags3 |= PFLAG3_IS_LAID_OUT;
}
在 layout
方法中,首先调用 setFrame
方法设置视图的边界,检查布局参数是否发生变化。如果布局参数发生变化或需要重新布局,调用 onLayout
方法进行布局,并清除 PFLAG_LAYOUT_REQUIRED
标志。如果有布局变化监听器,通知监听器布局发生了变化。最后清除 PFLAG_FORCE_LAYOUT
标志,设置 PFLAG3_IS_LAID_OUT
标志。
六、GridView 的绘制机制
6.1 绘制的基本概念
绘制是指将视图的内容显示在屏幕上的过程。在 Android 中,绘制过程主要涉及 onDraw
方法,该方法是一个受保护的方法,需要在自定义视图中重写,用于实现具体的绘制逻辑。
6.2 GridView 的 onDraw
方法
java
// 重写 onDraw 方法,实现 GridView 的绘制逻辑
@Override
protected void onDraw(Canvas canvas) {
// 调用父类的 onDraw 方法
super.onDraw(canvas);
// 获取适配器
ListAdapter adapter = getAdapter();
if (adapter == null) {
// 如果适配器为空,直接返回
return;
}
// 获取子视图的数量
int childCount = getChildCount();
if (childCount == 0) {
// 如果子视图数量为 0,直接返回
return;
}
// 绘制分隔线
drawDividers(canvas);
}
在 onDraw
方法中,首先调用父类的 onDraw
方法。然后检查适配器是否为空,如果为空则直接返回。接着获取子视图的数量,如果数量为 0 也直接返回。最后调用 drawDividers
方法绘制分隔线。
6.3 绘制分隔线的方法 drawDividers
java
// 绘制分隔线的方法
private void drawDividers(Canvas canvas) {
// 获取子视图的数量
int childCount = getChildCount();
if (childCount == 0) {
// 如果子视图数量为 0,直接返回
return;
}
// 获取左内边距
int paddingLeft = getPaddingLeft();
// 获取上内边距
int paddingTop = getPaddingTop();
// 获取右内边距
int paddingRight = getPaddingRight();
// 获取下内边距
int paddingBottom = getPaddingBottom();
// 获取分隔线绘制器
Drawable horizontalDivider = getHorizontalDivider();
Drawable verticalDivider = getVerticalDivider();
if (horizontalDivider != null) {
// 如果有水平分隔线绘制器
// 获取水平分隔线的高度
int dividerHeight = horizontalDivider.getIntrinsicHeight();
// 遍历所有行
for (int row = 0; row < (childCount + mNumColumns - 1) / mNumColumns; row++) {
// 计算当前行第一个子视图的索引
int firstChildIndex = row * mNumColumns;
if (firstChildIndex >= childCount) {
// 如果索引超出子视图数量,跳出循环
break;
}
// 获取当前行第一个子视图
View firstChild = getChildAt(firstChildIndex);
if (firstChild == null) {
// 如果子视图为空,跳过该行
continue;
}
// 计算水平分隔线的顶部位置
int top = firstChild.getBottom() + ((MarginLayoutParams) firstChild.getLayoutParams()).bottomMargin;
// 计算水平分隔线的底部位置
int bottom = top + dividerHeight;
// 设置水平分隔线的边界
horizontalDivider.setBounds(paddingLeft, top, getWidth() - paddingRight, bottom);
// 绘制水平分隔线
horizontalDivider.draw(canvas);
}
}
if (verticalDivider != null) {
// 如果有垂直分隔线绘制器
// 获取垂直分隔线的宽度
int dividerWidth = verticalDivider.getIntrinsicWidth();
// 遍历所有列
for (int column = 0; column < mNumColumns - 1; column++) {
// 计算当前列第一个子视图的索引
int firstChildIndex = column;
if (firstChildIndex >= childCount) {
// 如果索引超出子视图数量,跳出循环
break;
}
// 获取当前列第一个子视图
View firstChild = getChildAt(firstChildIndex);
if (firstChild == null) {
// 如果子视图为空,跳过该列
continue;
}
// 计算垂直分隔线的左边位置
int left = firstChild.getRight() + ((MarginLayoutParams) firstChild.getLayoutParams()).rightMargin;
// 计算垂直分隔线的右边位置
int right = left + dividerWidth;
// 设置垂直分隔线的边界
verticalDivider.setBounds(left, paddingTop, right, getHeight() - paddingBottom);
// 绘制垂直分隔线
verticalDivider.draw(canvas);
}
}
}
在 drawDividers
方法中,首先获取子视图的数量,如果数量为 0 则直接返回。然后获取内边距和分隔线绘制器。
如果有水平分隔线绘制器,遍历所有行,计算水平分隔线的顶部和底部位置,设置分隔线的边界并绘制。如果有垂直分隔线绘制器,遍历所有列,计算垂直分隔线的左边和右边位置,设置分隔线的边界并绘制。
七、GridView 的事件处理机制
7.1 事件分发的基本概念
在 Android 中,事件分发是指将触摸事件从屏幕传递到具体的视图的过程。事件分发主要涉及三个方法:dispatchTouchEvent
、onInterceptTouchEvent
和 onTouchEvent
。dispatchTouchEvent
方法用于分发事件,onInterceptTouchEvent
方法用于拦截事件,onTouchEvent
方法用于处理事件。
7.2 GridView 的 dispatchTouchEvent
方法
java
// 重写 dispatchTouchEvent 方法,实现事件分发逻辑
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 调用父类的 dispatchTouchEvent 方法进行事件分发
boolean handled = super.dispatchTouchEvent(ev);
if (!handled) {
// 如果父类没有处理该事件
// 获取适配器
ListAdapter adapter = getAdapter();
if (adapter != null && adapter.getCount() > 0) {
// 如果适配器不为空且有数据项
// 获取触摸事件的动作
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
handleDownEvent(ev);
break;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
handleMoveEvent(ev);
break;
case MotionEvent.ACTION_UP:
// 处理抬起事件
handleUpEvent(ev);
break;
case MotionEvent.ACTION_CANCEL:
// 处理取消事件
handleCancelEvent(ev);
break;
}
}
}
return handled;
}
在 dispatchTouchEvent
方法中,首先调用父类的 dispatchTouchEvent
方法进行事件分发。如果父类没有处理该事件,检查适配器是否为空且有数据项。如果满足条件,根据触摸事件的动作,调用相应的处理方法。
7.3 GridView 的 onInterceptTouchEvent
方法
java
// 重写 onInterceptTouchEvent 方法,实现事件拦截逻辑
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// 获取触摸事件的动作
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
mIsBeingDragged = false;
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
if (mIsBeingDragged) {
// 如果已经开始拖动,拦截事件
return true;
}
// 计算 X 轴和 Y 轴的移动距离
final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mLastMotionX;
final float dy = y - mLastMotionY;
final int touchSlop = ViewConfiguration.get(getContext()).getScaledTouchSlop();
if (Math.abs(dy) > touchSlop && Math.abs(dy) > Math.abs(dx)) {
// 如果 Y 轴移动距离大于触摸阈值且大于 X 轴移动距离,开始拖动,拦截事件
mIsBeingDragged = true;
return true;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
// 处理抬起和取消事件
mIsBeingDragged = false;
break;
}
// 默认不拦截事件
return false;
}
在 onInterceptTouchEvent
方法中,根据触摸事件的动作进行不同的处理。在按下事件中,初始化拖动状态和最后触摸位置。在移动事件中,计算 X 轴和 Y 轴的移动距离,如果 Y 轴移动距离大于触摸阈值且大于 X 轴移动距离,开始拖动并拦截事件。在抬起和取消事件中,重置拖动状态。默认情况下不拦截事件。
7.4 GridView 的 onTouchEvent
方法
java
// 重写 onTouchEvent 方法,实现事件处理逻辑
@Override
public boolean onTouchEvent(MotionEvent ev) {
// 获取触摸事件的动作
int action = ev.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 处理按下事件
mIsBeingDragged = false;
mLastMotionX = ev.getX();
mLastMotionY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
// 处理移动事件
if (mIsBeingDragged) {
// 如果已经开始拖动
final float x = ev.getX();
final float y = ev.getY();
final float dx = x - mLastMotionX;
final float dy = y - mLastMotionY;
// 滚动 GridView
scrollBy(0, (int) -dy);
mLastMotionX = x;
mLastMotionY = y;
}
break;
case MotionEvent.ACTION_UP:
// 处理抬起事件
mIsBeingDragged = false;
break;
case MotionEvent.ACTION_CANCEL:
// 处理取消事件
mIsBeingDragged = false;
break;
}
return true;
八、GridView 与适配器的交互
8.1 适配器的作用
在 Android 中,适配器(Adapter)是连接数据和视图的桥梁。对于 GridView
而言,适配器负责管理数据源,并将数据源中的每个数据项转换为对应的视图,然后将这些视图展示在 GridView
中。通过适配器,GridView
可以灵活地展示不同类型的数据,并且可以根据数据源的变化动态更新视图。
8.2 常见的适配器类型
ArrayAdapter
:这是一个简单的适配器,用于展示数组或列表中的数据。它可以将数组或列表中的每个元素转换为一个TextView
视图,并展示在GridView
中。SimpleAdapter
:该适配器可以将Map
类型的数据集合展示在GridView
中。它允许我们自定义每个数据项的视图布局,通过指定Map
中的键和视图控件的 ID 来实现数据绑定。BaseAdapter
:这是一个抽象类,我们可以通过继承BaseAdapter
来创建自定义的适配器。通过重写getCount
、getItem
、getItemId
和getView
等方法,我们可以实现复杂的数据展示和视图逻辑。
8.3 GridView 设置适配器的方法 setAdapter
java
// 设置适配器的方法
@Override
public void setAdapter(ListAdapter adapter) {
// 如果适配器已经设置,移除旧的适配器数据观察者
if (mAdapter != null && mDataSetObserver != null) {
mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
// 重置选择状态
resetList();
// 清空回收视图池
mRecycler.clear();
if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
// 如果有头部或尾部视图,使用包装适配器
mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
} else {
// 否则直接使用传入的适配器
mAdapter = adapter;
}
// 记录适配器的原始值
mOldAdapter = adapter;
if (mAdapter != null) {
// 如果适配器不为空
mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
mOldItemCount = mItemCount;
// 获取适配器的数据项数量
mItemCount = mAdapter.getCount();
checkFocus();
// 创建新的数据观察者
mDataSetObserver = new AdapterDataSetObserver();
// 注册数据观察者
mAdapter.registerDataSetObserver(mDataSetObserver);
// 获取适配器的视图类型数量
mViewTypeCount = mAdapter.getViewTypeCount();
// 初始化回收视图池
mRecycler.setViewTypeCount(mViewTypeCount);
// 获取第一个数据项的视图类型
int position = lookForSelectablePosition(0, true);
// 设置选择位置
setSelectedPositionInt(position);
setNextSelectedPositionInt(position);
if (mItemCount == 0) {
// 如果数据项数量为 0,检查空视图并进行相应处理
checkEmptyView();
}
} else {
// 如果适配器为空
mAreAllItemsSelectable = true;
checkFocus();
// 清空选择状态
resetList();
// 检查空视图并进行相应处理
checkEmptyView();
}
// 重新布局 GridView
requestLayout();
}
在 setAdapter
方法中,首先检查是否已经设置了适配器,如果是,则移除旧的适配器数据观察者。然后重置选择状态,清空回收视图池。根据是否有头部或尾部视图,决定使用包装适配器还是直接使用传入的适配器。
如果适配器不为空,获取适配器的数据项数量,创建并注册数据观察者,初始化回收视图池,设置选择位置。如果数据项数量为 0,检查空视图。如果适配器为空,清空选择状态并检查空视图。最后,请求重新布局 GridView
。
8.4 适配器的数据更新机制
当适配器的数据发生变化时,需要通知 GridView
进行更新。适配器提供了 notifyDataSetChanged
方法来实现这一功能。以下是 BaseAdapter
中 notifyDataSetChanged
方法的调用流程:
java
// BaseAdapter 中的 notifyDataSetChanged 方法
@Override
public void notifyDataSetChanged() {
// 调用观察者的 onChanged 方法
mDataSetObservable.notifyChanged();
}
// DataSetObservable 中的 notifyChanged 方法
public void notifyChanged() {
synchronized(mObservers) {
// 遍历所有观察者
for (int i = mObservers.size() - 1; i >= 0; i--) {
// 调用观察者的 onChanged 方法
mObservers.get(i).onChanged();
}
}
}
// AdapterDataSetObserver 中的 onChanged 方法
@Override
public void onChanged() {
mDataChanged = true;
mOldItemCount = mItemCount;
// 获取适配器的数据项数量
mItemCount = getAdapter().getCount();
if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
&& mOldItemCount == 0 && mItemCount > 0) {
// 如果适配器有稳定的 ID,且之前数据项数量为 0,现在有数据项
AdapterView.this.onRestoreInstanceState(mInstanceState);
mInstanceState = null;
} else {
// 记住选择状态
rememberSyncState();
}
// 重置数据
resetList();
// 重新布局 GridView
requestLayout();
}
当调用 notifyDataSetChanged
方法时,会触发 DataSetObservable
中的 notifyChanged
方法,该方法会遍历所有的观察者并调用它们的 onChanged
方法。在 AdapterDataSetObserver
的 onChanged
方法中,会更新数据项数量,根据情况恢复选择状态,重置数据并请求重新布局 GridView
。
九、GridView 的缓存机制
9.1 缓存的作用
在 Android 开发中,视图的创建和销毁是一个相对耗时的操作。对于 GridView
来说,如果每次滚动时都创建新的视图,会导致性能下降。因此,GridView
采用了缓存机制,通过复用已经创建的视图来减少视图的创建和销毁次数,从而提高性能。
9.2 GridView 的回收视图池 Recycler
Recycler
是 GridView
中用于管理回收视图的类。它的主要作用是将不再显示的视图缓存起来,当需要显示新的数据项时,优先从回收视图池中获取可用的视图进行复用。
以下是 Recycler
类的部分关键代码:
java
// Recycler 类
class Recycler {
// 视图类型数量
private int mViewTypeCount;
// 每个视图类型对应的回收视图列表
private ArrayList<View>[] mScrapViews;
// 设置视图类型数量
public void setViewTypeCount(int viewTypeCount) {
if (viewTypeCount < 1) {
throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
}
// 创建对应数量的回收视图列表
ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
for (int i = 0; i < viewTypeCount; i++) {
scrapViews[i] = new ArrayList<View>();
}
mViewTypeCount = viewTypeCount;
mScrapViews = scrapViews;
}
// 获取指定视图类型的回收视图
public View getScrapView(int position) {
if (mViewTypeCount == 1) {
// 如果视图类型数量为 1,直接从第一个回收视图列表中获取
return retrieveFromScrap(mScrapViews[0], position);
} else {
// 获取指定位置的数据项的视图类型
int viewType = mAdapter.getItemViewType(position);
if (viewType >= 0 && viewType < mViewTypeCount) {
// 从对应视图类型的回收视图列表中获取
return retrieveFromScrap(mScrapViews[viewType], position);
}
}
return null;
}
// 将视图添加到回收视图池
public void addScrapView(View scrap, int position) {
if (mViewTypeCount == 1) {
// 如果视图类型数量为 1,添加到第一个回收视图列表
mScrapViews[0].add(scrap);
} else {
// 获取视图的视图类型
int viewType = mAdapter.getItemViewType(position);
if (viewType >= 0 && viewType < mViewTypeCount) {
// 添加到对应视图类型的回收视图列表
mScrapViews[viewType].add(scrap);
}
}
}
// 从回收视图列表中检索视图
private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
if (scrapViews != null && !scrapViews.isEmpty()) {
// 获取最后一个回收视图
int size = scrapViews.size();
for (int i = size - 1; i >= 0; i--) {
View view = scrapViews.get(i);
if (view != null) {
// 移除并返回该视图
scrapViews.remove(i);
return view;
}
}
}
return null;
}
}
在 Recycler
类中,setViewTypeCount
方法用于设置视图类型数量,并创建对应数量的回收视图列表。getScrapView
方法用于获取指定视图类型的回收视图,addScrapView
方法用于将视图添加到回收视图池,retrieveFromScrap
方法用于从回收视图列表中检索视图。
9.3 适配器中视图的复用
在适配器的 getView
方法中,会使用 Recycler
提供的回收视图池来复用视图。以下是一个简单的适配器 getView
方法示例:
java
// 自定义适配器的 getView 方法
@Override
public View getView(int position, View convertView, ViewGroup parent) {
if (convertView == null) {
// 如果 convertView 为空,创建新的视图
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_gridview, parent, false);
}
// 获取视图中的控件
TextView textView = convertView.findViewById(R.id.textView);
// 设置控件的文本内容
textView.setText(mData.get(position));
return convertView;
}
在上述代码中,首先检查 convertView
是否为空。如果为空,使用 LayoutInflater
创建新的视图;如果不为空,则复用该视图。然后获取视图中的控件,并设置其文本内容。通过这种方式,减少了视图的创建和销毁次数,提高了性能。
十、GridView 的性能优化
10.1 减少视图创建和销毁
- 复用视图 :如前面所述,在适配器的
getView
方法中,使用convertView
来复用已经创建的视图,避免每次都创建新的视图。 - 使用
ViewHolder
模式 :ViewHolder
模式是一种常见的优化技巧,通过在ViewHolder
类中缓存视图中的控件,避免在每次调用getView
方法时都进行findViewById
操作。以下是一个使用ViewHolder
模式的适配器示例:
java
// 自定义适配器类,继承自 BaseAdapter
private class MyAdapter extends BaseAdapter {
private Context mContext;
private List<String> mData;
public MyAdapter(Context context, List<String> data) {
mContext = context;
mData = data;
}
@Override
public int getCount() {
return mData.size();
}
@Override
public Object 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) {
// 如果 convertView 为空,创建新的视图
convertView = LayoutInflater.from(mContext).inflate(R.layout.item_gridview, parent, false);
// 创建 ViewHolder 对象
holder = new ViewHolder();
// 缓存视图中的控件
holder.textView = convertView.findViewById(R.id.textView);
// 将 ViewHolder 对象设置为视图的标签
convertView.setTag(holder);
} else {
// 如果 convertView 不为空,从视图的标签中获取 ViewHolder 对象
holder = (ViewHolder) convertView.getTag();
}
// 设置控件的文本内容
holder.textView.setText(mData.get(position));
return convertView;
}
// ViewHolder 类,用于缓存视图中的控件
private static class ViewHolder {
TextView textView;
}
}
在上述代码中,定义了一个 ViewHolder
类,用于缓存视图中的 TextView
控件。在 getView
方法中,如果 convertView
为空,创建新的视图和 ViewHolder
对象,并将 ViewHolder
对象设置为视图的标签;如果 convertView
不为空,从视图的标签中获取 ViewHolder
对象。这样可以避免每次调用 getView
方法时都进行 findViewById
操作,提高了性能。
10.2 优化数据加载
- 异步加载数据 :如果数据加载过程比较耗时,如从网络或数据库中获取数据,应该使用异步任务(如
AsyncTask
、Thread
或RxJava
)来加载数据,避免阻塞主线程。以下是一个使用AsyncTask
异步加载数据的示例:
java
// 异步任务类,用于加载数据
private class LoadDataTask extends AsyncTask<Void, Void, List<String>> {
@Override
protected List<String> doInBackground(Void... voids) {
// 在后台线程中加载数据
List<String> data = new ArrayList<>();
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 20; i++) {
data.add("Item " + i);
}
return data;
}
@Override
protected void onPostExecute(List<String> data) {
super.onPostExecute(data);
// 在主线程中更新 UI
MyAdapter adapter = new MyAdapter(MainActivity.this, data);
gridView.setAdapter(adapter);
}
}
在上述代码中,定义了一个 LoadDataTask
类,继承自 AsyncTask
。在 doInBackground
方法中,模拟耗时操作加载数据;在 onPostExecute
方法中,在主线程中更新 GridView
的适配器。
- 分页加载数据:当数据量较大时,一次性加载所有数据会导致内存占用过高和性能下降。可以采用分页加载的方式,每次只加载部分数据,当用户滚动到列表底部时,再加载下一页数据。
10.3 避免过度绘制
- 减少不必要的背景设置 :如果
GridView
或其子视图设置了多层背景,会导致过度绘制。尽量减少不必要的背景设置,只保留必要的背景。 - 使用透明背景:在不需要背景的地方,使用透明背景,避免不必要的绘制操作。
10.4 优化布局文件
- 减少布局嵌套:布局嵌套过多会增加布局的测量和绘制时间。尽量使用扁平的布局结构,避免过多的嵌套。
- 使用
merge
标签 :merge
标签可以减少布局的层级,当一个布局文件的根元素是LinearLayout
或RelativeLayout
,并且该布局文件会被include
到另一个布局文件中时,可以使用merge
标签代替根元素。
十一、GridView 的自定义扩展
11.1 自定义布局参数
可以通过继承 MarginLayoutParams
类来创建自定义的布局参数,以满足特定的布局需求。以下是一个自定义布局参数的示例:
java
// 自定义布局参数类,继承自 MarginLayoutParams
public static class MyLayoutParams extends MarginLayoutParams {
public int customAttribute;
public MyLayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
// 从属性集合中获取自定义属性的值
TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.MyLayoutParams);
customAttribute = a.getInt(R.styleable.MyLayoutParams_customAttribute, 0);
a.recycle();
}
public MyLayoutParams(int width, int height) {
super(width, height);
}
public MyLayoutParams(MarginLayoutParams source) {
super(source);
}
}
在上述代码中,定义了一个 MyLayoutParams
类,继承自 MarginLayoutParams
。在构造函数中,从属性集合中获取自定义属性 customAttribute
的值。
11.2 自定义分隔线
可以通过重写 drawDividers
方法来实现自定义的分隔线效果。以下是一个自定义分隔线的示例:
java
// 自定义 GridView 类,继承自 GridView
public class CustomGridView extends GridView {
private Paint mDividerPaint;
public CustomGridView(Context context) {
super(context);
init();
}
public CustomGridView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
public CustomGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init();
}
private void init() {
// 初始化分隔线绘制器
mDividerPaint = new Paint();
mDividerPaint.setColor(Color.RED);
mDividerPaint.setStrokeWidth(2);
}
// 重写 drawDividers 方法,实现自定义分隔线绘制
@Override
protected void drawDividers(Canvas canvas) {
int childCount = getChildCount();
if (childCount == 0) {
return;
}
int paddingLeft = getPaddingLeft();
int paddingTop = getPaddingTop();
int paddingRight = getPaddingRight();
int paddingBottom = getPaddingBottom();
// 绘制水平分隔线
for (int i = 0; i < childCount; i += getNumColumns()) {
View child = getChildAt(i);
if (child != null) {
int top = child.getBottom() + ((MarginLayoutParams) child.getLayoutParams()).bottomMargin;
canvas.drawLine(paddingLeft, top, getWidth() - paddingRight, top, mDividerPaint);
}
}
// 绘制垂直分隔线
for (int i = 0; i < getNumColumns() - 1; i++) {
View child = getChildAt(i);
if (child != null) {
int left = child.getRight() + ((MarginLayoutParams) child.getLayoutParams()).rightMargin;
canvas.drawLine(left, paddingTop, left, getHeight() - paddingBottom, mDividerPaint);
}
}
}
}
在上述代码中,定义了一个 CustomGridView
类,继承自 GridView
。在 init
方法中,初始化分隔线绘制器。重写 drawDividers
方法,使用 Canvas
和 Paint
绘制自定义的分隔线。
11.3 自定义选择效果
可以通过设置 selector
属性来实现自定义的选择效果。以下是一个自定义选择效果的示例:
xml
<!-- 自定义选择器文件 selector_gridview_item.xml -->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_selected="true" android:drawable="@color/selected_color" />
<item android:state_pressed="true" android:drawable="@color/pressed_color" />
<item android:drawable="@android:color/transparent" />
</selector>
java
// 在代码中设置选择器
gridView.setSelector(R.drawable.selector_gridview_item);
在上述代码中,定义了一个自定义的选择器文件 selector_gridview_item.xml
,根据不同的状态(选中、按下)设置不同的背景颜色。然后在代码中使用 setSelector
方法为 GridView
设置选择器。
十二、总结与展望
12.1 总结
通过对 Android GridView
的深入分析,我们全面了解了其使用原理和内部机制。GridView
作为一个重要的 UI 组件,在 Android 开发中有着广泛的应用。它以二维网格的形式展示数据,为用户提供了直观、便捷的交互体验。
从源码层面来看,GridView
的测量、布局、绘制和事件处理等机制都有着严谨的设计和实现。测量机制决定了 GridView
和其子视图的大小,布局机制将子视图放置在合适的位置,绘制机制将视图的内容显示在屏幕上,事件处理机制则负责处理用户的触摸事件。
适配器是 GridView
与数据源之间的桥梁,通过适配器,GridView
可以灵活地展示不同类型的数据。同时,GridView
采用了缓存机制,通过复用已经创建的视图来提高性能。
在实际开发中,我们可以通过优化视图创建和销毁、数据加载、避免过度绘制和优化布局文件等方式来提高 GridView
的性能。此外,我们还可以通过自定义布局参数、分隔线和选择效果等方式来扩展 GridView
的功能。
12.2 展望
虽然 GridView
在 Android 开发中有着重要的地位,但随着技术的不断发展,也面临着一些挑战和机遇。
挑战
- 性能优化的挑战 :随着数据量的不断增大和用户对界面响应速度的要求越来越高,
GridView
的性能优化仍然是一个重要的挑战。需要不断探索新的优化方法和技术,以提高GridView
的性能。 - 兼容性问题 :不同版本的 Android 系统可能对
GridView
的实现有所不同,这可能会导致兼容性问题。需要在开发过程中充分考虑不同版本的兼容性,确保GridView
在各种设备上都能正常显示和使用。
机遇
- 与新技术的结合 :随着 Android 开发技术的不断发展,如 Kotlin 语言、Jetpack 组件库等,可以将这些新技术与
GridView
结合使用,提高开发效率和代码质量。 - 个性化定制需求的增加 :用户对应用界面的个性化需求越来越高,
GridView
可以通过进一步的自定义扩展,满足用户的个性化需求,提供更加丰富和独特的交互体验。
未来,GridView
有望在性能优化、兼容性处理和个性化定制等方面取得更大的进展,为 Android 开发带来更多的便利和可能性。开发者可以根据实际需求,灵活运用 GridView
的各种特性,开发出更加优秀的 Android 应用。