最近一直在写混合开发的东西,是时候温故下native的了。
一年多之前领导提了一个双性滚动+快点击的”TableView”那时候自己整了2 3天没整出来,本来想今天”圆梦”,但是发现轮子已经有了,但是少了一些小功能和足够多的解释,那就把这个轮子fork下来自己改!(我们不生产代码,我们只是代码的搬运工)
源码地址:https://github.com/ddwhan0123/ScrollTableView
按照习惯,安利下:https://github.com/ddwhan0123/Useful-Open-Source-Android (今天把search view也划分了出去)
效果图:
可以滚动,可以点击,基本满足了之前的要求
包结构:东西也不是很多,copy也不复杂
本文会详细的”拆”这个库
如何引用:
<com.loopeer.android.librarys.scrolltable.ScrollTableView android:id="@+id/scroll_table_view" android:layout_width="match_parent" android:layout_height="wrap_content" />
正常控件用就行了,没什么特别的姿势,这边讲下有哪些自定义标签
都在ScrollTable/src/main/res/values/attrs.xml里
dataMargin 数据间距 类型:dimension itemHeight 每个块的高度 类型:dimension itemWidth 每个块的宽度 类型:dimension topPlaceHeight 头部的高度 类型:dimension itemIndicatorCircleRadius 小圆圈的半径 类型:dimension itemIndicatorLineWidth 分割线的宽度 类型:dimension textTitleSize 标题大小 类型:dimension textLeftTitleColor 左边标题颜色 类型:dimension textTopTitleColor 上边标题颜色 类型:dimension 等等等。。。(一排可以自己看,不难理解)
组成
很明显,这是一个试图组,这边拆一下,给大家解释下
ScrollTable/src/main/res/layout/view_container.xml里
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <com.loopeer.android.librarys.scrolltable.view.IHorizontalScrollView android:id="@+id/scroll_header_horizontal" android:layout_width="match_parent" android:layout_height="@dimen/table_top_title_height" android:layout_marginLeft="@dimen/table_left_title_width" > <com.loopeer.android.librarys.scrolltable.view.TopTitleView android:id="@+id/header_horizontal" android:layout_width="wrap_content" android:layout_height="match_parent"/> </com.loopeer.android.librarys.scrolltable.view.IHorizontalScrollView> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="1" android:orientation="horizontal"> <com.loopeer.android.librarys.scrolltable.view.VerticalScrollView android:id="@+id/scroll_vertical" android:layout_width="wrap_content" android:layout_height="wrap_content"> <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="horizontal"> <com.loopeer.android.librarys.scrolltable.view.LeftTitleView android:id="@+id/header_vertical" android:layout_width="@dimen/table_left_title_width" android:layout_height="wrap_content" android:layout_marginBottom="24dp"/> <com.loopeer.android.librarys.scrolltable.view.IHorizontalScrollView android:id="@+id/scroll_horizontal" android:layout_width="match_parent" android:layout_height="match_parent"> <com.loopeer.android.librarys.scrolltable.view.CustomTableView android:layout_marginTop="@dimen/table_default_margin_top" android:id="@+id/content_view" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </com.loopeer.android.librarys.scrolltable.view.IHorizontalScrollView> </LinearLayout> </com.loopeer.android.librarys.scrolltable.view.VerticalScrollView> </LinearLayout> </LinearLayout>
一个水平滚动试图里包着一个TopTitle的view
一个垂直滚动的视图包着一个LeftTitle的view
中间是一个自己绘画的CustomView
横向滚动试图IHorizontalScrollView
public class IHorizontalScrollView extends HorizontalScrollView implements IScroller { private IScroller scroller; public IHorizontalScrollView(Context context) { this(context, null); } public IHorizontalScrollView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public IHorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); } @Override protected void onScrollChanged(int l, int t, int oldl, int oldt) { super.onScrollChanged(l, t, oldl, oldt); if (scroller != null) { scroller.onScrollXY(l, t); } } public void setIScroller(IScroller scroller) { this.scroller = scroller; } @Override public void onScrollXY(int offsetX, int offsetY) { scrollTo(offsetX, offsetY); } }
继承于HorizontalScrollView ,用于给titleview添加同步滚动的操作,让他们看上去是一体的
VerticalScrollView也是类似效果只不过一个横向一个纵向而已
横向的TopTitleView
因为有很多初始化的“没营养代码”,所以我只贴重要步骤
TopTitleView extends View
它是一个自定义的View
尺寸:
宽度=间距+字宽
高度=定义的高
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); int sizeHeight = MeasureSpec.getSize(heightMeasureSpec); mItemHeight = sizeHeight; setMeasuredDimension(column * mItemWidth + (column - 1) * mItemMargin, mItemHeight); }
绘画:
循环便利数据源,因为仅为单行多列,所以一个循环搞定,画字,算间距即可
private void drawItem(Canvas canvas) { for (int columnIndex = 0; columnIndex < column; columnIndex++) { String content = titles.get(columnIndex); Paint.FontMetrics fontMetrics = mPaintTextNormal.getFontMetrics(); float fontHeight = fontMetrics.bottom - fontMetrics.top; float textWidth = mPaintTextNormal.measureText(content); float y = mItemPlaceHeight - (mItemPlaceHeight - fontHeight) / 2 - fontMetrics.bottom; float x = (mItemMargin + mItemWidth) * columnIndex + mItemWidth / 2 - textWidth / 2; canvas.drawText(content, x, y, mPaintTextNormal); } }
LeftTitleView 左侧标题
也是一个自定义view只不过比top多了点,多了圈
也是一样,只挑重要代码解释
private void drawItem(Canvas canvas) { for (int rowIndex = 0; rowIndex < row; rowIndex++) { //获取文字内容 String content = titles.get(rowIndex); //计算文字尺寸 Paint.FontMetrics fontMetrics = mPaintTextNormal.getFontMetrics(); float fontHeight = fontMetrics.bottom - fontMetrics.top; float textWidth = mPaintTextNormal.measureText(content); float y = rowIndex * (mItemHeight + mItemMargin) + mItemHeight - (mItemHeight - fontHeight) / 2 - fontMetrics.bottom - mItemHeight / 2 + getResources().getDimension(R.dimen.table_default_margin_top) - mItemMargin / 2 + mItemMargin; float x = mItemWidth - textWidth - 2 * mItemIndicatorCircleRadius - mItemIndicatorLineWidth - 12; //画字 canvas.drawText(content, x, y, mPaintTextNormal); //画圈 canvas.drawCircle(mItemWidth - mItemIndicatorLineWidth - mItemIndicatorCircleRadius, rowIndex * (mItemHeight + mItemMargin) + getResources().getDimension(R.dimen.table_default_margin_top) - mItemMargin / 2 + mItemMargin, mItemIndicatorCircleRadius, mPaintItemIndicatorCircle); //规划矩阵 canvas.drawRect(mItemWidth - mItemIndicatorLineWidth, rowIndex * (mItemHeight + mItemMargin) + getResources().getDimension(R.dimen.table_default_margin_top) - mItemMargin / 2 - mItemMargin / 2 + mItemMargin, mItemWidth, rowIndex * (mItemHeight + mItemMargin) + getResources().getDimension(R.dimen.table_default_margin_top) - mItemMargin / 2 + mItemMargin / 2 + mItemMargin, mPaintItemIndicatorLine); } }
这是一个增量的绘画过程,每个循环都增量上一轮的参数值
CustomView
//大小计算 @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); //(间距总和+宽度总和),(高度总和+间距总和) setMeasuredDimension(column * mItemWidth + (column - 1) * mItemMargin, row * mItemHeight + row * mItemMargin); }
//实际画框的操作,循环画框,也就是说在初始化时所有的ui已经画出来了只是没滑到的时候不显示而已 private void drawItem(Canvas canvas) { for (int rowIndex = 0; rowIndex < row; rowIndex++) { for (int columnIndex = 0; columnIndex < column; columnIndex++) { adJustSelectPaintColor(columnIndex, rowIndex); float left = mItemWidth * columnIndex + mItemMargin * columnIndex; float right = left + mItemWidth; float top = mItemHeight * rowIndex + mItemMargin * (rowIndex + 1); float bottom = top + mItemHeight; canvas.drawRect(left, top, right, bottom, mPaintItemBg); String content = getShowData(rowIndex, columnIndex); Paint.FontMetrics fontMetrics = mPaintTextNormal.getFontMetrics(); float fontHeight = fontMetrics.bottom - fontMetrics.top; float textWidth = mPaintTextNormal.measureText(content); float y = top + mItemHeight - (mItemHeight - fontHeight) / 2 - fontMetrics.bottom; float x = left + mItemWidth / 2 - textWidth / 2; //画字 canvas.drawText(content, x, y, mPaintTextNormal); } } }
双循环画字画方块画间距。
写个接口解决点击行为
public interface OnPositionClickListener { void onPositionClick(Position position); }
public boolean onTouchEvent(MotionEvent event) { Position position = getPositionFromLocation(event.getX(), event.getY()); if (position == null) { return true; } switch (event.getAction()) { case MotionEvent.ACTION_DOWN: break; case MotionEvent.ACTION_UP: if (mPositionChangeListener != null) { mPositionChangeListener.onPositionClick(position); } break; } return true; }
在onTouchEvent记录坐标点
内部的子View都讲完了,剩下就是”载体”ScrollTableView
public class ScrollTableView extends LinearLayout implements CustomTableView.OnPositionClickListener { //各种对象 private IHorizontalScrollView scrollHeaderHorizontal; private IHorizontalScrollView scrollHorizontal; private LeftTitleView headerVertical; private TopTitleView headerHorizontal; private CustomTableView contentView; //被点击的坐标集合 private ArrayList<Position> selectPositions; private ArrayList<String> topTitles; private ArrayList<String> leftTitles; //数据源 private ArrayList<ArrayList<String>> datas; public ScrollTableView(Context context) { this(context, null); } public ScrollTableView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public ScrollTableView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); init(context, attrs, defStyleAttr); } private void init(Context context, AttributeSet attrs, int defStyleAttr) { LayoutInflater.from(context).inflate(R.layout.view_container, this); setUpView(); setUpData(); selectPositions = new ArrayList<>(); if (attrs == null) return; final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ScrollTableView, defStyleAttr, 0); if (a == null) return; headerVertical.setUpAttrs(context, attrs, defStyleAttr); headerHorizontal.setUpAttrs(context, attrs, defStyleAttr); contentView.setUpAttrs(context, attrs, defStyleAttr); a.recycle(); } //同步了2个横向 2个纵向滚动的行为 private void setUpView() { scrollHeaderHorizontal = (IHorizontalScrollView) findViewById(R.id.scroll_header_horizontal); scrollHorizontal = (IHorizontalScrollView) findViewById(R.id.scroll_horizontal); headerVertical = (LeftTitleView) findViewById(R.id.header_vertical); headerHorizontal = (TopTitleView) findViewById(R.id.header_horizontal); contentView = (CustomTableView) findViewById(R.id.content_view); //传递滚动行为 scrollHorizontal.setIScroller(scrollHeaderHorizontal); scrollHeaderHorizontal.setIScroller(scrollHorizontal); } public LeftTitleView getLeftTitleView() { return headerVertical; } public TopTitleView getTopTitleView() { return headerHorizontal; } public CustomTableView getContentView() { return contentView; } private void setUpData() { leftTitles = new ArrayList<>(); topTitles = new ArrayList<>(); datas = new ArrayList<>(); contentView.setRowAndColumn(leftTitles.size(), topTitles.size()); contentView.setOnPositionChangeListener(this); } //填充数据 public void setDatas(ArrayList<String> topTitlesData, ArrayList<String> leftTitlesData, ArrayList<ArrayList<String>> itemData) { topTitles.clear(); leftTitles.clear(); datas.clear(); topTitles.addAll(topTitlesData); leftTitles.addAll(leftTitlesData); datas.addAll(itemData); updateView(); } //刷新数据 private void updateView() { headerVertical.updateTitles(leftTitles); headerHorizontal.updateTitles(topTitles); contentView.setRowAndColumn(leftTitles.size(), topTitles.size()); contentView.setDatas(datas); } //记录坐标点 @Override public void onPositionClick(Position position) { if (selectPositions.contains(position)) { selectPositions.remove(position); } else { selectPositions.add(position); } contentView.setSelectPositions(selectPositions); } //获取被点击的集合 public ArrayList<Position> getSelectPositions() { return selectPositions; } }
整体不是很难,但是要考虑好各个空间的关系,对于View Group的入门学习还是不错的,解释的过程中省略了一些传递参数和逻辑代码,不明白的建议看源码!!
作者:王亟亟