让你直呼666的仿Excel表格效果
今日科技快讯
据CNBC报道,网络安全记者发布报告声称,Facebook在未加密的情况下存储了多达6亿个用户账户密码,并以明文形式存储,公司数万名员工可以访问。在Facebook的27亿用户中,6亿用户已经占了相当大的比例。该公司周四表示,计划开始通知受到影响的用户,以便他们更改密码。
作者简介
周一上午好,新的一周在春意盎然中继续加油吧!
本篇文章来自 至死仍是少年心 的投稿,和大家分享了如何在 Android中快速实现一个Excel表格,希望对大家有所帮助!
至死仍是少年心的博客地址:
https://www.jianshu.com/u/823f9ad8aa8f
前言
最近公司做的OA项目有会议室预订功能,设计图是这个样子的:
看到设计图的时候就想到了要做成左右、上下滑动的样子,因为以前朋友做过一个抢位置app也是这种效果,当时感觉很炫。不过他是用的是github上的一个开源项目,虽然那个效果比较好,但美中不足的是它横向滑动的时候比较卡顿,而且代码相对来说比较复杂,改造成完全适应自身项目所需要花费的时间就比较多,对于赶项目的程序员来说,没什么是花费时间少且完美运行的完成任务更有吸引力了,所以后面我就采用了原生布局嵌套的方式快速的实现了功能。
先不说其他的,摆上最终效果图:
这里先讲解下业务逻辑,头部的横轴是时间段(0:00-23:59),纵轴是会议室,内容区域展示的是被预订的会议室,上下滑动内容区域的时候会议室列表要联合滚动,左右滑动内容区域的时候头部的时间轴也要联合滚动,滚动会议室和时间轴的时候内容区域也要相应的滚动。由于预订的时间可能是 8:10-8:50或者是9:10-10:30这种的,所以这个地方我们还要考虑不是零起点、跨区域等情况,而不是单纯的填充背景色。
布局嵌套思维图
根据布局思维图可以很清晰的看到:
时间轴的外层是套了个横向的scrollview,里面是一个linearLayout布局,布局xml代码代码:
纵轴会议室简单,就是一个listview。
内容区域由于需要刷新,所以在最外层加了个swipeRefresh,然后在内部一次加上横向的scrollview和listview,布局xml代码如下:
把三个主要布局画出来后就需要在页面中设置数据了:
这里有两个listview,所以需要两个适配器,而两个listview需要联动。当然,两个横向的scrollview也需要联动,两个滑动监听代码如下:
/**
* HorizontalScrollView的滑动监听(水平方向同步控制)
*/
private class HorizontalScrollListener implements MyHorizontalScrollView.OnHorizontalScrollListener {
public void onHorizontalScrolled(MyHorizontalScrollView view, int l, int t, int oldl, int oldt) {
if (view == mhscContent) {
mhscRow.scrollTo(l, t);
} else {
mhscContent.scrollTo(l, t);
}
}
}
/**
* 两个ListView的滑动监听(垂直方向同步控制)
*/
private class VerticalScrollListener implements AbsListView.OnScrollListener {
int scrollState;
public void onScrollStateChanged(AbsListView view, int scrollState) {
this.scrollState = scrollState;
if (scrollState == SCROLL_STATE_IDLE || scrollState == SCROLL_STATE_TOUCH_SCROLL) {
View subView = view.getChildAt(0);
if (subView != null && view == lvContent) {
int top = subView.getTop();
int position = view.getFirstVisiblePosition();
lvColumn.setSelectionFromTop(position, top);
} else if (subView != null && view == lvColumn) {
int top = subView.getTop();
int position = view.getFirstVisiblePosition();
lvContent.setSelectionFromTop(position, top);
}
}
// 滑动事件冲突的解决:如果ListView的首条item的position != 0,即此时不再顶上,则将下拉刷新禁用
// if (swipeRefreshEnable) {
if (view.getFirstVisiblePosition() != 0 && srlTableContent.isEnabled()) {
srlTableContent.setEnabled(false);
}
if (view.getFirstVisiblePosition() == 0) {
srlTableContent.setEnabled(true);
}
// }
}
public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
//判断滑动是否终止,以停止自动对齐,否则该方法会一直被调用,影响性能
if (scrollState == SCROLL_STATE_IDLE) {
return;
}
View subView = view.getChildAt(0);
if (subView != null && view == lvContent) {
int top = subView.getTop();
lvColumn.setSelectionFromTop(firstVisibleItem, top);
} else if (subView != null && view == lvColumn) {
int top = subView.getTop();
lvContent.setSelectionFromTop(firstVisibleItem, top);
}
}
}
这里就不摆出listview设置数据的代码了,应该都会。
设置横向时间轴
设置横向时间轴的代码如下:
private void initRowLayou() {
topItmeWidth = AppDensityUtil.dip2px(this, 100);
topItmeHeight = AppDensityUtil.dip2px(this, 35);
List<String> rowDataList = getRowDataList();
for (int i = 0; i < rowDataList.size(); i++) {
RelativeLayout inflate = (RelativeLayout) LayoutInflater.from(this).inflate(R.layout.item_table_row_view, null);
final TextView tvRowTxt = (TextView) inflate.findViewById(R.id.tv_row_txt);
tvRowTxt.setText(rowDataList.get(i));
tvRowTxt.setTag(i);
LinearLayout.LayoutParams layoutParams = new LinearLayout.LayoutParams(topItmeWidth, topItmeHeight);
// tvRowTxt.setWidth(topItmeWidth);//设置宽度
// tvRowTxt.setHeight(topItmeHeight);//设置宽度
tvRowTxt.getPaint().setFakeBoldText(true);
tvRowTxt.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
ToastUtil.showMessage(tvRowTxt.getTag() + "");
}
});
llRowContent.addView(inflate, layoutParams);
}
}
意思很简单,根据集合遍历,然后动态new出布局。
一开始在上面也说了预订的时间可能是 8:10-8:50或者是9:10-10:30这种的,所以这个地方我们还要考虑不是零起点、跨区域等情况。因为原生的progressbar必须从头开始绘制,不满足要求,所以我们需要自定义控件才能满足要求,那就来个简单的自定义控件,代码如下:
/**
* 名称:范围进度条 没做动画效果
* 创建人:邹安富
* 创建时间:2018/3/2
* 详细说明:
*/
public class MyProgress extends View {
private Paint backPaint;
private Paint fillPaint;
private int backColor;
private int fillColor;
private float layout_width;
private float layout_height;
private int maxProgress;
private int startProgress;
private int endProgress;
public MyProgress(Context context) {
super(context);
}
public MyProgress(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
init(context, attrs);
}
public MyProgress(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context, attrs);
}
public void setStartProgress(int startProgress) {
this.startProgress = startProgress;
}
public void setEndProgress(int endProgress) {
this.endProgress = endProgress;
}
public void setMaxProgress(int maxProgress) {
this.maxProgress = maxProgress;
}
public void setStardEndProgress(int start, int endProgress) {
this.startProgress = start;
this.endProgress = endProgress;
invalidate();
}
public void clearProgress() {
this.startProgress = 0;
this.endProgress = 0;
invalidate();
}
private void init(Context context, AttributeSet attrs) {
TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.MyProgress);
layout_height = a.getDimension(R.styleable.MyProgress_mpLayoutHeight, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 30, context.getResources().getDisplayMetrics()));
layout_width = a.getDimension(R.styleable.MyProgress_mpLayoutWidth, TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 60, context.getResources().getDisplayMetrics()));
maxProgress = a.getInteger(R.styleable.MyProgress_mpMaxValues, 100);
startProgress = a.getInteger(R.styleable.MyProgress_mpStartValues, 0);
endProgress = a.getInteger(R.styleable.MyProgress_mpEndValues, 0);
backColor = a.getColor(R.styleable.MyProgress_mpBackGroundColor, getResources().getColor(R.color.white));
fillColor = a.getColor(R.styleable.MyProgress_mpFillGroundColor, getResources().getColor(R.color.red));
backPaint = new Paint();
backPaint.setAntiAlias(true);
backPaint.setColor(backColor);
backPaint.setDither(true);//防抖动
backPaint.setStyle(Paint.Style.FILL);
fillPaint = new Paint();
fillPaint.setAntiAlias(true);
fillPaint.setColor(fillColor);
fillPaint.setDither(true);
fillPaint.setStyle(Paint.Style.FILL);
}
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int measureView_width = measureView(widthMode, widthSize, layout_width);
int measureView_height = measureView(heightMode, heightSize, layout_height);
setMeasuredDimension(measureView_width, measureView_height);
}
private int measureView(int mode, int size, float defaultSize) {
switch (mode) {
case MeasureSpec.UNSPECIFIED:
case MeasureSpec.AT_MOST:
if (size > defaultSize) {
size = (int) Math.ceil(defaultSize);
}
return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
case MeasureSpec.EXACTLY:
return MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
}
return -1;
}
private static final String TAG = "MyProgress";
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.drawRect(0, 0, layout_width, layout_height, backPaint);
double startReta = (double) startProgress / maxProgress;
double endReta = (double) endProgress / maxProgress;
float fillStartWidth = (float) (startReta * layout_width);
float fillEndWidth = (float) (endReta * layout_width);
Log.i(TAG, "onDraw: startProgress==" + startProgress);
Log.i(TAG, "onDraw: endProgress==" + endProgress);
Log.i(TAG, "onDraw: maxProgress==" + maxProgress);
Log.i(TAG, "onDraw: endReta==" + endReta);
Log.i(TAG, "onDraw: fillStartWidth==" + fillStartWidth);
Log.i(TAG, "onDraw: fillEndWidth==" + fillEndWidth);
canvas.drawRect(fillStartWidth, 0, fillEndWidth, layout_height, fillPaint);
}
这个自定义控件很简单,就是传入起始和结束的位置。然后和最大值比较,得到比率。最后根据比率算出应该绘制的长度和区域。这个view写好后还需要封装一下,RowItmeViewLayout就是对改view的封装,封装的具体内容有: 动态增加字内容、清除所有的进度展示、根据传入预订时间范围设置进度条,代码如下:
/**
* 名称:
* 创建人:邹安富
* 创建时间:2018/3/5
* 详细说明:
*/
public class RowItmeViewLayout extends LinearLayout {
private static final String TAG = "RowItmeViewLayout";
private List<MyProgress> myProgressList = new ArrayList<>();
public RowItmeViewLayout(Context context) {
super(context);
}
public RowItmeViewLayout(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initView(context, attrs);
}
public RowItmeViewLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initView(context, attrs);
}
private void initView(Context context, AttributeSet attrs) {
this.setOrientation(HORIZONTAL);
//整天没有预订
for (int i = 0; i < 24; i++) {
View inflate = LayoutInflater.from(context).inflate(R.layout.include_item_yd_row, null);
MyProgress myProgress = (MyProgress) inflate.findViewById(R.id.mp_progress);
myProgress.setMaxProgress(60);
myProgress.setStardEndProgress(0, 0);
this.addView(inflate);
// myProgress.setOnClickListener(new View.OnClickListener() {
// @Override
// public void onClick(View v) {
// ToastUtil.showMessage("没有预订");
// }
// });
myProgressList.add(myProgress);
}
}
/**
* 清楚所有的进度展示
*/
public void clearAllProgress() {
for (int i = 0; i < myProgressList.size(); i++) {
myProgressList.get(i).clearProgress();
}
}
/**
* 设置进度条
*/
public void setProgress(String progressStr) {
// String str = "09:10-10:20";
try {
String[] split = progressStr.split("-");
String startHour = split[0].split(":")[0];
String startMinute = split[0].split(":")[1];
String endHour = split[1].split(":")[0];
String endMinute = split[1].split(":")[1];
if (TextUtils.equals(startHour, endHour)) {
//相等
String index;
if (startHour.startsWith("0") && !startHour.endsWith("0")) {
index = startHour.replace("0", "");
} else {
index = startHour;
}
MyProgress myProgressStart = myProgressList.get(Integer.parseInt(index));
myProgressStart.setStardEndProgress(Integer.parseInt(startMinute), Integer.parseInt(endMinute));
} else {
//不相等
int indexStart;
int indexEnd;
if (startHour.startsWith("0") && !startHour.endsWith("0")) {
indexStart = Integer.parseInt(startHour.replace("0", ""));
} else {
indexStart = Integer.parseInt(startHour);
}
if (endHour.startsWith("0")) {
indexEnd = Integer.parseInt(endHour.replace("0", ""));
} else {
indexEnd = Integer.parseInt(endHour);
}
for (int i = indexStart; i <= indexEnd; i++) {
MyProgress myProgressStart = myProgressList.get(i);
myProgressStart.setStardEndProgress(0, 60);
if (i == indexStart) {
myProgressStart.setStardEndProgress(Integer.parseInt(startMinute), 60);
}
if (i == indexEnd) {
myProgressStart.setStardEndProgress(0, Integer.parseInt(endMinute));
}
}
}
} catch (Exception e) {
Log.e(TAG, "setProgress: e==" + e.toString());
}
}
}
把RowItmeViewLayout封装好以后就可以在适配器item布局中使用啦,xml布局代码是这样的:
适配器中布局使用
看下在适配器中布局使用:
/**
* 名称:主内容适配器
* 创建人:邹安富
* 创建时间:2018/3/1
* 详细说明:
*/
public class TableContentAdapter extends LvSuperBaseAdapter<TableContentBean> {
private static final String TAG = "TableContentAdapter";
public TableContentAdapter(Context context, List<TableContentBean> mDatas) {
super(context, mDatas);
}
public int getLayoutItemId() {
return R.layout.item_table_content_view;
}
public LvBaseViewHolder getViewHolder(View itemView) {
return new ViewHolder(itemView);
}
public void setDatas(LvBaseViewHolder holder, TableContentBean itemBean, int position) {
final ViewHolder viewHolder = (ViewHolder) holder;
int childCount = viewHolder.llTableContent.getChildCount();
Log.e(TAG, "setDatas: childCount===" + childCount);
List<TableContentBean.HourData> hourData = itemBean.getHourData();
viewHolder.rowlContent.clearAllProgress();
if (itemBean.getYdStatc() == 1) {
for (int i = 0; i < hourData.size(); i++) {
String ydTimeSlot = hourData.get(i).getYdTimeSlot();
if (!TextUtils.isEmpty(ydTimeSlot)) {
viewHolder.rowlContent.setProgress(ydTimeSlot);
}
}
} else {
viewHolder.rowlContent.clearAllProgress();
}
}
static class ViewHolder extends LvBaseViewHolder {
(R.id.rowl_content)
RowItmeViewLayout rowlContent;
(R.id.ll_table_content)
CheckableLinearLayout llTableContent;
ViewHolder(View view) {
super(view);
ButterKnife.bind(this, view);
}
}
}
总结
注意事项:
1. MyProgress要封装起来,不然在listview上下滑动的时候会巨卡。
2. 主内容区域往左滑动到最后面有多余的空白条,在被嵌套的listview中需要加上: android:scrollbars="none"这个消除空白条。
3. 嵌套的listview没有用recyclerview替换,理论上是没问题的,可能处理滑动和高度的地方需要注意下,有兴趣的童鞋可以试试。
思路参考地址:
https://www.cnblogs.com/begin1949/p/5910785.html
其它方式的实现:
https://github.com/Kelin-Hong/ScrollablePanel
文章中Github地址:
https://github.com/zouanfu/AndroidExcel
推荐阅读:
欢迎关注我的公众号,学习技术或投稿
长按上图,识别图中二维码即可关注