分享一下自定义控件的思路,单纯是个人的经验。 首先是美工妹子给的效果图。
然后是这次做出来的真机截图。
这次主要分享我的思路,而不是具体的代码实现。
1.功能分析
显示0~100的数据,数据个数不确定,横向可以滑动,点击会出现气泡。这是典型的折线图的需求。
2.细节效果确认
背景色,横向坐标背景色,纵向坐标背景色,单位线,折线渐变背景,顶点有弧度,点击出现气泡,点击顶点出现圆圈,相应的单位线高亮,相应的横坐标高亮。
3.绘制思路
①确定控件大小
控件宽度铺满屏幕,高度可以自定义。但是这一切都是通过xml设置的,控件本身不需要做特殊处理。但是这里有个比较重要的点。就是字体的大小,字体的大小不能是不变的,要可以根据控件的尺寸进行缩放。甚至可以同将要绘制的字数进行缩放。
②确定关键部件的坐标
关键坐标可以死自定义view坐标基本不变的地方,比如这次的横纵坐标的位置。其他的部件的位置都是根据关键部件的位置来绘制的。控件大小确定了之后,关键部件的位置就可以大致确定,比如这次的这个折线图,横坐标的y轴位置是控件的高度分成12.5个格子后的第12个格子里面,同理纵坐标的x轴位置是控件宽度分成10.5个格子后最后一个格子里面。
③确定可变量
可变量的确定是很关键的,某些可变量可以通过外部设置,从而提高控件的适用性。另外某些变量是动画的因子,或者滑动的因子。这次的控件的可变量是横向的格子的数量,曲线的横坐标(用于滑动)。
④确定绘制顺序
后绘制的会把前面绘制的图层遮挡,所以要确定绘制顺序。还有事件触发的绘制的时机。
⑤了解绘制时会用到的API和数据的结构
就是要知道具体效果绘制时要要的API,比如如何画曲线啊,如何绘制渐变啊等等。数据的结构也是在这个时候就要确定了。
⑥绘制固定状态
我的个人习惯是先绘制固定状态,就是不要考虑动画或者触摸事件,只是绘制关键状态的控件的样子,当然这里就要使用上面说到的可变量绘制。这么做类似做动画时的关键帧吧。
⑦改变可变量
改变可变量的值,让各个固定状态连接起来,适时的呼起重绘,从而控件就动了起来。
⑧触摸事件处理
根据触摸事件修改可变量。当然前提是要十分了解Android触摸事件处理机制。
⑨美工过目
效果出来,给设计人员看看,看看是否漏掉某些细节。或者哪些细节需要修改。
⑩优化
优化界面效果,增加动画,优化内存和绘制效率,适配分辨率。
4.使用
实际项目中使用,持续优化。
最后,代码分享,用到这个效果的同学可以自己改改。
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.CornerPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PathEffect;
import android.graphics.RectF;
import android.graphics.Shader;
import android.support.annotation.Nullable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import com.allrun.arsmartelevatorformanager.R;
import com.allrun.arsmartelevatorformanager.util.DPUnitUtil;
import java.util.List;
/**
* Created by GrenndaMi on 2017/4/5.
*/
public class PPChart extends View {
Context mContext;
Paint mPaint;
private int mXDown, mLastX;
//最短滑动距离
int a = 0;
float startX = 0;
float lastStartX = 0;//抬起手指后,当前控件最左边X的坐标
float cellCountW = 9.5f;//一个屏幕的宽度会显示的格子数
float cellCountH = 12.5f;//整个控件的高度会显示的格子数
float cellH, cellW;
float topPadding = 0.25f;
PathEffect mEffect = new CornerPathEffect(20);//平滑过渡的角度
int state = -100;
int lineWidth;
public void setData(List<dataObject> data) {
this.data = data;
state = -100;
postInvalidate();
}
List<dataObject> data;
public PPChart(Context context) {
super(context);
mContext = context;
}
public PPChart(Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
mContext = context;
a = DPUnitUtil.px2dip(context, ViewConfiguration.get(context).getScaledDoubleTapSlop());
setClickable(true);
lineWidth = DPUnitUtil.dip2px(mContext, 1);
}
public PPChart(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
mContext = context;
}
private void initPaint() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
//线的颜色
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
initPaint();
cellH = getHeight() / cellCountH;
cellW = getWidth() / cellCountW;
//画底部背景
mPaint.setColor(0xff44b391);
canvas.drawRect(0, (((int) cellCountH - 1) + topPadding) * cellH, getWidth(), cellCountH * cellH, mPaint);
if (data == null || data.size() == 0) {
return;
}
DrawAbscissaLines(canvas);
DrawOrdinate(canvas);
//------------到此背景结束---------------
canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
DrawDataBackground(canvas);
canvas.restore();
canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
DrawDataLine(canvas);
canvas.restore();
canvas.saveLayer(0, 0, getWidth(), getHeight(), mPaint, Canvas.ALL_SAVE_FLAG);
DrawAbscissa(canvas);
canvas.restore();
showPop(canvas);
if (state == -100) {
gotoEnd();
}
}
//画横坐标
private void DrawOrdinate(Canvas canvas) {
mPaint.reset();
float i = 0.5f;
for (dataObject tmp : data) {
mPaint.setColor(0xffb4e1d3);
mPaint.setTextSize(getWidth() / cellCountW / 3.2f);
dataObject tmp2 = getDataByX(mLastX);
//选中的那一项需要加深
if (tmp2 != null && tmp2.getHappenTime().equals(tmp.getHappenTime()) && state == MotionEvent.ACTION_UP && Math.abs(mLastX - mXDown) < a) {
mPaint.setColor(0xffffffff);
} else {
mPaint.setColor(0xffb4e1d3);
}
String str1 = tmp.getHappenTime().split("-")[0];
canvas.drawText(str1,
startX + cellW * i - mPaint.measureText(str1) / 2,
(((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH,
mPaint);
mPaint.setTextSize(getWidth() / cellCountW / 3.5f);
String str2 = tmp.getHappenTime().split("-")[1] + "." + tmp.getHappenTime().split("-")[2];
canvas.drawText(str2,
startX + cellW * i - mPaint.measureText(str2) / 2,
(((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH - 1.5f * (mPaint.ascent() + mPaint.descent()),
mPaint);
//画背景竖线
mPaint.setColor(0xff92dac4);
canvas.drawLine(startX + cellW * i,
topPadding * cellH,
startX + cellW * i,
(topPadding + 10.5f) * cellH,
mPaint);
i++;
}
mPaint.setColor(0xffb4e1d3);
mPaint.setTextSize(getWidth() / cellCountW / 3f);
canvas.drawText("end",
startX + cellW * i - mPaint.measureText("end") / 2,
(((int) cellCountH - 1) + topPadding + cellCountH) / 2 * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
}
//画纵坐标
public void DrawAbscissaLines(Canvas canvas) {
mPaint.setColor(0xff92dac4);
//画背景横线
canvas.drawLine(0,
topPadding * cellH,
cellW * 9.5f,
topPadding * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 1) * cellH,
cellW * 9.5f,
(topPadding + 1) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 2) * cellH,
cellW * 9.5f,
(topPadding + 2) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 3) * cellH,
cellW * 9.5f,
(topPadding + 3) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 4) * cellH,
cellW * 9.5f,
(topPadding + 4) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 5) * cellH,
cellW * 9.5f,
(topPadding + 5) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 6) * cellH,
cellW * 9.5f,
(topPadding + 6) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 7) * cellH,
cellW * 9.5f,
(topPadding + 7) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 8) * cellH,
cellW * 9.5f,
(topPadding + 8) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 9) * cellH,
cellW * 9.5f,
(topPadding + 9) * cellH,
mPaint);
canvas.drawLine(0,
(topPadding + 10) * cellH,
cellW * 9.5f,
(topPadding + 10) * cellH,
mPaint);
}
//画纵坐标
public void DrawAbscissa(Canvas canvas) {
mPaint.reset();
mPaint.setColor(mContext.getResources().getColor(R.color.colorPrimary));
//画纵坐标背景9.51 = 10 - 0.5(i 的起步)+ 0.01(把最后一条线露出来)
canvas.drawRect(cellW * ((int) cellCountW - 0.5f + 0.01f), 0, cellW * ((int) cellCountW + 1), 11.2f * cellH, mPaint);
mPaint.setColor(0xffb6e6d7);
mPaint.setTextSize(getWidth() / cellCountW / 3);
canvas.drawText("100%",
cellW * (int) cellCountW - mPaint.measureText("100%") / 2,
topPadding * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
canvas.drawText("80%",
cellW * (int) cellCountW - mPaint.measureText("80%") / 2,
(topPadding + 2) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
canvas.drawText("60%",
cellW * (int) cellCountW - mPaint.measureText("60%") / 2,
(topPadding + 4) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
canvas.drawText("40%",
cellW * (int) cellCountW - mPaint.measureText("40%") / 2,
(topPadding + 6) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
canvas.drawText("20%",
cellW * (int) cellCountW - mPaint.measureText("20%") / 2,
(topPadding + 8) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
canvas.drawText("0%",
cellW * (int) cellCountW - mPaint.measureText("0%") / 2,
(topPadding + 10) * cellH - (mPaint.ascent() + mPaint.descent()) / 2,
mPaint);
}
//画渐变背景
private void DrawDataBackground(Canvas canvas) {
if (data == null || data.size() == 0) {
return;
}
LinearGradient lg = new LinearGradient(getWidth() / 2, topPadding * cellH, getWidth() / 2, (topPadding + 10) * cellH, 0xaaffffff, 0xaa61ccab, Shader.TileMode.CLAMP);
mPaint.setShader(lg);
float i = 0.5f;
Path path = new Path();
//起点和终点要多画2次,防止圆角出现
path.moveTo(startX + cellW * i, (topPadding + 10) * cellH);
path.lineTo(startX + cellW * i, (topPadding + 10) * cellH);
path.lineTo(startX + cellW * i, getHByValue(data.get(0).getNum()));
for (dataObject tmp : data) {
path.lineTo(startX + cellW * i, getHByValue(tmp.getNum()));
i++;
}
path.lineTo(startX + cellW * (i -1), getHByValue(data.get(data.size()-1).getNum()));
path.lineTo(startX + cellW * (i - 1), (topPadding + 10) * cellH -1);
path.lineTo(startX + cellW * (i - 1), (topPadding + 10) * cellH);
path.close();
mPaint.setPathEffect(mEffect);
canvas.drawPath(path, mPaint);
}
//画数据线
public void DrawDataLine(Canvas canvas) {
float i = 0.5f;
mPaint.reset();
mPaint.setStrokeWidth(lineWidth);
mPaint.setColor(0xffffffff);
Path path = new Path();
path.moveTo(startX + cellW * i -1, getHByValue(data.get(0).getNum()));
path.lineTo(startX + cellW * i, getHByValue(data.get(0).getNum()));
for (dataObject tmp : data) {
path.lineTo(startX + cellW * i, getHByValue(tmp.getNum()));
i++;
}
path.lineTo(startX + cellW * (i -1), getHByValue(data.get(data.size()-1).getNum()));
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setPathEffect(mEffect);
canvas.drawPath(path, mPaint);
}
//显示数据气泡
private void showPop(Canvas canvas) {
//点击了
if (state == MotionEvent.ACTION_UP && Math.abs(mLastX - mXDown) < a) {
dataObject data = getDataByX(mLastX);
if (data == null) {
return;
}
initPaint();
// 选中的线
mPaint.setColor(0xaaffffff);
canvas.drawLine(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), getXBykey(data.getHappenTime()), (topPadding + 10f) * cellH, mPaint);
//画气泡背景
mPaint.setColor(0xffffffff);
mPaint.setTextSize(getWidth() / cellCountW / 3f);
Paint.FontMetricsInt fontMetrics = mPaint.getFontMetricsInt();
RectF r;
//气泡距离顶点有0.5个格子高度的距离,气泡的高度是文字高度的1.5倍。宽度是文字宽度的1.6倍(0.8+0.8)
float left = getXBykey(data.getHappenTime()) - mPaint.measureText(data.getNum() + "%") * 0.8f;
if(left < 0 ){
left = 0;
}
float right = left + 2 * mPaint.measureText(data.getNum() + "%") * 0.8f;
if (data.getNum() >= 10) {
r = new RectF(left,
getHByValue(data.getNum()) + 0.5f * cellH,
right,
getHByValue(data.getNum()) + 0.5f * cellH + 1.5f * (fontMetrics.bottom - fontMetrics.top));
} else {
r = new RectF(left,
getHByValue(data.getNum()) - 0.5f * cellH - 1.5f * (fontMetrics.bottom - fontMetrics.top),
right,
getHByValue(data.getNum()) - 0.5f * cellH);
}
//画气泡上的文字
canvas.drawRoundRect(r, 90, 90, mPaint);
mPaint.setColor(0xff414141);
float baseline = (r.bottom + r.top - fontMetrics.bottom - fontMetrics.top) / 2;
canvas.drawText(data.getNum() + "%",
(r.left+ r.right)/2 - mPaint.measureText(data.getNum() + "%") / 2f,
baseline, mPaint);
//画线上的圆
mPaint.setStrokeWidth(lineWidth * 2);
mPaint.setColor(0xff49c29d);
canvas.drawCircle(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), lineWidth * 5, mPaint);
mPaint.setColor(0xffffffff);
mPaint.setStyle(Paint.Style.STROKE);
canvas.drawCircle(getXBykey(data.getHappenTime()), getHByValue(data.getNum()), lineWidth * 5, mPaint);
mPaint.setStrokeWidth(lineWidth);
}
}
//触摸处理
@Override
public boolean onTouchEvent(MotionEvent event) {
if (data == null || data.size() == 0) {
return super.onTouchEvent(event);
}
final int action = event.getAction();
switch (action) {
case MotionEvent.ACTION_DOWN:
// 按下
mXDown = (int) event.getRawX();
state = MotionEvent.ACTION_DOWN;
break;
case MotionEvent.ACTION_MOVE:
// 移动
mLastX = (int) event.getRawX();
if (Math.abs(lastStartX - mXDown) < a) {
break;
}
//滑动限制
if (lastStartX + mLastX - mXDown > 0.5f * cellW || lastStartX + mLastX - mXDown + cellW * (data.size() + 0.5f) < cellW * (cellCountW - 1)) {
break;
}
state = MotionEvent.ACTION_MOVE;
startX = lastStartX + mLastX - mXDown;
postInvalidate();
break;
case MotionEvent.ACTION_UP:
// 抬起
lastStartX = startX;
state = MotionEvent.ACTION_UP;
postInvalidate();
break;
default:
break;
}
return super.onTouchEvent(event);
}
//通过坐标,获得附近的点
private dataObject getDataByX(int pointX) {
float i = 0.5f;
dataObject result = null;
for (dataObject tmp : data) {
float x = startX + cellW * i;
if (Math.abs(x - pointX) < cellW / 2) {
result = tmp;
return result;
}
i++;
}
return result;
}
private float getHByValue(float value) {
return (topPadding + 10) * cellH - (cellH * 10) * value / 100;
}
//通过横坐标文字获取该点的X坐标
private float getXBykey(String key) {
float i = 0.5f;
for (dataObject tmp : data) {
if (tmp.getHappenTime().equals(key)) {
return startX + cellW * i;
}
i++;
}
return 0;
}
//显示最右边的最新数据
public void gotoEnd() {
if (data == null || data.size() == 0) {
return;
}
if (data.size() < cellCountW - 1) {
startX = 0;
lastStartX = startX;
postInvalidate();
return;
}
startX = -(cellW) * (data.size() - cellCountW + 1);
lastStartX = startX;
postInvalidate();
}
}
复制代码
用到的数据结构
public class dataObject {
String happenTime;
float num;
public dataObject(String happenTime, float num) {
this.happenTime = happenTime;
this.num = num;
}
public String getHappenTime() {
return happenTime;
}
public void setHappenTime(String happenTime) {
this.happenTime = happenTime;
}
public float getNum() {
return num;
}
public void setNum(float num) {
this.num = num;
}
}
复制代码
xml布局
<com.allrun.arsmartelevatorformanager.widget.PPChart
android:id="@+id/chart"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="3"
android:background="@color/colorPrimary" />
复制代码
代码中设置数据