一、前言
Android官方提供了很多支持Adapter的布局,提供了实现网格布局的 GridView、还有 RecyclerView 等,都是通过Adapter实现适配的。
其实使用Adapter不仅仅可以优化大量Item的展示,实际上对于少量Item且View容易重复的View,显然复用也可以减少性能损耗。
另一方面,在开发过程中,发现很少有人使用Adapter自定义布局,因此也没人借助此机制去做一些View复用的优化,减少requestLayout和inflate。
1.1 本篇案例
本篇我们以自定义一个网格布局。
我们知道,Android GridView与ListView等都能实现网格布局,但是在互相嵌套的问题上冲突很多,尤其是 ListView 中 GridView 事件冲突导致体验相当不好。
1.2 Adapter优势
-
复用性强
-
可动态刷新
-
可扩展性高
二、实现
2.1 Adapter接口
本篇我们自己定义了一套Adapter机制,使用Adapter的目的是减少View的创建,实现View的复用,那么有哪些机制呢?看过RecyclerView或者ListView,我们就能明白,其能力来自两个方面:
-
Adapter 匹配机制
-
DataSetObserver机制
也就是说,我们Android中的Adapter使用的同时还有观察者模式,两者组合起来是爱你View的刷新和复用。
2.2 刷新和复用机制
我们参考RecyclerView或者ListView,手续爱你是替换Adapter,其次是注册监听器,本篇我们自定义了监听器GridDataSetObserver
public static class GridDataSetObserver extends DataSetObserver{
private AutoFixedHeightGridView fixedHeightGridView;
public GridDataSetObserver(AutoFixedHeightGridView fixedHeightGridView) {
this.fixedHeightGridView = fixedHeightGridView;
}
@Override
public void onChanged() {
super.onChanged();
fixedHeightGridView.datasetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
fixedHeightGridView.datasetChanged();
}
}
为了避免泄露问题,一般来说还要通过setAdapter进行替换DataSetObserver
public void setAdapter(BaseAdapter mAdapter) {
if(this.mAdapter!=null && mDataSetObserver != null){
this.mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
if(mDataSetObserver==null){
mDataSetObserver = new GridDataSetObserver(this);
}
this.mAdapter = mAdapter;
if(this.mAdapter!=null) {
this.mAdapter.registerDataSetObserver(mDataSetObserver);
this.mAdapter.notifyDataSetChanged();
}else{
datasetChanged();
}
}
到这里,通过上述方式就实现了Adapter的设置和观察者注册
2.3 布局逻辑
我们这里要实现的自适应高度的GridView,因此,这里在测量View前需要记录高度每行View的高度的最大值,方便View按行对齐
final Map<Integer,Integer> rowMaxHeight = new HashMap<>();
2.4 无刷新View 添加和移除
我们知道,调用addView或者removeView时,容易出发requestLayout,这个显然不是我们想要的。
这里方式有两种
attachViewToParent();
detachViewFromParent();
以及
removeViewInLayout()
addViewInLayout()
前者相比后者更加轻量,但是这里为了提前设置一些状态,我们选用后者
if(type!=viewType){
view = this.mAdapter.getView(i, null, this);
if(convertView!=null) {
removeViewsInLayout(i,1);//使用removeViewInLayout避免频繁刷新,最后统一调用requestLayout
addViewInLayout(view, i); //使用addViewInLayout避免频繁刷新,最后统一调用requestLayout
}else{
addViewInLayout(view,1);
}
}else{
view = this.mAdapter.getView(i, convertView, this);
if(convertView!=null && view!=convertView){
removeViewsInLayout(i,1);
addViewInLayout(view,i);
}
}
2.5 完整代码
下面是本篇的完整逻辑,使用方式基本和GridView一样
public class AutoFixedHeightGridView extends ViewGroup {
private BaseAdapter mAdapter;
private DataSetObserver mDataSetObserver;
private int columnNums;
private int columnPadding;
private int rowPadding;
final Map<Integer,Integer> rowMaxHeight = new HashMap<>();
public AutoFixedHeightGridView(Context context) {
this(context,null);
}
public AutoFixedHeightGridView(Context context, AttributeSet attrs) {
this(context, attrs,0);
}
public AutoFixedHeightGridView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
final TypedArray a = context.obtainStyledAttributes(
attrs, R.styleable.AutoFixedHeightGridView, defStyleAttr, 0);
columnNums = a.getInt(R.styleable.AutoFixedHeightGridView_numColumns, 1);
int hSpacing = a.getDimensionPixelOffset(
R.styleable.AutoFixedHeightGridView_horizontalSpacing, 0);
columnPadding = hSpacing;
int vSpacing = a.getDimensionPixelOffset(
R.styleable.AutoFixedHeightGridView_verticalSpacing, 0);
rowPadding = vSpacing;
a.recycle();
}
public void setAdapter(BaseAdapter mAdapter) {
if(this.mAdapter!=null && mDataSetObserver != null){
this.mAdapter.unregisterDataSetObserver(mDataSetObserver);
}
if(mDataSetObserver==null){
mDataSetObserver = new GridDataSetObserver(this);
}
this.mAdapter = mAdapter;
if(this.mAdapter!=null) {
this.mAdapter.registerDataSetObserver(mDataSetObserver);
this.mAdapter.notifyDataSetChanged();
}else{
datasetChanged();
}
}
public BaseAdapter getAdapter() {
return mAdapter;
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int rowNums = 0;
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = getPaddingLeft() + getPaddingRight();
if(columnNums>1){
widthSize += (columnNums-1) * columnPadding;
}
}
if (heightMode == MeasureSpec.UNSPECIFIED || heightMode == MeasureSpec.AT_MOST) {
heightSize = getPaddingBottom() + getPaddingTop();
int contentWidth = widthSize - (getPaddingLeft() + getPaddingRight());
if (columnNums > 1) {
contentWidth = contentWidth - (columnNums - 1) * columnPadding;
}
int columnWidth = contentWidth / columnNums;
int count = this.getAdapter() != null ? this.getAdapter().getCount() : 0;
if (count != getChildCount()) return;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.rightMargin = 0;
params.leftMargin = 0;
int childHeightSpec = getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
MeasureSpec.UNSPECIFIED), 0, params.height);
int childWidthSpec = getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY), 0, columnWidth);
child.measure(childWidthSpec, childHeightSpec);
int childHeight = child.getMeasuredHeight();
if (i % columnNums == 0 && i > 1) {
rowNums++;
}
Integer lastMaxHeight = rowMaxHeight.get(rowNums);
if (lastMaxHeight == null) {
lastMaxHeight = 0;
}
rowMaxHeight.put(rowNums, Math.max(lastMaxHeight, childHeight));
}
int rowCount = (int) Math.ceil(count * 1.0f / columnNums);
for (int i = 0; i < rowCount; i++) {
heightSize += rowMaxHeight.get(i);
}
if (rowCount > 1) {
heightSize += (rowCount - 1) * rowPadding;
}
}else{
int contentWidth = widthSize - (getPaddingLeft() + getPaddingRight());
if (columnNums > 1) {
contentWidth = contentWidth - (columnNums - 1) * columnPadding;
}
int columnWidth = contentWidth / columnNums;
int count = this.getAdapter() != null ? this.getAdapter().getCount() : 0;
if (count != getChildCount()) return;
for (int i = 0; i < getChildCount(); i++) {
final View child = getChildAt(i);
LayoutParams params = (LayoutParams) child.getLayoutParams();
params.rightMargin = 0;
params.leftMargin = 0;
int childHeightSpec = getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
MeasureSpec.UNSPECIFIED), 0, heightSize);
int childWidthSpec = getChildMeasureSpec(
MeasureSpec.makeMeasureSpec(columnWidth, MeasureSpec.EXACTLY), 0, columnWidth);
child.measure(childWidthSpec, childHeightSpec);
}
}
setMeasuredDimension(widthSize, heightSize);
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
int count = this.getAdapter()!=null?this.getAdapter().getCount():0;
int rowCount = 0;
int nextChildLeft = getPaddingLeft();
int nextChildTop = getPaddingTop();
int contentWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight());
if(columnNums>1){
contentWidth = contentWidth - (columnNums-1) * columnPadding;
}
int columnWidth = contentWidth /columnNums;
if(count!=getChildCount()) return;
for (int i=0;i<count;i++){
final View child = getChildAt(i);
if(i>0 && i%columnNums==0){
Integer H = rowMaxHeight.get(rowCount);
rowCount++;
nextChildLeft = getPaddingLeft();
if(H==null){
H = 0;
}
nextChildTop = nextChildTop +H + rowPadding;
}
child.layout(nextChildLeft,nextChildTop,nextChildLeft+columnWidth,nextChildTop+child.getMeasuredHeight());
nextChildLeft = nextChildLeft+child.getMeasuredWidth() + columnPadding ;
}
}
protected void datasetChanged() {
if(this.mAdapter==null){
removeAllViews();
return;
}
final int count = this.mAdapter.getCount();
for (int i=0;i<count;i++){
View convertView = null;
final int viewTypeCount = this.mAdapter.getViewTypeCount();
final int viewType = this.mAdapter.getItemViewType(i);
if(viewType>viewTypeCount) {
throw new IllegalArgumentException("viewType is correct");
}
View view = null;
if(i<getChildCount()){
int type = LayoutParams.TYPE_UNDFINED;
convertView = getChildAt(i);
if(convertView!=null){
type = ((LayoutParams)convertView.getLayoutParams()).getViewType();
}else {
type = LayoutParams.TYPE_UNDFINED;
}
if(type!=viewType){
view = this.mAdapter.getView(i, null, this);
if(convertView!=null) {
removeViewsInLayout(i,1);//使用removeViewInLayout避免频繁刷新,最后统一调用requestLayout
addViewInLayout(view, i); //使用addViewInLayout避免频繁刷新,最后统一调用requestLayout
}else{
addViewInLayout(view,1);
}
}else{
view = this.mAdapter.getView(i, convertView, this);
if(convertView!=null && view!=convertView){
removeViewsInLayout(i,1);
addViewInLayout(view,i);
}
}
}else{
view = this.mAdapter.getView(i, convertView, this);
addViewInLayout(view);
}
if(view!=null){
((LayoutParams)(view.getLayoutParams())).viewType = viewType;
}
}
int delta = getChildCount() - count;
if(delta>0){
removeViewsInLayout(getChildCount()-delta,delta);
}
requestLayout();
invalidate();
}
protected void addViewInLayout(View child){
this.addViewInLayout(child,-1);
}
/**
*
* @param child
* @param index 位置,如果是-1,则自动添加到末尾
*/
protected void addViewInLayout(View child,int index){
if (child == null) {
throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
}
ViewGroup.LayoutParams params = child.getLayoutParams();
if (params == null) {
params = generateDefaultLayoutParams();
if (params == null) {
throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
}
}
super.addViewInLayout(child,index,params);
}
public static class GridDataSetObserver extends DataSetObserver{
private AutoFixedHeightGridView fixedHeightGridView;
public GridDataSetObserver(AutoFixedHeightGridView fixedHeightGridView) {
this.fixedHeightGridView = fixedHeightGridView;
}
@Override
public void onChanged() {
super.onChanged();
fixedHeightGridView.datasetChanged();
}
@Override
public void onInvalidated() {
super.onInvalidated();
fixedHeightGridView.datasetChanged();
}
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() { //child默认布局参数
return new LayoutParams(LayoutParams.MATCH_PARENT,LayoutParams.MATCH_PARENT);
}
@Override
protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
return new LayoutParams(p);
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) { //重写该方法,否则
return (p instanceof LayoutParams) ;
}
@Override
public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
return new LayoutParams(getContext(), attrs);
}
public static class LayoutParams extends ViewGroup.MarginLayoutParams{
private int viewType;
public static final int TYPE_UNDEFINED = 0;
public LayoutParams(Context c, AttributeSet attrs) {
super(c, attrs);
TypedArray a=c.obtainStyledAttributes(attrs, R.styleable.AutoFixedHeightGridView);
this.viewType = a.getInt(R.styleable.AutoFixedHeightGridView_viewType, TYPE_UNDEFINED); //该属性定义给child的
a.recycle();
}
public LayoutParams(int width, int height) {
super(width, height);
this.viewType = TYPE_UNDEFINED;
}
public LayoutParams(MarginLayoutParams source) {
super(source);
if(source instanceof LayoutParams){
this.viewType = ((LayoutParams)source).viewType;
}else{
this.viewType = TYPE_UNDEFINED;
}
}
public LayoutParams(ViewGroup.LayoutParams source) {
super(source);
if(source instanceof LayoutParams){
this.viewType = ((LayoutParams)source).viewType;
} else{
this.viewType = TYPE_UNDEFINED;
}
}
public int getViewType(){
return viewType;
}
}
}
2.6 自定义属性部分
这里为了支持一些属性,我们给布局加一些样式
<?xml version="1.0" encoding="utf-8"?>
<resources>
<declare-styleable name="AutoHeightFiexedGrideView">
<attr name="numColumns" format="integer"/>
<attr name="horizontalSpacing" format="dimen|reference"/>
<attr name="verticalSpacing" format="dimen|reference"/>
<attr name="viewType" format="integer" />
</declare-styleable>
</resources>
三、总结
本文的重点是利用Adapter实现View的复用,我们经常遇到addView、removeView频繁操作,触发requestLayout,通过本篇,我们还利用removeViewInlayout和addViewLayout,抑制requestLayout,优化了耗时逻辑。