查看原文
其他

让你直呼666的仿Excel表格效果

至死仍是少年心 郭霖 2019-04-29



今日科技快讯


据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(this100);
        topItmeHeight = AppDensityUtil.dip2px(this35);
        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(00, 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(00);
            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(060);
                    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妙用,统一化自动依赖管理


欢迎关注我的公众号,学习技术或投稿

长按上图,识别图中二维码即可关注

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存