简述:
为什么要另写这篇Demo博客?
上篇博客最后给出了一个折线图的例子,记得当时是说下篇博客给出其源码,但是后来我又想了下,咱们既然是新手系列的自定义View,内容就需要做到详细,清楚和明白。如果随意丢出一坨源码对于新手来说太坑爹了,我们每一位老司机都是从新手走过来,相信大家对于动不动就扔出一坨代码博主,心中很是无奈。今天咱们就简单分析下这个demo。
回答上篇博客的问题?
上篇博客说到如何在一个自定义View中对某个特定区域实现点击触摸反馈。我们都知道对于View独立单一组件唯一可以设置点击的就是对整个View设置点击事件。对于一个View控件的某个区域也能实现点击触摸反馈吗?答案是可以的,本期的例子柱状图也是只有点击矩形才会有事件触发,而不是整个View控件。这实际上就是用到了一个特殊的API的Region区域。它可以框出某个VIew的某个特定的区域,然后在View的OnTouchEvent事件中,监听手指按下和抬起点是否落入对应的Region区域,如果落入就会给出回调,就这么简单粗暴。
自定义View直方图思路分析?
首先从整个控件角度出发,任何控件实现都得包括了数据接口定义和UI渲染绘制两个方面。
对于数据接口定义,从本控件可以看到就两个数据因素,一是类型对应的名称(String),二是对应类型占最大值得比例(float),用比例更加直观反映出绘制的高度。两个数据因素,很多人认为写个类包一下,个人感觉对于View层直接渲染数据越是简单数据类型,它的通用性越强。所以我想到的时候通过外部传入一个List < Pair < String, float> >即可。
对于UI渲染绘制,考虑有两个方面静态绘制和点击交互。
对于静态绘制可以分为纯直方图的绘制和折线图绘制。
直方图的绘制包括坐标系绘制、文字集合绘制、矩形集合绘制。
对于坐标系绘制为了计算绘制坐标方便,将原来默认为View左上角的坐标原点,通过translate位移canvas画布正好移动到直方图坐标的原点位置。 文字的绘制就是上期博客讲的,不过是文字集合通过一个循环来绘制,矩形集合的绘制使用的是循环 + path.addRect方式,最后绘制整个Path就OK。
折线图的绘制包括点的集合绘制、线段的集合绘制。
点集合绘制采用的是drawPoints绘制多个点,线段集合绘制采用drawLines绘制多条线。
对于点击交互就比较麻烦点,思路是定义一个和View控件画布一样大小的Region,姑且定义为globalRegion.然后针对每一个矩形都定义一个Path和Region.把每个矩形通过path.addRect加入对应的Path中,通过region.setPath(path, globalRegion)将path区域和clip区域取交集,捕获出对应path的region区域坐标范围。最后把每个path添加到全局的mPath中,把每个region区域加入mRegionList集合中。
通过重写onTouchEvent,获得点击的点的坐标,通过遍历mRegionList集合判断触摸点是否落入对应的区域的Region的position。然后再次重绘,在onDraw方法中针对相应的position绘制不同颜色背景矩形即可。
最后一个坑就是坐标转换,因为在绘制的时候我们为了计算坐标简单将坐标系原点移到直方图坐标原点位置,但是在onTouchEvent触摸坐标系却还是以View左上角为原点的坐标系,所以需要有个触摸点坐标系向绘制坐标系的转换。利用了Matrix矩阵中逆矩阵的知识。
本篇博客包含哪些核心知识点内容?
Canvas中绘制矩形(上篇博客已经讲过)
Canvas的绘制多条线段(上篇博客已经讲过)
Canvas的绘制多个点(上篇博客已经讲过)
Canvas绘制文字(上篇博客已经讲过)
Canvas的几何变换使用translate移动绘图坐标系(后期博客会详细讲解,可以先记住一下,下次讲的时候不会太陌生)
Canvas的绘制Path(下期博客会详细讲解,可以先记住一下,下次讲的时候不会太陌生)
Canvas中的Path与Region的组合使用裁切矩形区域。(难点,下期会深入讲解)
Region区域的使用判断触摸点是否落入柱状图形的范围区域并给出触摸反馈的回调。(难点,下期会深入讲解)
变换坐标系后,通过Matrix类的逆矩阵实现,触摸坐标系的坐标向绘图坐标的坐标转换。(难点,下期会深入讲解)
1、数据接口定义List < Pair < String, float> >类型
使用基本数据类型,有利于提高View的通用性
List<Pair<String,Float
>> pairList
= new ArrayList
<>(); pairList
.add(
new Pair<>(
"Java",
0.71f));
pairList
.add(
new Pair<>(
"Swift",
0.61f));
pairList
.add(
new Pair<>(
"C",
0.26f));
pairList
.add(
new Pair<>(
"C++",
0.37f));
pairList
.add(
new Pair<>(
"Python",
0.84f));
pairList
.add(
new Pair<>(
"Go",
0.6f));
setContentView(
new RectChart(this, pairList));
2、坐标轴的绘制
为了绘制方便和坐标计算方便,采用translate位移画布将原来View左上角原点移动到直方图原点(控件中两个坐标轴交点位置)
private void initCoordinateCanvas(Canvas
canvas) {
canvas.drawColor(Color.parseColor(
"#eeeeee"));
canvas.translate(
100, mHeight -
100);
if (mMatrix.isIdentity()) {
canvas.getMatrix().invert(mMatrix);
}
float[] opts = {
0,
0, mWidth -
200,
0,
0,
0,
0, -(mHeight -
200)};
canvas.drawLines(opts, mCoordinatePaint);
mCoordinatePaint.setStrokeCap(Paint.Cap.ROUND);
mCoordinatePaint.setStrokeWidth(
20f);
canvas.drawPoint(
0,
0, mCoordinatePaint);
}
3、文字集合的绘制
文字绘制采用了paint.setTextBounds方式拿到文字的尺寸
mTextPaint =
new Paint();
mTextPaint.setAntiAlias(
true);
mTextPaint.setColor(Color.parseColor(
"#C2185B"));
mTextPaint.setTextSize(
40);
if (mDataList ==
null || mDataList.isEmpty()) {
return;
}
for (Pair<String, Float> pair : mDataList) {
Rect textBound =
new Rect();
mTextPaint.getTextBounds(pair.first,
0, pair.first.length(), textBound);
mTextBounds.add(textBound);
}
private void drawTextList(Canvas canvas) {
for (
int i =
0; i < mRegionList.size(); i++) {
canvas.drawText(mDataList.
get(i).first, mRegionList.
get(i).getBounds().left + (mRectWidth /
2 - mTextBounds.
get(i).width() /
2), mTextBounds.
get(i).height() +
20F, mTextPaint);
}
}
3、矩形集合的绘制
矩形集合绘制比较麻烦,需要裁剪每个rect的region,以及添加对应的path
mGlobalRegion =
new Region(-w, -h, w, h);
mPointList.clear();
mRegionList.clear();
for (
int i =
0; i < mDataList.size(); i++) {
float left = mGap * (i +
1) + mRectWidth * i;
float top = -mDataList.
get(i).second * (mHeight -
200);
float right = left + mRectWidth;
float bottom = -mCoordinatePaint.getStrokeWidth();
Point point =
new Point(left + mRectWidth /
2, top);
mPointList.add(point);
Path path =
new Path();
path.addRect(left, top, right, bottom, Path.Direction.CW);
Region region =
new Region();
region.setPath(path, mGlobalRegion);
mPath.addPath(path);
mRegionList.add(region);
}
private void drawHistogram(Canvas canvas) {
canvas.drawPath(mPath, mRectPaint);
if (mClickPosition != -
1) {
mRectPaint.setColor(mPressColor);
canvas.drawRect(mRegionList.
get(mClickPosition).getBounds(), mRectPaint);
mClickPosition = -
1;
}
}
4、折线图的绘制
折线图绘制比较简单,就是对一组点和一组线段的绘制,关键就是坐标的计算,我们在计算矩形的位置顺带就把点的信息计算好,最后保存在pointList集合,直接拿来用即可。
Point point =
new Point(left + mRectWidth /
2, top);
mPointList.add(point);
private void drawPolyline(Canvas canvas) {
for (
int i =
0; i < mPointList.size(); i++) {
mCoordinatePaint.setStrokeWidth(
20f);
canvas.drawPoint(mPointList.
get(i).x, mPointList.
get(i).y, mCoordinatePaint);
if (i < mPointList.size() -
1) {
mCoordinatePaint.setStrokeWidth(
5f);
canvas.drawLine(mPointList.
get(i).x, mPointList.
get(i).y, mPointList.
get(i +
1).x, mPointList.
get(i +
1).y, mCoordinatePaint);
}
}
}
5、点击交互的实现
它实现核心在于region中提供一个region.contain(x, y)方法可以判断传入点的坐标是否落入当前的region内,返回true or false
@Override
public boolean
onTouchEvent(MotionEvent
event) {
float[] pts =
new float[
2];
pts[
0] =
event.getX();
pts[
1] =
event.getY();
mMatrix.mapPoints(pts);
int touchX = (
int) pts[
0];
int touchY = (
int) pts[
1];
switch (
event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
mClickPosition = findTouchPoint(touchX, touchY);
if (mClickPosition != -
1) {
invalidate();
Toast.makeText(getContext(), String.format(Locale.US,
"当前选中: %s 数据为: %f", mDataList.
get(mClickPosition).first, mDataList.
get(mClickPosition).second), Toast.LENGTH_SHORT).show();
}
break;
}
return super.onTouchEvent(
event);
}
private int findTouchPoint(
int touchX,
int touchY) {
int position = -
1;
for (
int i =
0; i < mRegionList.size(); i++) {
Region region = mRegionList.
get(i);
if (region.contains(touchX, touchY)) {
position = i;
return position;
}
}
return position;
}
6、坐标转换的坑
由于一开始为了绘制方便变换了绘图坐标系,可是触摸坐标又不能变换,手指触摸的坐标系和画布坐标系不统一,就可能引起手指触摸位置和绘制位置不统一,只能将触摸的坐标转化成绘图坐标系。这里需要用到Matrix矩阵知识,Matrix最大功能之一就是坐标映射,数值转换,后期会专门了解Matrix。
canvas.translate(
100, mHeight -
100);
if (mMatrix.isIdentity()) {
canvas.getMatrix().invert(mMatrix);
}
mMatrix.mapPoints(pts);
7、最后附上全部源码
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Rect;
import android.graphics.Region;
import android.util.AttributeSet;
import android.util.Pair;
import android.view.MotionEvent;
import android.widget.Toast;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class RectChart extends CanvasView {
private int mNormalColor;
private int mPressColor;
private Paint mRectPaint;
private Paint mTextPaint;
private Paint mCoordinatePaint;
private Matrix mMatrix;
private Path mPath =
new Path();
private Region mGlobalRegion;
private List<Rect> mTextBounds =
new ArrayList<>();
private List<Point> mPointList =
new ArrayList<>();
private List<Region> mRegionList =
new ArrayList<>();
private int mWidth;
private int mHeight;
private float mGap =
40f;
private float mRectWidth =
80f;
private boolean isShowHistogram =
true;
private boolean isShowPolyline =
true;
private int mClickPosition = -
1;
private List<Pair<String, Float>> mDataList;
public RectChart(Context context, List<Pair<String, Float>> mDataList) {
super(context);
this.mDataList = mDataList;
}
@Override
protected void initDrawTools() {
mRectPaint =
new Paint();
mRectPaint.setAntiAlias(
true);
mRectPaint.setColor(mNormalColor);
mRectPaint.setStyle(Paint.Style.FILL);
mCoordinatePaint =
new Paint();
mCoordinatePaint.setAntiAlias(
true);
mCoordinatePaint.setColor(Color.RED);
mCoordinatePaint.setStyle(Paint.Style.STROKE);
mCoordinatePaint.setStrokeWidth(
5f);
mTextPaint =
new Paint();
mTextPaint.setAntiAlias(
true);
mTextPaint.setColor(Color.parseColor(
"#C2185B"));
mTextPaint.setTextSize(
40);
if (mDataList ==
null || mDataList.isEmpty()) {
return;
}
for (Pair<String, Float> pair : mDataList) {
Rect textBound =
new Rect();
mTextPaint.getTextBounds(pair.first,
0, pair.first.length(), textBound);
mTextBounds.add(textBound);
}
mMatrix =
new Matrix();
}
@Override
protected void fetchDefAttrValues(Context context, AttributeSet attrs,
int defStyleAttr) {
mNormalColor = Color.parseColor(
"#ff9900");
mPressColor = Color.parseColor(
"#ff0000");
}
@Override
protected void onMeasure(
int widthMeasureSpec,
int heightMeasureSpec) {
setMeasuredDimension(
1080,
1000);
}
@Override
protected void onSizeChanged(
int w,
int h,
int oldw,
int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
if (mMatrix !=
null) {
mMatrix.reset();
}
mWidth = w;
mHeight = h;
mGlobalRegion =
new Region(-w, -h, w, h);
mPointList.clear();
mRegionList.clear();
for (
int i =
0; i < mDataList.size(); i++) {
float left = mGap * (i +
1) + mRectWidth * i;
float top = -mDataList.get(i).second * (mHeight -
200);
float right = left + mRectWidth;
float bottom = -mCoordinatePaint.getStrokeWidth();
Point point =
new Point(left + mRectWidth /
2, top);
mPointList.add(point);
Path path =
new Path();
path.addRect(left, top, right, bottom, Path.Direction.CW);
Region region =
new Region();
region.setPath(path, mGlobalRegion);
mPath.addPath(path);
mRegionList.add(region);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initDrawTools();
initCoordinateCanvas(canvas);
drawTextList(canvas);
if (isShowHistogram) {
drawHistogram(canvas);
}
if (isShowPolyline) {
drawPolyline(canvas);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
float[] pts =
new float[
2];
pts[
0] = event.getX();
pts[
1] = event.getY();
mMatrix.mapPoints(pts);
int touchX = (
int) pts[
0];
int touchY = (
int) pts[
1];
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_UP:
mClickPosition = findTouchPoint(touchX, touchY);
if (mClickPosition != -
1) {
invalidate();
Toast.makeText(getContext(), String.format(Locale.US,
"当前选中: %s 数据为: %f", mDataList.get(mClickPosition).first, mDataList.get(mClickPosition).second), Toast.LENGTH_SHORT).show();
}
break;
}
return super.onTouchEvent(event);
}
private int findTouchPoint(
int touchX,
int touchY) {
int position = -
1;
for (
int i =
0; i < mRegionList.size(); i++) {
Region region = mRegionList.get(i);
if (region.contains(touchX, touchY)) {
position = i;
return position;
}
}
return position;
}
private void drawTextList(Canvas canvas) {
for (
int i =
0; i < mRegionList.size(); i++) {
canvas.drawText(mDataList.get(i).first, mRegionList.get(i).getBounds().left + (mRectWidth /
2 - mTextBounds.get(i).width() /
2), mTextBounds.get(i).height() +
20F, mTextPaint);
}
}
private void initCoordinateCanvas(Canvas canvas) {
canvas.drawColor(Color.parseColor(
"#eeeeee"));
canvas.translate(
100, mHeight -
100);
if (mMatrix.isIdentity()) {
canvas.getMatrix().invert(mMatrix);
}
float[] opts = {
0,
0, mWidth -
200,
0,
0,
0,
0, -(mHeight -
200)};
canvas.drawLines(opts, mCoordinatePaint);
mCoordinatePaint.setStrokeCap(Paint.Cap.ROUND);
mCoordinatePaint.setStrokeWidth(
20f);
canvas.drawPoint(
0,
0, mCoordinatePaint);
}
private void drawHistogram(Canvas canvas) {
canvas.drawPath(mPath, mRectPaint);
if (mClickPosition != -
1) {
mRectPaint.setColor(mPressColor);
canvas.drawRect(mRegionList.get(mClickPosition).getBounds(), mRectPaint);
mClickPosition = -
1;
}
}
private void drawPolyline(Canvas canvas) {
for (
int i =
0; i < mPointList.size(); i++) {
mCoordinatePaint.setStrokeWidth(
20f);
canvas.drawPoint(mPointList.get(i).x, mPointList.get(i).y, mCoordinatePaint);
if (i < mPointList.size() -
1) {
mCoordinatePaint.setStrokeWidth(
5f);
canvas.drawLine(mPointList.get(i).x, mPointList.get(i).y, mPointList.get(i +
1).x, mPointList.get(i +
1).y, mCoordinatePaint);
}
}
}
class Point {
private float x;
private float y;
public Point(
float x,
float y) {
this.x = x;
this.y = y;
}
}
}
结束
这个例子就算是讲完,下篇博客咱们一起讲下Path相关的内容。一下子水了这么多,好渴喝水去了。对于还不是很不知道知识,可以先记住。