非对称网格布局AsymmetricGridView小解

由于项目要求定制桌面,并且根据应用功能的主次来决定应用图标的大小,有的垮行、有的跨列。当时还不知道有这个开源项目可以用,所以就用LinearLayout+weight的方式实现的,但也只实现了跨列布局,并且每行的图标weight都必须相等。总之当图标比重变化的时候就要做一番调整,使用性很差。

虽然网上很多关于磁贴的实现方式都是利用上述的方式实现,但后来知道AsymmetricGridView可以解决的时候立马放弃了上述的方式。用起来也挺方便的,但由于我的使用情景是桌面图标显示,所以在使用的过程发现其存在两个缺点:

  1. 默认单元列个数为3,每行图标所占单元列数总和大于3时,布局比例就失调了。
  2. 行高与单元列宽相等,这就导致在一些屏幕尺寸上图标超出屏幕显示区域。

一、AsymmetricGridView的实现原理

从GitHub上下在源码后,查看其工程目录可以发现library目录。其中就是实现非对称布局的最重要部分,该实现方式有两种:1.基于GridView实现;2.基于RecyclerView实现;本文针对GridView方式进行进行分析。

说到GridView,不外乎两点:1.数据源;2.适配器Adapter。AsymmetricGridView的数据源是DemoItem,也就是子项显示的数据;适配器Adapter是AsymmetricGridViewAdapter。所以我们先从AsymmetricGridViewAdapter入手。

AsymmetricGridViewAdapter的作用是基于GridView的适配器,封装成AsymmetricGridView使用的适配器。其中主要用到的类有:“AppsAdapter”和“AdapterImpl”,前者是GridView使用的适配器,后者提供计算行高、列宽的方法,是实现非对称布局的重点研究对象。在此先总体描述下该布局的实现方式:

1. 先将AppAdapter中的数据DemoItem按行所占的比例计算分类成行数据。例如四个子控件,行总单元数为4.每个控件所占比例为:1、2、1、2;那么前三个控件:1+2+1=4,放在第一行、第四个控件放第二行。以此类推。

2. 根据屏幕尺寸大小、行高总单元数和控件所占单元数来计算每个字控件的宽高尺寸。

3. 根据子控件尺寸参数和所在位置填充。

1. AppsAdapter

该类实现方式与普通的GridView适配器实现方式一致,其中强调两个方法:

        /**
	 * 获取Item 类型个数
	 */
	@Override 
	public int getViewTypeCount() {
		return 3;
	}

	/**
	 * 获取Item类型 :
	 * @param : position
	 * 			0: 长 = 宽
	 * 			1: 长 > 宽
	 * 			2: 长 < 宽
	 */
	@Override 
	public int getItemViewType(int position) {
		float clum = mItems.get(position).getColumnSpan();
		float row = mItems.get(position).getRowSpan();
		if(row == clum){
			return 0;
		}else if(row > clum){
			return 1;
		}else {
			return 2;
		}
	}

这两个方法的返回值会在“AdapterImpl”中用到。

2. AdapterImpl

为了更好的理解,我们根据AsymmetricGridViewAdapter的实现方式来研究AdapterImpl。先贴上代码:

package com.felipecsl.asymmetricgridview;


import android.content.Context;
import android.database.DataSetObserver;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.WrapperListAdapter;

/**
 * 对 AppsAdapter 进行封装,并增加一些接口
 * @author chenyuye
 */
public final class AsymmetricGridViewAdapter extends BaseAdapter
    implements AGVBaseAdapterDao, WrapperListAdapter {
	//AppsAdapter存放数据源
	private final ListAdapter appsAdapter;
	//适配器接口
	private final AdapterImpl adapterImpl;
	/**
	 * 对 AppsAdapter 进行封装,并增加一些接口
	 * @param context
	 * @param listView  AsymmetricGridView
	 * @param adapter  AppsAdapter
	 */
	public AsymmetricGridViewAdapter(Context context, AsymmetricGridView listView,
			ListAdapter adapter) {
		this.adapterImpl = new AdapterImpl(context, this, listView);
		this.appsAdapter = adapter;
		//注册数据源变化观察者
		appsAdapter.registerDataSetObserver(new GridDataSetObserver());
	}

	class GridDataSetObserver extends DataSetObserver {
		
		@Override 
		public void onChanged() {
			recalculateItemsPerRow();
		}

		@Override 
		public void onInvalidated() {
			recalculateItemsPerRow();
		}
	}
	/**
	 * 重新计算Item获取每行数据
	 */
	void recalculateItemsPerRow() {
		adapterImpl.recalculateItemsPerRow();
	}
	/**
	 * 获取AsymmetricItem
	 */
	@Override 
	public AsymmetricItemDao getItem(int position) {
		return (AsymmetricItemDao) appsAdapter.getItem(position);
	}

	
	@Override 
	public AsymmetricViewHolder onCreateAsymmetricViewHolder(
			int position, ViewGroup parent, int viewType) {
		return new AsymmetricViewHolder<>(appsAdapter.getView(position, null, parent));
	}

	@Override 
	public void onBindAsymmetricViewHolder(
      AsymmetricViewHolder holder, ViewGroup parent, int position) {
		appsAdapter.getView(position, holder.itemView, parent);
	}

	@Override 
	public long getItemId(int position) {
		return appsAdapter.getItemId(position);
	}

	/**
	 * 重点关注,子控件布局由这里实现
	 */
	@Override 
	public View getView(int position, View convertView, ViewGroup parent) {
		//创建ListView的子Item  LinearLayout
		AdapterImpl.ViewHolder viewHolder = adapterImpl.onCreateViewHolder();
		adapterImpl.onBindViewHolder(viewHolder, position, parent);
		return viewHolder.itemView;
	}

	/**
	 * Returns the row count for ListView display purposes
	 */
	@Override 
	public int getCount() {
	    return adapterImpl.getRowCount();
	}
	/**
	 * 获取Item类型 偶数排列:1 基数排列:0
	 */
	@Override 
	public int getItemViewType(int position) {
		return appsAdapter.getItemViewType(position);
	}

	@Override 
	public int getActualItemCount() {
		return appsAdapter.getCount();
	}

	@Override 
	public ListAdapter getWrappedAdapter() {
		return appsAdapter;
	}
}

该适配器继承自“BaseAdapter”,所以会重写几个方法:getView(),getCount()等,由于该布局实现方式是逐行排列,故getCount()是获取每行的子控件个数,这个参数是从AdapterImpl中获取。另外实现了两个接口:

AGVBaseAdapterDao和WrapperListAdapter。其中getActualItemCount()是获取全部数据源的个数,从AppAdapter中获取。其他方法在接下来的分析中再作说明。

getView()是该适配器的入口,也就是每行子控件的布局具体实现方式。每行控件用RecyclerView.ViewHolder来存储,接下来我们来分析getView()所调用的两个方法:onCreateViewHolder()和onBindViewHolder()。

onCreateViewHolder():目的是创建一个没有子控件的LinearLayout提供onBindViewHolder()使用。

/**
	 * 创建ListView的Item的LinearLayout
	 * @return
	 */
	ViewHolder onCreateViewHolder() {
		if (debugEnabled) {
			Log.d(TAG, "onCreateViewHolder()");
		}
		LinearLayout layout = new LinearLayout(mContext, null);
		if (debugEnabled) {
			layout.setBackgroundColor(Color.parseColor("#83F27B"));
		}

		layout.setShowDividers(LinearLayout.SHOW_DIVIDER_MIDDLE);
		layout.setDividerDrawable(
				ContextCompat.getDrawable(mContext, R.drawable.felipescsl_item_divider_horizontal));

		AbsListView.LayoutParams layoutParams = new AbsListView.LayoutParams(
        AbsListView.LayoutParams.MATCH_PARENT, AbsListView.LayoutParams.WRAP_CONTENT);
		layout.setLayoutParams(layoutParams);
		return new ViewHolder(layout);
	}

onBindViewHolder():实现非对称布局的重点方法

/**
	 * 重点函数,子空间对排列实现
	 * @param holder    ListView Item
	 * @param position  当前应用对位置
	 * @param parent   ListView 父控件
	 */
	void onBindViewHolder(ViewHolder holder, int position, ViewGroup parent) {
		if (debugEnabled) {
			Log.d(TAG, "onBindViewHolder(" + String.valueOf(position) + ")");
		}
		//已经排列好的一行数据
		RowInfo rowInfo = itemsPerRow.get(position);
		FyLog.i(TAG, "RowInfo  h="+rowInfo.getRowHeight() + " getSpaceLeft="+ rowInfo.getSpaceLeft()+ " size="+ rowInfo.getItems().size());
		if (rowInfo == null) {
			return;
		}

		List<RowItem> rowItems = new ArrayList<>(rowInfo.getItems());
		LinearLayout layout = initializeLayout(holder.itemView());
		// Index to control the current position of the current column in this row
		int columnIndex = 0;
		// Index to control the current position in the array of all the items available for this row
		int currentIndex = 0;
		float spaceLeftInArea = rowInfo.getSpaceLeft();
		int spaceLeftInColumn = listView.getNumColumns();	//总列数
		float spaceLeftInRow = rowInfo.getRowHeight();   //一行的单元高数
		boolean isClumFinish = false;
		List<String> rowSign = new ArrayList<>();
		while (!rowItems.isEmpty() && columnIndex < listView.getNumColumns()) {
			RowItem currentItem = rowItems.get(currentIndex);
			
			if (spaceLeftInColumn == 0) {
				// No more space in this column. Move to next one
				currentIndex = 0;
				spaceLeftInRow = rowInfo.getRowHeight();
				spaceLeftInColumn = listView.getNumColumns();
				continue;
			}
			
			// Is there enough space in this column to accommodate currentItem?
			float currentClum = currentItem.getItem().getColumnSpan();
			float currentRow = currentItem.getItem().getRowSpan();
			float currentArea = currentRow * currentClum;
			if (currentArea <= spaceLeftInArea) {
				rowItems.remove(currentItem);
				
				//都是对ObjectPool进行操作
				int actualIndex = currentItem.getIndex();
				int viewType = agvAdapter.getItemViewType(actualIndex);
				FyLog.d(TAG, "viewType="+ viewType+ "viewHoldersMap size is: "+ viewHoldersMap.size());
				ObjectPool<AsymmetricViewHolder<?>> pool = viewHoldersMap.get(viewType);
				if (pool == null) {
					pool = new ObjectPool<>();
					viewHoldersMap.put(viewType, pool);
				}
				AsymmetricViewHolder viewHolder = pool.get();
				if (viewHolder == null) {
					//持有AppsAdapter ListView Item
					viewHolder = agvAdapter.onCreateAsymmetricViewHolder(actualIndex, parent, viewType);
				}
				//将AppsAdapter中对Item 绑定到ViewHolder对LinearLayout中
				agvAdapter.onBindAsymmetricViewHolder(viewHolder, parent, actualIndex);
				
				View view = viewHolder.itemView;//持有AppsAdapter中对Item
				view.setTag(new ViewState(viewType, currentItem, viewHolder));
				view.setOnClickListener(this);
				view.setOnLongClickListener(this);
				
				if(!isClumFinish){
					//行没被全部填充的要进行备份
					spaceLeftInRow -= currentRow;
					FyLog.i(TAG, "spaceLeftInRow="+spaceLeftInRow);
					if(spaceLeftInRow != 0)
						rowSign.add(String.valueOf(columnIndex));
					spaceLeftInRow = rowInfo.getRowHeight();
				}
				currentIndex = 0;
				columnIndex++;
				spaceLeftInArea -= currentArea;
				
				boolean b = rowSign.contains(String.valueOf(columnIndex));
				if(isClumFinish && !b)
					columnIndex++;
				FyLog.e(TAG, "rowSign"+ b+ " rowInfo.getRowHeight()="+rowInfo.getRowHeight()+
						"  spaceLeftInColumn="+ spaceLeftInColumn+ " currentClum="+ currentClum);
				view.setLayoutParams(new LinearLayout.LayoutParams(getRowWidth(currentItem.getItem()),
						getRowHeight(currentItem.getItem())));
				LinearLayout childLayout = findOrInitializeChildLayout(layout, columnIndex);//垂直布局
				childLayout.addView(view);
				
				spaceLeftInColumn -= currentClum;
				if(spaceLeftInColumn == 0){
					spaceLeftInColumn = listView.getNumColumns();
					isClumFinish = true;
					columnIndex = -1;
				}
			} else if (currentIndex < rowItems.size() - 1) {
				// Try again with next item
				FyLog.i(TAG, "only add currentIndex="+currentIndex);
				currentIndex++;
			} else {
				rowSign = null;
				break;
			}
		}

		if (debugEnabled && position % 20 == 0) {
			Log.d(TAG, linearLayoutPool.getStats("LinearLayout"));
			for (Map.Entry<Integer, ObjectPool<AsymmetricViewHolder<?>>> e : viewHoldersMap.entrySet()) {
				Log.d(TAG, e.getValue().getStats("ConvertViewMap, viewType=" + e.getKey()));
			}
		}
	}						

该方法最开始是从itemsPerRow获取数据,那么这些数据是从哪里获取的呢?通过查找很容易发现该数据是通过异步任务类“ProcessRowsTask”获取的。那接下来我们先分析ProcessRowsTask。

@Override 
  		protected final List<RowInfo> doInBackground(Void... params) {
  			// We need a map in order to associate the item position in the wrapped adapter.
  			List<RowItem> itemsToAdd = new ArrayList<>();
  			for (int i = 0; i < agvAdapter.getActualItemCount(); i++) {
  				try {
  					//从AsymmetricGridViewAdapter获取数据源RowItem
  					itemsToAdd.add(new RowItem(i, agvAdapter.getItem(i)));
  				} catch (CursorIndexOutOfBoundsException e) {
  					Log.w(TAG, e);
  				}
  			}
  			FyLog.e(TAG, "itemsToAdd list="+ itemsToAdd.size());
  			//再封装成RowInfo返回
  			return calculateItemsPerRow(itemsToAdd);
  		}

doInbackground()方法是将AppAdapter适配器中的数据封装成RowItem列表,然后再利用calculateItemsPerRow()方法处理成按行归类的RowInfo列表。那关键就在于calculateItemsPerRow()方法,该方法主要由两个子方法组成:calculateItemsPerRow()和calculateItemsForRow(),calculateItemsPerRow()目的是利用calculateItemsForRow()获取RowInfo列表,比较简单。先说明下calculateItemsForRow()方法。

private RowInfo calculateItemsForRow(List<RowItem> items, float initialSpaceLeft) {
  			final List<RowItem> itemsThatFit = new ArrayList<>();
  			int currentItem = 0; 
  			float rowHeight = 1;
  			float areaLeft = 1 * initialSpaceLeft; //剩余单元面积数
  			float totalArea = 0;
  			//还有列没填充             还有剩余数据没有使用
  			while (areaLeft > 0 && currentItem < items.size()) {
  				final RowItem item = items.get(currentItem++);
  				//计算所占单元面积数
  				float itemArea = item.getItem().getRowSpan() * item.getItem().getColumnSpan();
  				//小于当前格行所占行数
  				FyLog.i(TAG, "rowHeight="+ rowHeight+ " item.getItem().getRowSpan()="+ item.getItem().getRowSpan());
  				if (rowHeight < item.getItem().getRowSpan()) {
  					// restart with double height 重新计算
  					FyLog.i(TAG, "clear");
  					itemsThatFit.clear();
  					rowHeight = item.getItem().getRowSpan();  //使用最高的行单元数进行计算
  					currentItem = 0;
  					areaLeft = initialSpaceLeft * item.getItem().getRowSpan(); //计算总单元面积数
  					totalArea = 0;
  				} else if (areaLeft >= itemArea) {
  					//还有地方没被占用
  					areaLeft -= itemArea;//剩余单元面积数
  					totalArea += itemArea;
  					FyLog.e(TAG, "itemArea="+ itemArea + ", areaLeft="+ areaLeft);
  					itemsThatFit.add(item);
  				} else if (!listView.isAllowReordering()) {
  					FyLog.i(TAG, "isAllowReordering");
  					break;
  				}
  			}
  			FyLog.e(TAG, "list="+ itemsThatFit.size()+ "totalarea="+ totalArea);
  			return new RowInfo(rowHeight, itemsThatFit, totalArea);
  		}

主要实现方式是计算面积,根据总单元列数和行所占单元数来计算总面积和子控件面积,然后根据面积来决定是放在第一行还是第二行。例如:假设总单元列数为3,总共有5个控件,其宽高所占比例为1*1、1*1、2*1、1*1、1*1。那么总面积应用总列数3*行最高单元数2=6,并且子控件面积为:1、1、2、1、1。那前三个控件面积和1+1+2=4 < 6,故剩下两个控件依然可以作为这一行的数据。但如果再来一个控件面积1,由于1+1+2+1+1=6,总面积已全部被占,故第六个控件只能放在第二行。

 

 

二、AsymmetricGridView使用方法

该控件的使用方式和普通的GridView一致,需要在xml中添加:

<com.felipecsl.asymmetricgridview.AsymmetricGridView
            android:id="@+id/mGridView"
            android:layout_width="fill_parent"
            android:layout_height="fill_parent"
            android:divider="@android:color/transparent"
            android:dividerHeight="@dimen/launcher_pageview_devider"
            android:fadingEdge="none"
            android:focusable="false"
            android:gravity="center"
            android:listSelector="@android:color/transparent"
            android:scrollbars="none"
            android:cacheColorHint="@android:color/transparent"
            />

在Activity中也需要为AsymmetricGridView设置适配器AppsAdapter,该适配器需要传入DemoItem的List列表。由于该方法属于不规则布局,每个方块的布局参数需要手动设定,例如:

private void initDatas() {
		// TODO Auto-generated method stub
		for (int i = 0 ; i < 11 ; i++) {
			float rowSpan = 1;
			float colSpan = 0;
			if (0 == i) {
				rowSpan = (float) 3.5;
				colSpan = 2;
			} else if (1 == i) {
				rowSpan = (float) 3.5;
				colSpan = 1;
				
			} else if (2 == i) {
				rowSpan = (float) 3.3;
				colSpan = 1;
			} else if (3 == i) {
				rowSpan = (float) 3.3;
				colSpan = 1;
			} else if (4 == i) {
				rowSpan = (float) 6.6;
				colSpan = 1;
			}else if (5 == i) {
				rowSpan = (float) 3.3;
				colSpan = 1;
			}else if (6 == i) {
				rowSpan = (float) 3.3;
				colSpan = 1;
				
			}else if (7 == i) {
				rowSpan = (float) 5;
				colSpan = 1;
			}else if (8 == i) {
				rowSpan = (float) 2.5;
				colSpan = 2;
			}else if (9 == i) {
				rowSpan = (float) 2.5;
				colSpan = 2;
			}else if(10 == i){
				rowSpan = 3;
				colSpan = 3;
			}
            //根据列,行和次序索引创建DemoItem
			DemoItem item = new DemoItem(colSpan, rowSpan, i);
			item.drawable = Utils.getStateListDrawable(colors_pressed[i],
					colors_normal[i], mContext);//方块北京颜色
			item.title = "Label"+i;//文字说明
			item.icon = getIcons("ui_Speech Recorder_icon.png");//图片
			item.titleSize = 10;//文字说明字体大小
			mItems.add(item);
		}
		mGridView.setAdapter(new AsymmetricGridViewAdapter(mContext, mGridView, mAdapter));
                mAdapter.notifyDataSetChanged();
	}

                                                                     

                                                                                      效果图如上

该控件目前应用在Launcher,首页采用这种方式布局,用户可以定义要显示的应用,该项目会在另外一篇博客上分享。稍后会上传本文的实例Demo源码

 

 

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
使用ListView实现GridView效果,其中每个子元素可以设置自己的占位,比如当前元素占几行几列(rowSpan 和columnSpan),所以看起来就像一个不规则的随机的网格布局。项目地址:https://github.com/felipecsl/AsymmetricGridView 效果图:使用说明:xml<com.felipecsl.asymmetricgridview.library.widget.AsymmetricGridView     xmlns:android="http://schemas.android.com/apk/res/android"     android:id="@ id/listView"     android:layout_width="match_parent"     android:layout_height="match_parent"/>activity中:@Override protected void onCreate(Bundle savedInstanceState) {     super.onCreate(savedInstanceState);     setContentView(R.layout.activity_main);     listView = (AsymmetricGridView) findViewById(R.id.listView);     // Choose your own preferred column width     listView.setRequestedColumnWidth(Utils.dpToPx(this, 120));     final List<AsymmetricItem> items = new ArrayList<>();     // initialize your items array     adapter = new ListAdapter(this, listView, items);     listView.setAdapter(adapter); }支持追加更多的元素:// Will append more items at the end of the adapter. listView.getAdapter().appendItems(moreItems); // resetting the adapter items. Will clear the adapter // and add the new items. listView.getAdapter().setItems(items);设置是否重新排列达到更好的显示效果:// Setting to true will move items up and down to better use the space // Defaults to false. listView.setAllowReordering(true); listView.isAllowReordering(); // true设置item的占位:item 一般这样定义:public DemoItem(final int columnSpan, final int rowSpan, int position) {     this.columnSpan = columnSpan;     this.rowSpan = rowSpan;     this.position = position; }columnSpan 和rowSpan分别代表列占位和行占位。说明:目前当item的rowSpan = 2 columnSpan = 2时可以达到最佳的状态。这个后续会继续改进。item的大小越统一,效率越高,特殊大小的元素少于20%是比较理想的状态。不然没法在不预留很多空位的情况下,合理的显示。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值