在做项目开发时,有个这样的需求:
就中间的那个支付明细,要求点击时能收缩,这个功能非常简单,从界面来看,用LinearLayout或TableLayout来做,没啥难度,但是如果是用布局来写的话,那么要写的可多了,这只是列出了几种支付方式,有可能还有更多的,也有可能没这么多,那么用这种方式来写,代码非常啰嗦,维护起来更麻烦,针对这种情况,我采用的是自定义控件来写,动态画出来这些文本,详细代码:
package com.example.viewtest; import java.util.LinkedHashMap; import java.util.Map; import android.content.Context; import android.content.res.Resources; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetrics; import android.graphics.Typeface; import android.util.AttributeSet; import android.util.TypedValue; import android.view.View; import android.view.View.OnClickListener; import android.view.ViewGroup; import android.widget.TextView; /** * 支付详情自定义控件 * <p> * 传入参数:Map<String,String> * <p> * key:支付方式,value:支付金额 * * @author xiec * */ public class PayDetailView extends View implements OnClickListener { Map<String, String> data; TextView tv_title; String title = "线上支付明细"; /** * 是否显示详情,标题点击时会切换 */ boolean isShow = true; /** * 自己的宽度 */ int width; /** * 自己的高度 */ int hight; /** * 字体大小 */ float textSize; Resources res; /** * 画笔 */ Paint p; FontMetrics fm; /** * 画笔粗细 */ float penSize = 3f; public PayDetailView(Context context) { super(context); init(); } public PayDetailView(Context context, AttributeSet attrs) { super(context, attrs); init(); } private void init() { res = getResources(); setBackgroundColor(res.getColor(R.color.white)); textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX, res.getDimension(R.dimen.textsize_middle), res.getDisplayMetrics()); p = new Paint(); p.setAntiAlias(true);// 设置抗锯齿F p.setStrokeWidth(penSize); p.setTextAlign(Paint.Align.LEFT); p.setTextSize(textSize); fm = p.getFontMetrics(); data = new LinkedHashMap<String, String>(); LogUtils.d("init width=" + width); this.setOnClickListener(this); } /** * 设置数据 * * @param map */ public void setData(Map<String, String> map) { // 设置数据 data.clear(); if (map != null) { // String[] tmp = new String[map.size()]; // map.keySet().toArray(tmp); // LogUtils.d(Arrays.toString(tmp)); data.putAll(map); } // 重绘 invalidate(); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { ViewGroup parent = (ViewGroup) getParent(); width = parent.getMeasuredWidth(); // 计算控件高度,根据数据来算 hight = (int) (textSize * 2); if (data != null && data.size() > 0 && isShow) { hight += (data.keySet().size() + 1) * textSize * 2; } setMeasuredDimension(width, hight); } @Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub LogUtils.d("onDraw isShow:" + isShow); drawTitle(canvas, isShow, p); if (isShow) { // 画值 drawCotent(canvas, data, p); } } /** * 画支付详情 * * @param data * 数据 * @param p * 画笔 */ private void drawCotent(Canvas canvas, Map<String, String> data, Paint p) { if (data != null && data.size() > 0) { int size = data.size(); float[] pts = new float[8 + size * 4]; // 画框 int ptsLen = pts.length; LogUtils.d("data size=" + size); LogUtils.d("pts size=" + ptsLen); // 竖线 pts[0] = (width - penSize) / 2; pts[1] = textSize * 2; pts[2] = (width - penSize) / 2; pts[3] = hight; pts[4] = 0; pts[5] = textSize * 4; pts[6] = width; pts[7] = textSize * 4; // 是否是左边 for (int i = 8; i < ptsLen; i += 4) { pts[i] = 0; pts[i + 1] = textSize * (i / 4 + 1) * 2; pts[i + 2] = width; pts[i + 3] = textSize * (i / 4 + 1) * 2; } p.setColor(res.getColor(R.color.black_90)); canvas.drawLines(pts, p); // 头部 // 支付方式 float tmp = getTextlen(p, "支付方式"); p.setColor(res.getColor(R.color.main_color)); // 设置粗体 Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); p.setTypeface(font); float d_x = (width / 2 - tmp) / 2; float d_y = textSize * 2 + (textSize - fm.descent + (fm.bottom - fm.top) / 2); canvas.drawText("支付方式", d_x, d_y, p); tmp = getTextlen(p, "金额"); d_x = (int) (width / 2 + ((width / 2 - tmp) / 2)); canvas.drawText("金额", d_x, d_y, p); size = data.keySet().size(); String[] payway = new String[size]; data.keySet().toArray(payway); // size = payway.length; font = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); p.setTypeface(font); for (int i = 0; i < size; i++) { // 每种支付方式都画出来 p.setColor(res.getColor(R.color.black)); tmp = getTextlen(p, payway[i]); d_x = (width / 2 - tmp) / 2; d_y = (textSize * (2 + i) * 2) + (textSize - fm.descent + (fm.bottom - fm.top) / 2); canvas.drawText(payway[i], d_x, d_y, p); p.setColor(res.getColor(R.color.tab_spinner_color)); String value = data.get(payway[i]); tmp = getTextlen(p, value); d_x = (int) (width / 2 + ((width / 2 - tmp) / 2)); canvas.drawText(value, d_x, d_y, p); } } } // 画标题 private void drawTitle(Canvas canvas, boolean isShow, Paint p) { LogUtils.d("drawTitle isShow:" + isShow); Resources res = getResources(); p.setColor(res.getColor(R.color.msdcolorbg));// 设置红色 p.setStyle(Paint.Style.FILL);// 设置填满 // 画矩形 canvas.drawRect(0, 0, width, textSize * 2, p);// 长方形 p.setColor(res.getColor(R.color.white)); // 画右边箭头 drawArrow(canvas, isShow, p); // 计算文字长度 float textlen = getTextlen(p, title); float d_x = (width - textlen) / 2; float d_y = textSize - fm.descent + (fm.bottom - fm.top) / 2; canvas.drawText(title, d_x, d_y, p); } /** * 画右上角箭头 * * @param canvas * @param isShow * @param p */ private void drawArrow(Canvas canvas, boolean isShow, Paint p) { float size = textSize; float d_x = width - size - size / 2; float d_y = textSize / 2; // 大小固定在80px // {x0,y0,x1,y1,x2,y2},总共四个点,八个坐标 float[] pts = new float[8]; if (isShow) { // 向下箭头 pts[0] = d_x; pts[1] = d_y; pts[2] = d_x + size / 2; pts[3] = d_y + size / 2; pts[4] = d_x + size / 2; pts[5] = d_y + size / 2; pts[6] = d_x + size; pts[7] = d_y; } else { // 向上箭头 pts[0] = d_x; pts[1] = d_y + size / 2; pts[2] = d_x + size / 2; pts[3] = d_y; pts[4] = d_x + size / 2; pts[5] = d_y; pts[6] = d_x + size; pts[7] = d_y + size / 2; } canvas.drawLines(pts, p); } /** * 计算文本长度 * * @param p * @param text * @return */ private float getTextlen(Paint p, String text) { if (text == null) { return 0; } return p.measureText(text); } @Override public void onClick(View v) { // 单击时隐藏支付详情 LogUtils.d("onClick isShow=" + isShow); isShow = !isShow; // invalidate(); requestLayout(); } }
下面对一些地方作补充:
首先,支付方式是不固定的,在解析完成后,以Map<String,String>的形式传给这个View:
/** * 设置数据 * * @param map */ public void setData(Map<String, String> map) { // 设置数据 data.clear(); if (map != null) { // String[] tmp = new String[map.size()]; // map.keySet().toArray(tmp); // LogUtils.d(Arrays.toString(tmp)); data.putAll(map); } // 重绘 invalidate(); }
当收到Map数据后,先清下原数据,再加载数据,再重新绘制界面,会调用onDraw方法,这里Map采用的是LinkedHashMap
,使用LinkedHashMap目的是使传入的Map按顺序来画出来。
重点是在onDraw方法,
@Override protected void onDraw(Canvas canvas) { // TODO Auto-generated method stub LogUtils.d("onDraw isShow:" + isShow); drawTitle(canvas, isShow, p); if (isShow) { // 画值 drawCotent(canvas, data, p); } }
这个方法里很简单,首先是画标题,再看下是否要画内容区域(有个收缩功能),如果是缩的状态,就不用往下走了。isShow这个变量在点击的时候进行切换
@Override public void onClick(View v) { // 单击时隐藏支付详情 LogUtils.d("onClick isShow=" + isShow); isShow = !isShow; // invalidate(); requestLayout(); }
点击时,要求重新布局:requestLayout();
重新布局的时候会重新计算控件的大小(onMeasure()),并重新绘制界面,关于控件的大小计算,我这里的宽,用的是父布局的宽:
ViewGroup parent = (ViewGroup) getParent();
width = parent.getMeasuredWidth();
要注意一下,getParent()要强转成ViewGroup。关键是高度的计算:
// 计算控件高度,根据数据来算
hight = (int) (textSize * 2);
if (data != null && data.size() > 0 && isShow) {
hight += (data.keySet().size() + 1) * textSize * 2;
}
setMeasuredDimension(width, hight);
我这里高度计算是根据传入的Map的大小来算的,首先头部高度是固定的,我用的是2倍的字体大小,字体大小是从dime中获取的
textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
res.getDimension(R.dimen.textsize_middle),
res.getDisplayMetrics());
关键是setMeasuredDimension方法,使用重新布局时控件大小重新布局。
再回到onDraw方法来,drawTitle方法是画头部,即 线上支付明细
记住一条:在onDraw方法中,不要用new来新建对象,不然会有警告:onDraw会常调用,在这里用new来create对象,内存会使用比较多。
drawTitle代码就不贴了,上面有,只是简单说一下,有几个问题需要注意:
一、文字居中,
canvas.drawText有四个参数,特别是第二个参数和第三个参数,是决定这个文本从哪个地方开始写的,
// 计算文字长度
float textlen = getTextlen(p, title);
float d_x = (width - textlen) / 2;
float d_y = textSize - fm.descent + (fm.bottom - fm.top) / 2;
canvas.drawText(title, d_x, d_y, p);
居中的方法:(总长度-文本长度)/2
文本长度的获取:
ps:这里的Paint参数,最好是全局的,不然会出现计算出来的文本长度与预期的不一样,原因就是Paint使用的不是同一个对象
/** * 计算文本长度 * * @param p * @param text * @return */ private float getTextlen(Paint p, String text) { if (text == null) { return 0; } return p.measureText(text); }
还有一个d_y的计算(可以理解成文本的y轴),Android比较坑的是:这个文本的y轴,有点难理解,drawText画文本是基于baseLine来画的,什么是baseLine?见下图:
因此,这个d_y不能像d_x那样计算,得用FontMetrics来计算,计算方法:
fm = p.getFontMetrics();//获取FontMetrice对象,根据Paint对象来获取
float d_y = textSize - fm.descent + (fm.bottom - fm.top) / 2;
//控件的高度-fm.descent+(fm.bottom - fm.top) / 2;
写完头部文本后,再画上一个箭头(其实就是两条线段组成,利用drawLine方法,参数是一个float[]数组,这个一维数据{x0,y0,x1,y1,x2,y2,x3,y3....},这有两条线段,所以得要8个坐标):
/** * 画右上角箭头 * * @param canvas * @param isShow * @param p */ private void drawArrow(Canvas canvas, boolean isShow, Paint p) { float size = textSize; float d_x = width - size - size / 2; float d_y = textSize / 2; // 大小固定在80px // {x0,y0,x1,y1,x2,y2},总共四个点,八个坐标 float[] pts = new float[8]; if (isShow) { // 向下箭头 pts[0] = d_x; pts[1] = d_y; pts[2] = d_x + size / 2; pts[3] = d_y + size / 2; pts[4] = d_x + size / 2; pts[5] = d_y + size / 2; pts[6] = d_x + size; pts[7] = d_y; } else { // 向上箭头 pts[0] = d_x; pts[1] = d_y + size / 2; pts[2] = d_x + size / 2; pts[3] = d_y; pts[4] = d_x + size / 2; pts[5] = d_y; pts[6] = d_x + size; pts[7] = d_y + size / 2; } canvas.drawLines(pts, p); }
画完头部,再根据isShow来画Content:
内容区域分两部分来画:
黑线组成的框和文字部分,
先画框:框是由线段组成,根据data.size来决定有多少条横线,竖线就一条,用float[]数组来存组成线段的点的坐标。
int size = data.size(); float[] pts = new float[8 + size * 4]; // 画框 int ptsLen = pts.length; LogUtils.d("data size=" + size); LogUtils.d("pts size=" + ptsLen); // 竖线 pts[0] = (width - penSize) / 2; pts[1] = textSize * 2; pts[2] = (width - penSize) / 2; pts[3] = hight; pts[4] = 0; pts[5] = textSize * 4; pts[6] = width; pts[7] = textSize * 4; // 是否是左边 for (int i = 8; i < ptsLen; i += 4) { pts[i] = 0; pts[i + 1] = textSize * (i / 4 + 1) * 2; pts[i + 2] = width; pts[i + 3] = textSize * (i / 4 + 1) * 2; } p.setColor(res.getColor(R.color.black_90)); canvas.drawLines(pts, p);
画完线后,再写文本,这里需要注意的就是文本的坐标,d_x,d_y的计算,d_x好说,关键是d_y的值,详情见代码注释,
// 头部 // 支付方式 float tmp = getTextlen(p, "支付方式"); p.setColor(res.getColor(R.color.main_color)); // 设置粗体 Typeface font = Typeface.create(Typeface.SANS_SERIF, Typeface.BOLD); p.setTypeface(font); float d_x = (width / 2 - tmp) / 2; float d_y = textSize * 2 + (textSize - fm.descent + (fm.bottom - fm.top) / 2); canvas.drawText("支付方式", d_x, d_y, p); tmp = getTextlen(p, "金额"); d_x = (int) (width / 2 + ((width / 2 - tmp) / 2)); canvas.drawText("金额", d_x, d_y, p); size = data.keySet().size(); String[] payway = new String[size]; data.keySet().toArray(payway); // size = payway.length; font = Typeface.create(Typeface.SANS_SERIF, Typeface.NORMAL); p.setTypeface(font); for (int i = 0; i < size; i++) { // 每种支付方式都画出来 p.setColor(res.getColor(R.color.black)); tmp = getTextlen(p, payway[i]); d_x = (width / 2 - tmp) / 2; d_y = (textSize * (2 + i) * 2) + (textSize - fm.descent + (fm.bottom - fm.top) / 2); canvas.drawText(payway[i], d_x, d_y, p); p.setColor(res.getColor(R.color.tab_spinner_color)); String value = data.get(payway[i]); tmp = getTextlen(p, value); d_x = (int) (width / 2 + ((width / 2 - tmp) / 2)); canvas.drawText(value, d_x, d_y, p); }
基本情况就这样
源码下载:http://download.csdn.net/detail/ytmfdw/9518286