让你直呼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 { @Override 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;
@Override 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); }// } }
@Override 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() { @Override 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); }
@Override 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";
@Override 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); }
@Override public int getLayoutItemId() { return R.layout.item_table_content_view; }
@Override public LvBaseViewHolder getViewHolder(View itemView) { return new ViewHolder(itemView); }
@Override 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 { @BindView(R.id.rowl_content) RowItmeViewLayout rowlContent; @BindView(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
推荐阅读:
Kotlin的特性应用示例,原来还可以这么玩
分享一个我开发的MVVM架构的开源小项目
Gradle妙用,统一化自动依赖管理
欢迎关注我的公众号,学习技术或投稿
长按上图,识别图中二维码即可关注
让你直呼666的仿Excel表格效果相关推荐
- Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666
Spring MVC和Spring Boot有什么区别? 这样答,面试官直呼666 作为初级程序员,这样的问题在面试中,也被问到过,随着越来越了解,发现以前自己答的真水. 一般的回答 先来说说我以 ...
- Python仅用3行代码就能输出花式字符串图集,同事直呼666!
Python用3行代码输出花式字符串图集,同事直呼666! 高逼格的日志 springboot 相信Java程序员看到上面的图,一定不会陌生.没错,springboot的启动日志.不知道其他人怎么想, ...
- html 仿excel,智表-浏览器端仿EXCEL表格jQuery插件
智表(ZCELL)是一款浏览器端仿EXCEL表格jQuery插件.智表可以为你提供EXCEL般的智能体验,并带有灵活的单元格选中与实时计算功能,强大的复制与粘贴功能,标准化数据加载与获取,以及灵活的外 ...
- Qt高仿Excel表格组件-支持冻结列、冻结行、内容自适应和合并单元格
目录 一.概述 二.效果展示 三.实现思路 1.冻结行.冻结列 2.行高自适应 3.蚂蚁线 四.测试代码 1.添加表格数据 2.设置冻结行.列 3.行高.列宽 4.单元格背景色 5.单元格文字 6.其 ...
- HTML Table 固定列宽,实现excel表格效果
(1)获取行号 <table> <tr onmouseover ="showIndex(this)"><td>1</td></ ...
- 前端仿Excel表格
可持久化存储表格信息和样式 <!DOCTYPE html> <html lang="en"><head><meta charset=&qu ...
- 这样规范写代码,同事直呼“666”
点击上方"方志朋",选择"设为星标" 回复"666"获取新整理的面试资料 来源:http://i7q.cn/5iDTto 一.MyBatis ...
- 这样设计订单系统,同事直呼 666!
来源:人人都是产品经理 | http://dwz.win/Wkh 本文主要讲述了在传统电商企业中,订单系统应承载的角色,就订单系统所包含的主要功能模块梳理了设计思路,并对订单系统未来的发展做了一些思考 ...
- Spring 事务失效的 8 大场景,面试官直呼666...
前几天发了一篇文章里面有一个关于事务失效的问题: 用 Spring 的 @Transactional 注解控制事务有哪些不生效的场景? 其中有个热心粉丝留言分享了下,我觉得总结得有点经验,给置顶了: ...
最新文章
- POJ2153 (C++ map)
- TCP/IP协议的一个具体实现Socket
- 2019.08.04 新建随笔
- 合并表格,并实现对datatable的group by 功能
- final类是否可以被代理_设计模式——代理模式
- 使用X Manager远程CentOS 7服务器(XDMCP)
- Go unsafe Pointer
- key_t IPC键和ftok函数详解和剖析
- 从入门到放弃之大数据Hive
- java web redis_java web网页版redis客户端工具
- Android仿苹果版QQ下拉刷新实现(二) ——贝塞尔曲线开发鼻涕下拉粘连效果
- tornado SQLAlchemy
- dwf怎么合成一个_cad多张图纸拆分(如何将一个有多个图的CAD文件,按图分成几个文件)...
- 5G的调制方式,到底是怎么实现的?
- 牛顿与莱布尔茨的微积分战争
- vss 迁入后,服务器上面的文件没有变化,VSS迁移
- 条件运算符的嵌套使用
- 内存错误分析工具----asan(AddressSanitizer)的介绍和使用
- Caused by: org.dom4j.DocumentException异常信息记录
- 面试之满帮与达达京东到家(未完全解析)
热门文章
- 查找我的iphone怎么用详细教程
- 嵌入式Linux系统libevent异步事件库的移植
- java教育机构管理计算机毕业设计MyBatis+系统+LW文档+源码+调试部署
- Opencv 三对角线矩阵(Tridiagonal Matrix)解法之(Thomas Algorithm)
- .bat 批处理文件实现微信多开
- 使用windows身份验证登录sqlserver问题记录
- 【高数】高数第一章节——极限无穷连续与间断
- 中科新媒体:实体零售企业如何在数字化浪潮下创新营销
- python的语句有哪些_python基本语句有哪些
- 弘辽科技:商家不要错过的淘宝直播妙招!