前言
写完了
android
图表,一个朋友说他们公司需要做表格。问我能做吗?我答这有啥不能做。我就开始几个吧唧吧唧写,快写完了,朋友说表格在android
体验不好。坑壁啊,最好放在github
上。通过这次我对自定义有点体会,就想这篇文章。哈哈。文笔不好,将就的看吧。
github地址:https://github.com/huangyanbin/smartTable
效果
俗话说无图无真相,先上图:
支持图片以及
Text drawpadding
四个方向
分页模式
网络数据注解模式
功能
- 快速配置自动生成表格;
- 自动计算表格宽高;
- 表格列标题组合;
- 可配置是否固定左序列、顶部序列、第一行、列标题、统计行;
- 自动统计,排序(也可自定义统计规则);
- 表格图文、序列号、列标题格式化;
- 表格各属性背景、文字、网格、
padding
等配置; - 表格批注;
- 表格内容、列标题点击事件;
- 缩放模式(待优化)和滚动模式;
- 支持注解模式;
- 支持数据分页;
- 支持每个格子字体大小,颜色,背景设置。
分析
一看就知道需要是自定义
View
。首先一看表格。表格标题,顶部序号,左侧序号,列标题,统计行,表格内容。分析完了表格属性。但是我们要自动生成表格,表格数据一般都是一个List<Data>
,马上就想到了注解。对的,每列对应的Data
一个成员变量。开始动手了!
frist
首先我们自定义一个
View
SmartTable
,里面分别有表格标题,顶部序号,左侧序号,表格内容。等等,咋没有统计行,统计行我放在表格内容里面了。里面泛型T是啥鬼,答曰表格数据类型。
public class SmartTable<T> extends View{
//标题
private ITableTitle tableTitle;
//顶部序号
private XSequence<T> xAxis;
//左侧序号
private YSequence<T> yAxis;
// 表格内容
private TableProvider<T> provider;
two
我们要用注解来解析数据,所以我们要定义注解。当然,我们也要支持普通模式。我们这里先做注解。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SmartTable {
String name() default "";
boolean count() default false;
int pageSize() default Integer.MAX_VALUE;
int currentPage() default 0;
}
为啥要写这么多属性,解释一下:
name
代表表格标题,count
是打开统计,pageSize
和currentPage
是用于分页的。放在类名上。再就是需要显示的列数据注解。
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
public @interface SmartColumn {
/**
* 名称
* @return
*/
String name() default "";
/**
* id 越小越在前面
* @return
*/
int id() default 0;
String parent() default "";
/**
* 设置是否查询下一级
* @return
*/
ColumnType type() default ColumnType.Own;
boolean autoCount() default false;
}
public enum ColumnType {
Own,Child;
}
为啥需要设置
Id
。因为表格列位置可以排序,列排序是根据Id
大小来排的。
ColumnType
是啥鬼,比如数据User
对象有个Father
属性,Father
有name
.我们可能需要显示father.name
。我们就可以用ColumnType.child
就会进去这个对象里面递归的查询是否有smartColumn
注解。
autoCount
是否开启统计,因为大部分列是不需要统计的。
parent
:表格经常几列为一大列,parent
就是指定父列名称。我们开始来解析注解,首先解析
SmartTable
注解,将注解属性放入TableData
,然后获取Class
的Fields
,迭代去获取列信息:
public class AnnotationParser<T> {
//获取解析之后表格数据
public PageTableData<T> parse(List<T> dataList){
if(dataList!= null && dataList.size() >0) {
T firstData = dataList.get(0);
if(firstData != null) {
Class clazz = firstData.getClass();
Annotation tableAnnotation = clazz.getAnnotation(SmartTable.class);
if(tableAnnotation != null){
//将注解的SmartTable的属性放入TableData保存
SmartTable table = (SmartTable) tableAnnotation;
List<Column> columns = new ArrayList<>();
PageTableData<T> tableData = new PageTableData<>(table.name(),dataList,columns);
tableData.setCurrentPage(table.currentPage());
tableData.setPageSize(table.pageSize());
tableData.setShowCount(table.count());
FieldGenericHandler genericHandler = new FieldGenericHandler();
Map<String,Column> parentMap = new HashMap<>();
getColumnAnnotation(clazz, null,columns, genericHandler, parentMap);
//根据ID排序列
Collections.sort(columns);
return tableData;
}
}
}
return null;
接下来就是递归去解析
SmartColumn
注解。
//递归去获取SmartColumn注解
private void getColumnAnnotation(Class clazz, String parentFieldName, List<Column> columns, FieldGenericHandler genericHandler, Map<String, Column> parentMap) {
Field[] fields = clazz.getDeclaredFields();
//迭代Field
for(Field field:fields){
field.setAccessible(true);
//获取属性的类型
Class<?> fieldClass = field.getType();
Annotation fieldAnnotation = field.getAnnotation(SmartColumn.class);
if(fieldAnnotation != null){
SmartColumn smartColumn = (SmartColumn) fieldAnnotation;
ColumnType type = smartColumn.type();
if(type == ColumnType.Own) {
//将SmartColumn属性放入Column对象
String name = smartColumn.name();
int id = smartColumn.id();
String parent = smartColumn.parent();
boolean isAutoCount = smartColumn.autoCount();
if (name.equals("")) {
name = field.getName();
}
String fieldName =parentFieldName != null? (parentFieldName+field.getName()) :field.getName();
//生成列信息
Column<?> column = genericHandler.getGenericColumn(fieldClass, name, fieldName);
column.setId(id);
column.setAutoCount(isAutoCount);
if (!parent.equals("")) {
//如果父列有的话,就直接使用
Column parentColumn = parentMap.get(parent);
if (parentColumn == null) {
//创建父列
List<Column> childColumns = new ArrayList<>();
childColumns.add(column);
parentColumn = new Column(parent, childColumns);
parentColumn.setId(id);
columns.add(parentColumn);
parentMap.put(parent, parentColumn);
}
//添加子列
parentColumn.addChildren(column);
if (id < parentColumn.getId()) {
parentColumn.setId(id);
}
}else{
//添加到columns
columns.add(column);
}
}else{
//因为是下层,所有用.连接起来 比如:father.name
String fieldName = (parentFieldName != null ?parentFieldName:"")
+field.getName()+".";
//递归去获取下层
getColumnAnnotation(fieldClass,fieldName,columns,genericHandler,parentMap);
}
}
}
}
}
将
List<Data>
转换成每列数据.每列都需要去通过反射获取真实的值。然后保存到Column List<T> datas
里面.这里只是解析数据部分
/**
* 递归解析获取数据
*
*/
public T getData(Object o) throws NoSuchFieldException, IllegalAccessException {
Class clazz = o.getClass();
//解析
String[] fieldNames = fieldName.split("\\.");
String firstFieldName = fieldNames.length == 0 ? fieldName : fieldNames[0];
Field field = clazz.getDeclaredField(firstFieldName);
if (field != null) {
Object child = o;
if (fieldNames.length == 0 || fieldNames.length == 1) {
return getFieldValue(field, o);
}
for (int i = 0; i < fieldNames.length; i++) {
if (child == null) {
return null;
}
Class childClazz = child.getClass();
Field childField = childClazz.getDeclaredField(fieldNames[i]);
if (childField == null) {
return null;
}
if (i == fieldNames.length - 1) {
return getFieldValue(childField, child);
} else {
field.setAccessible(true);
child = field.get(child);
}
}
}
return null;
}
获取完成数据之后,发现每列的宽是由最长哪个决定的,每列是宽是由行高决定的。突然发现这个无语。我们需要算出每行的宽和高。在计算表格的宽高时,我们把想要的每行宽和高保存下来,这里时计算高。高= 顶部序列号高+ 每行的高…+统计行高
/**
* 计算table高度
* @param tableData
* @param config
* @return
*/
private int getTableHeight(TableData<T> tableData,TableConfig config){
Paint paint = config.getPaint();
int topHeight = 0;
//是否显示顶部序列号
if(config.isShowXSequence()) {
//计算顶部序列号高 加上设置的cell上下左右padding
topHeight = DrawUtils.getTextHeight(config.getXSequenceStyle(), paint)
+ 2 * config.getVerticalPadding();
}
int titleHeight = tableData.getTitleDrawFormat().measureHeight(config);
TableInfo tableInfo = tableData.getTableInfo();
tableInfo.setTitleHeight(titleHeight);
tableInfo.setTopHeight(topHeight);
int totalContentHeight = 0;
//把之前保存每行的高拿出来相加
for(int height :tableInfo.getLineHeightArray()){
totalContentHeight+=height;
}
int totalTitleHeight = titleHeight*tableInfo.getMaxLevel();
int totalHeight = topHeight +totalTitleHeight+totalContentHeight;
//计算底部统计行的高
if(tableData.isShowCount()) {
int countHeight = DrawUtils.getTextHeight(config.getCountStyle(), paint)
+ 2 * config.getVerticalPadding();
tableInfo.setCountHeight(countHeight);
totalHeight+=countHeight;
}
return totalHeight;
}
好像没看见咋计算行高的啊?在解析数据时,就开始在计算行高了。那
drawFormat
是什么鬼?因为每列有些需要显示图片,有的需要显示文字,需求不同,所以定义了这个接口,提供每列的格式化。宽高也就可以获取到了,把canvas
和paint
都交出来了。有了这个一切都可以实现。
/**
* 设置每行的高度
* 以及计算总数
*
* @param config 配置
* @param lineHeightArray 储存高度数组
* @param position 位置
*/
private void setRowHeight(TableConfig config, int[] lineHeightArray, int position,T t) {
if(t != null && isAutoCount && countFormat ==null){
if(LetterUtils.isBasicType(t)){
if(LetterUtils.isNumber(this)) {
countFormat = new NumberCountFormat<>();
}else{
countFormat = new DecimalCountFormat<>();
}
}else{
countFormat = new StringCountFormat<>(this);
}
}
if(countFormat != null){
countFormat.count(t);
}
//看这里 比较行高
int height = drawFormat.measureHeight(this, position, config);
if (height > lineHeightArray[position]) {
lineHeightArray[position] = height;
}
}
/**
* 绘制格式化
*/
public interface IDrawFormat<T> {
/**
*测量宽
*/
int measureWidth(Column<T> column, TableConfig config);
/**
*测量高
*/
int measureHeight(Column<T> column,int position, TableConfig config);
//绘制
void draw(Canvas c,Column<T> column,T t,String value,int left,int top,int right,int bottom,int position,TableConfig config);
//绘制背景
boolean drawBackground(Canvas c, CellInfo<T> cellInfo, int left, int top, int right, int bottom, TableConfig config);
}
我还定义了序号格式化,
ISequenceFormat
用于格式化序号,ITitleDrawFormat
用于计算列标题高宽以及绘制,ICountFormat
自定义列的统计规则(比如你可以定义如果是value
是”美女”就加一),IBackgroundFormat
自定义背景和字体。这是整个SmartTable
强大之处。