这里仅仅只对直接继承View来说明。下面是一个模仿QQ运动的一个View来讲解;
图示:
自定义View无外呼几个具体的步骤;
A.构造函数中初始化,一般为定义画笔,如果我们还为该view自定义了属性的话就要解析xml文件中的属性了,但是我觉得可以在代码中实现就没必要那么麻烦了。
B.onMeasure中测量View大小啊,关键调用setMeasuredDimension(adjust_width, adjust_height);宽和高就是我们要定义的大小了。我们一般按最小的一个为准,然后按固定比例缩放就可以了。
C.在OnSizeChanged()中根据我们获得的实际控件的宽和高,然后依照我们已经知晓的各种比例,这里包括各种字体的比例,图片比例,矩形,圆形,弧形,字体位置在这里都已可以计算出来的。就是简单的加减乘除了,复杂点搞个sin,cos或者搞个2介3介贝塞尔曲线等等了。
D.重点就在OnDraw()中了,其实如果在C步骤中我们定义好了而且计算精确的话,下面无非就是些调用函数的问题了。是一些死的东西。比如
画文字的话,要注意字体大小(根据字体高度适配),字体颜色(随便你了,只要不是色盲),字体的位置(根据我提供的工具就可以计算baseLineY)
画图片,就容易了,调用就行了,传个RectF,限制图片有多大,具体位置如何。
画弧形,还是要计算RectF,这个矩形是外接于弧形的,确定下弧形的圆形,弧的半径,就可以轻松得到这个RectF了。然后确定下弧的宽度。
画线,也容易定义起始点就ok了,画笔的描边宽度,填充方式,画笔颜色。画笔的线冒(就是画完线后可以看到两端有圆润,有 方的,或者什么都没有)
复杂点的线状,就是定义Path,然后moveto来定义path的起点,然后lineto一个个点连起来。在path中还可以添加弧的,想要闭合了就close,想要为path设置虚线效果,
生锈铁丝效果等用setPathEffect().
要动画的话,可以使用属性动画ValueAnimator,监听值的变化来改变onDraw中我们要变化的值,然后调用nvalidate()不断刷新即可。
想要响应点击事件就onTouchEvent中识别下手势。比如单击,双击,长按,拖拉等等。这就涉及到手势这块了。这里我们就简单的响应单击事件,单击“查看”那个区域响应单击事件。这里我们定义了一个小方块clcikRectF如果单击是发生在这里边,就使用定义一个接口给回调出去了。
E.定义好了View之后,可以在Xml文件中使用,或者在java中new都行。
以上步骤在该View中都有涉及;
1.定义一个健康参数的实体类;Sports
package com.example.sport;
import java.util.Arrays;
import java.util.List;
import android.R.integer;
public class Sports {
public int[] recentDaysSteps = new int[7];
public int friendsAverageSteps;
public int selfRank;
public List<String> arrayString;
public String championName;
public Sports(int[] recentDaysSteps, int friendsAverageSteps, int selfRank,
List<String> arrayString, String championName) {
super();
this.recentDaysSteps = recentDaysSteps;
this.friendsAverageSteps = friendsAverageSteps;
this.selfRank = selfRank;
this.arrayString = arrayString;
this.championName = championName;
}
public Sports() {
super();
}
public float getRatio() {
return (float) (1.0*recentDaysSteps[6] / friendsAverageSteps);
}
public float getAngle() {
float swipe = getRatio() * 300;
swipe = swipe>=300?300:swipe;
return swipe ;
}
public int getAverageRecentSteps(){
float average =0;
int length = recentDaysSteps.length;
for(int i=0;i<length;i++){
average += recentDaysSteps[i];
}
return (int) (average/length);
}
public int getMaxFromRecentDaysSteps(){
int length = recentDaysSteps.length;
int max = recentDaysSteps[0];
for(int i=1;i<length;i++){
if(recentDaysSteps[i]>max){
max = recentDaysSteps[i];
}
}
return max;
}
}
2.这里写一个有关画字体时,计算位置的工具类:DrawTextUtil
package com.example.sport;
import android.graphics.Paint;
import android.graphics.Paint.FontMetrics;
import android.graphics.Rect;
public class DrawTextUtil {
/**
* mPaint.setTextAlign(Align.CENTER); canvas.drawText("aafdADF", x, y,
* mPaint); Align.CENTER, 绘制的时候保证整体文字的中间在(x,y)处 Align.LEFT,
* 绘制的时候保证整体文字最左边位于(x,y)处 Align.Right, 绘制的时候保证整体文字最右边位于(x,y)处
*
*
* FontMetrics fontMetrics = mPaint.getFontMetrics(); fontMetrics.ascent;
* 可绘制的最顶部 相当于影片一样,显示的安全区域 fontMetrics.descent; 可绘制的最底部 fontMetrics.top;
* 物理最顶部,相当于电视剧的屏幕一样 fontMetrics.bottom; 物理最底部
*
* 我们的文字是显示在ascent与descent之间的 这4个值都是相当于baseline而言的。所以fontMetrics.ascent=
* ascent线-baseline线 得到的结果是负的
*
* 从而可以推出; ascent线Y坐标 = baseline线的y坐标 + fontMetric.ascent; descent线Y坐标 =
* baseline线的y坐标 + fontMetric.descent; top线Y坐标 = baseline线的y坐标 +
* fontMetric.top; bottom线Y坐标 = baseline线的y坐标 + fontMetric.bottom;
*
*
* 所绘文字宽度、int width = paint.measureText(String text); 高度
* Paint.FontMetricsInt fm = paint.getFontMetricsInt(); int top = baseLineY
* + fm.top; int bottom = baseLineY + fm.bottom; int height = fm.bottom -
* fm.top; 和最小矩形获取
*/
/**
*
* @param textPaint
* @return
* fontMetrics.top,fontMetrics.ascent,fontMetrics.descent,fontMetrics
* .bottom
*/
private static float[] getMuxLineOffset(Paint textPaint) {
FontMetrics fontMetrics = textPaint.getFontMetrics();
return new float[] { fontMetrics.top, fontMetrics.ascent,
fontMetrics.descent, fontMetrics.bottom };
}
public static float getTextWidth(Paint textPaint, String text) {
return textPaint.measureText(text);
}
// 物理尺寸的高度,实际中就是我们用来确定基线的
public static float getTextMaxHeight(Paint textPaint) {
Paint.FontMetrics fm = textPaint.getFontMetrics();
return fm.bottom - fm.top;
}
// 显示区域刚好包裹文字的高度
public static int getTextMinHeight(Paint textPaint, String text) {
Rect minRect = new Rect();
textPaint.getTextBounds(text, 0, text.length(), minRect);
return minRect.bottom - minRect.top;
}
/**
* 返回基线位置,已知top点Y坐标
*
* @param textPaint
* @return
*/
public static float getBaseLineYFromTopY(Paint textPaint, float textTopY) {
float baseLineY = textTopY - getMuxLineOffset(textPaint)[0];
return baseLineY;
}
/**
* 返回基线位置,已知center点Y坐标
* @param textPaint
* @param textCenterY
* @return
*/
public static float getBaseLineYFromCenterY(Paint textPaint,
float textCenterY) {
float baseLineY = textCenterY + getTextMaxHeight(textPaint) / 2
- getMuxLineOffset(textPaint)[3];
return baseLineY;
}
}
3.开始绘制QQ运动View:
package com.example.sport;
import android.animation.ValueAnimator;
import android.animation.ValueAnimator.AnimatorUpdateListener;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.DashPathEffect;
import android.graphics.Paint;
import android.graphics.Paint.Align;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.RectF;
import android.text.TextPaint;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
//模仿QQ运动控件
public class SportView extends View {
public interface OnLookChanmpionInfo {
public void lookChanmpionInfo(String chanmpionName);
}
//下边都是些根据具体的UI用尺子量出的比例。
private static final float LARGE_CIRCLE_LEFT_W = (float) (3 / 14.8);
private static final float LARGE_CIRCLE_RADIUS_W = (float) (((14.8 - 3 * 2) / 2) / 14.8);
private static final float LARGE_CIRCLE_TOP_H = (float) (1.2 / 18.3);
private static final float LARGE_CIRCLE_WIDTH_H = (float) (0.5 / 18.3);
private static final float RADIO_BOTTOM_H_ = (float) (2.8 / 18.3);
private static final float RADIO_H_W = (float) (18.3 / 14.8);
private static final float RECT_ROUND_RADIUS_H = (float) (0.3 / 18.3);
private float bottomHeight;
private Path bottomPath;
private RectF clickRect;
private int defaultAverageLineColor = Color.parseColor("#DDDDDD");
private int defaultBackgroundColor = Color.WHITE;
private int defaultBottomBackgroundColor = Color.parseColor("#2EC3FF");
private int defaultLargeCircleBackgroundColor = Color.parseColor("#CCEDF9");
private int defaultLargeCircleColor = Color.parseColor("#26B8F3");
private int defaultMaxLargeTextColor = Color.parseColor("#26B8F3");
private int defaultMinTextColor = Color.parseColor("#AEAFB2");
private int defaultSportValuesColor_WhenLowAverage = Color
.parseColor("#DDDDDD");
private int defaultSportValuesColor_WhenOverAverage = Color
.parseColor("#26B8F3");
private float dividerWidth;
private float firstValueX;
private boolean hasDown;
private Bitmap iconBitmap;
private int iconWidth;
private float largeCircleRadius;
private RectF largeCircleRect;
private float largeCircleWidth;
private float lookTextX;
private float maxValueY;
private Paint mPaint;
private Bitmap nextBitmap;
private float nextIcon_left;
private int nextIconSize;
private OnLookChanmpionInfo onLookChanmpionInfo;
private float perValueWidth;
private RectF rect;
private float rect_round_radius;
private Sports sports;
private float tempSwipeAngle;
private TextPaint textPaint;
private float valueBottomY;
private float viewHeight;
private float viewWidth;
private float textSize_one;
private float textSize_two;
private float textSize_three;
private float textSize_four;
//构造方法
public SportView(Context context, AttributeSet attrs) {
super(context, attrs);
mPaint = new Paint();
mPaint.setAntiAlias(true);
textPaint = new TextPaint();
textPaint.setAntiAlias(true);
iconBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.ic_launcher);
nextBitmap = BitmapFactory.decodeResource(getResources(),
R.drawable.next);
}
//根据measureSpec计算大小
private int adjustViewSize(int measureSpec) {
int defaultSize = Integer.MAX_VALUE;
int mode = MeasureSpec.getMode(measureSpec);
int size = MeasureSpec.getSize(measureSpec);
if (MeasureSpec.AT_MOST == mode || MeasureSpec.EXACTLY == mode) {
defaultSize = size;
}
return defaultSize;
}
private void drawBottom(Canvas canvas) {
// 画冠军icon
float icon_left = (float) (viewWidth * 1.1 / 14.8);
float icon_top = (float) (viewHeight - 1.8 / 2.5 * bottomHeight);
canvas.drawBitmap(iconBitmap, null, new RectF(icon_left, icon_top,
icon_left + iconWidth, icon_top + iconWidth), null);
textPaint.setTextAlign(Align.LEFT);
textPaint.setColor(Color.WHITE);
float x = (float) (viewWidth * 2.6 / 14.8);
float y = DrawTextUtil.getBaseLineYFromCenterY(textPaint, viewHeight
- bottomHeight / 2);
textPaint.setTextSize(textSize_two);
canvas.drawText(sports.championName + "获得今日冠军", x, y, textPaint);
textPaint.setTextSize(textSize_two);
canvas.drawText("查看", lookTextX, y, textPaint);
// 画查看的箭头
canvas.drawBitmap(nextBitmap, null, new RectF(nextIcon_left, viewHeight
- bottomHeight / 2 - nextIconSize / 2, nextIcon_left
+ nextIconSize, viewHeight - bottomHeight / 2 + nextIconSize
/ 2), null);
}
private void drawLargeCircleForSports(Canvas canvas) {
mPaint.setPathEffect(null);
// 大圆弧背景
mPaint.setColor(defaultLargeCircleBackgroundColor);
mPaint.setStyle(Style.STROKE);
mPaint.setStrokeCap(Cap.ROUND);
mPaint.setStrokeWidth(largeCircleWidth);
canvas.drawArc(largeCircleRect, 120f, 300, false, mPaint);
// 大圆弧前景
mPaint.setColor(defaultLargeCircleColor);
canvas.drawArc(largeCircleRect, 120f, tempSwipeAngle, false, mPaint);
// 大圆弧内文字
textPaint.setTextSize(textSize_two);
textPaint.setTextAlign(Align.CENTER);
textPaint.setColor(defaultMinTextColor);
float centerX = viewWidth / 2;
float textTopY1 = (float) (viewHeight * (3.8 / 18.3));
float y1 = DrawTextUtil.getBaseLineYFromTopY(textPaint, textTopY1);
canvas.drawText("截至23:59已走", centerX, y1, textPaint);
textPaint.setColor(defaultMaxLargeTextColor);
textPaint.setTextSize(textSize_four);
float textTopY2 = (float) (viewHeight * (4.8 / 18.3));
float y2 = DrawTextUtil.getBaseLineYFromTopY(textPaint, textTopY2);
canvas.drawText(sports.recentDaysSteps[6] + "", centerX, y2, textPaint);
textPaint.setColor(defaultMinTextColor);
textPaint.setTextSize(textSize_two);
float textTopY3 = (float) (viewHeight * (7.1 / 18.3));
float y3 = DrawTextUtil.getBaseLineYFromTopY(textPaint, textTopY3);
canvas.drawText("好友平均 " + sports.friendsAverageSteps + " 步", centerX,
y3, textPaint);
float textCenterY4 = (float) (viewHeight * (9.8 / 18.3));
textPaint.setTextSize(textSize_two);
textPaint.setColor(defaultMinTextColor);
canvas.drawText("第", (float) (centerX - 1.2 / 14.8 * viewWidth),
DrawTextUtil.getBaseLineYFromCenterY(textPaint, textCenterY4),
textPaint);
textPaint.setTextSize(textSize_three);
textPaint.setColor(defaultMaxLargeTextColor);
canvas.drawText(sports.selfRank + "", centerX,
DrawTextUtil.getBaseLineYFromCenterY(textPaint, textCenterY4),
textPaint);
textPaint.setTextSize(textSize_two);
textPaint.setColor(defaultMinTextColor);
canvas.drawText("名", (float) (centerX + 1.2 / 14.8 * viewWidth),
DrawTextUtil.getBaseLineYFromCenterY(textPaint, textCenterY4),
textPaint);
}
private void drawOuterAndInnerRect(Canvas canvas) {
mPaint.setColor(defaultBackgroundColor);
mPaint.setStyle(Style.FILL);
canvas.drawRoundRect(rect, rect_round_radius, rect_round_radius, mPaint);
mPaint.setColor(defaultBottomBackgroundColor);
mPaint.setStyle(Style.FILL);
canvas.drawPath(bottomPath, mPaint);
}
private void drawRecentDaysRecords(Canvas canvas) {
textPaint.setTextAlign(Align.LEFT);
textPaint.setColor(defaultMinTextColor);
textPaint.setTextSize(textSize_one);
float textLeft = (float) (viewWidth * 0.8 / 14.8);
canvas.drawText("最近7天", textLeft, DrawTextUtil.getBaseLineYFromTopY(
textPaint, (float) (viewHeight * 11.3 / 18.3)), textPaint);
textPaint.setTextAlign(Align.RIGHT);
canvas.drawText("平均" + sports.getAverageRecentSteps() + "步/天",
viewWidth - textLeft, DrawTextUtil.getBaseLineYFromTopY(
textPaint, (float) (viewHeight * 11.3 / 18.3)),
textPaint);
// //记录虚线Y坐标
float valueDashLineY = (float) (viewHeight * 12.8 / 18.3);
float dashWidth = (viewWidth - 2 * textLeft) / 50;
float[] intervals = { dashWidth, dashWidth };
// 画虚线
mPaint.setStrokeWidth(3);
mPaint.setColor(defaultAverageLineColor);
mPaint.setPathEffect(new DashPathEffect(intervals, 0));
Path dashPath = new Path();
dashPath.moveTo(textLeft + dashWidth / 2, valueDashLineY);
dashPath.lineTo(viewWidth - textLeft, valueDashLineY);
canvas.drawPath(dashPath, mPaint);
// 画7天柱形
int maxSteps = sports.getMaxFromRecentDaysSteps();
float pxPerStep = (valueBottomY - maxValueY) / maxSteps;
mPaint.setStrokeWidth(perValueWidth);
for (int i = 0; i < sports.recentDaysSteps.length; i++) {
float stopY = valueBottomY - pxPerStep * sports.recentDaysSteps[i];
if (stopY < valueDashLineY + 1.5) {
mPaint.setColor(defaultSportValuesColor_WhenOverAverage);
} else {
mPaint.setColor(defaultSportValuesColor_WhenLowAverage);
}
canvas.drawLine(firstValueX + dividerWidth * i, valueBottomY,
firstValueX + dividerWidth * i, stopY, mPaint);
}
// 画7天下的文字
textPaint.setTextAlign(Align.CENTER);
textPaint.setTextSize(textSize_one);
float textTopY = (float) (viewHeight * 14 / 18.3);
for (int i = 0; i < sports.recentDaysSteps.length; i++) {
canvas.drawText(sports.arrayString.get(i), firstValueX
+ dividerWidth * i,
DrawTextUtil.getBaseLineYFromTopY(textPaint, textTopY),
textPaint);
}
}
@Override
protected void onDraw(Canvas canvas) {
if (sports == null) {
return;
}
drawOuterAndInnerRect(canvas);
drawLargeCircleForSports(canvas);
drawRecentDaysRecords(canvas);
drawBottom(canvas);
}
// 测量View宽和高,并根据宽高比调整
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int adjust_width = adjustViewSize(widthMeasureSpec);
int adjust_height = adjustViewSize(heightMeasureSpec);
// 以用户填写的宽和高的最小值作为基准,缩放得到实际宽和高
if (adjust_height >= adjust_width) {
adjust_height = (int) (adjust_width * RADIO_H_W);
} else {
adjust_width = (int) (adjust_height / RADIO_H_W);
}
setMeasuredDimension(adjust_width, adjust_height);
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
viewWidth = w;
viewHeight = h;
// 圆角矩形半径
rect_round_radius = (RECT_ROUND_RADIUS_H * viewHeight);
// 最外面的圆角矩形
rect = new RectF(0, 0, viewWidth, viewHeight);
// 最下面的圆角矩形的高度
bottomHeight = RADIO_BOTTOM_H_ * viewHeight;
// 最下面的圆角矩形
bottomPath = new Path();
bottomPath.moveTo(0, viewHeight - bottomHeight);
RectF bottomRect = new RectF(0, viewHeight - bottomHeight, viewWidth,
viewHeight);
float[] radii = new float[] { 0, 0, 0, 0, rect_round_radius,
rect_round_radius, rect_round_radius, rect_round_radius };
bottomPath.addRoundRect(bottomRect, radii, Direction.CW);
// 大圆环的宽度
largeCircleWidth = viewHeight * LARGE_CIRCLE_WIDTH_H;
// 大圆弧半径
largeCircleRadius = viewWidth * LARGE_CIRCLE_RADIUS_W;
// 大圆弧矩形
largeCircleRect = new RectF(LARGE_CIRCLE_LEFT_W * viewWidth,
LARGE_CIRCLE_TOP_H * viewHeight, LARGE_CIRCLE_LEFT_W
* viewWidth + 2 * largeCircleRadius, LARGE_CIRCLE_TOP_H
* viewHeight + 2 * largeCircleRadius);
firstValueX = (float) (viewWidth * 1.8 / 14.8);
valueBottomY = (float) (viewHeight * 13.6 / 18.3);
maxValueY = (float) (viewHeight * 12 / 18.3);
perValueWidth = (float) (viewWidth * 0.4 / 14.8);
dividerWidth = (float) (viewWidth * 1.8 / 14.8);
iconWidth = (int) (bottomHeight * 1.1 / 2.5);
nextIconSize = iconWidth / 2;
nextIcon_left = (float) (viewWidth * 13.6 / 14.8);
lookTextX = (float) (viewWidth * 12.3 / 14.8);
float top = viewHeight - bottomHeight / 2 - nextIconSize / 2;
clickRect = new RectF(lookTextX - nextIconSize, viewHeight
- bottomHeight, viewWidth, viewHeight);
//适配字体
textSize_one = (float) (viewHeight * 0.3 / 18.3);
textSize_two = (float) (viewHeight * 0.4 / 18.3);
textSize_three = (float) (viewHeight * 0.5 / 18.3);
textSize_four = (float) (viewHeight * 1.1 / 18.3);
}
@Override
public boolean onTouchEvent(MotionEvent event) {
int action = event.getAction();
float x2 = event.getX();
float y2 = event.getY();
if (clickRect.contains(x2, y2)) {
if (action == MotionEvent.ACTION_DOWN) {
hasDown = true;
return true;
} else if (action == MotionEvent.ACTION_UP && hasDown == true) {
// 单击
hasDown = false;
if (onLookChanmpionInfo != null) {
onLookChanmpionInfo.lookChanmpionInfo(sports.championName);
}
}
}
return super.onTouchEvent(event);
}
public void setIconBitmap(Bitmap iconBitmap) {
this.iconBitmap = iconBitmap;
}
public void setOnLookChanmpionInfo(OnLookChanmpionInfo onLookChanmpionInfo) {
this.onLookChanmpionInfo = onLookChanmpionInfo;
}
public void setSports(Sports sports) {
this.sports = sports;
ValueAnimator animator = ValueAnimator.ofFloat(0f, sports.getAngle());
animator.setDuration(2000);
animator.addUpdateListener(new AnimatorUpdateListener() {
@Override
public void onAnimationUpdate(ValueAnimator animation) {
tempSwipeAngle = (float) animation.getAnimatedValue();
invalidate();
}
});
animator.start();
}
}
4.使用
package com.example.sport;
import java.util.ArrayList;
import java.util.List;
import android.app.Activity;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.widget.Toast;
import com.example.sport.SportView.OnLookChanmpionInfo;
public class MainActivity extends Activity implements OnLookChanmpionInfo {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SportView sportView = (SportView) findViewById(R.id.sportView);
Sports sports = createSports();
sportView.setSports(sports);
sportView.setIconBitmap(BitmapFactory.decodeResource(getResources(),R.drawable.icon_i));
sportView.setOnLookChanmpionInfo(this);
}
private Sports createSports() {
Sports sports = new Sports();
sports.recentDaysSteps[0] = 230;
sports.recentDaysSteps[1] = 600;
sports.recentDaysSteps[2] = 1002;
sports.recentDaysSteps[3] = 840;
sports.recentDaysSteps[4] = 2014;
sports.recentDaysSteps[5] = 365;
sports.recentDaysSteps[6] = 740;
sports.friendsAverageSteps = 1500;
sports.selfRank = 22;
List<String> list = new ArrayList<>();
for (int i = 23; i < 30; i++) {
list.add(i + "日");
}
sports.arrayString = list;
sports.championName = "安妮";
return sports;
}
@Override
public void lookChanmpionInfo(String chanmpionName) {
Toast.makeText(this,chanmpionName, 0).show();
}
}