您的位置:首页 > 其它

一篇学会自定义View

2016-05-30 20:57 609 查看
这里仅仅只对直接继承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();

}

}
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: