本文介绍Android平台飞行航线剖面图自定义控件绘制的实现,给出实现效果截图,Java和Kotlin具体实现代码。
一、实现效果
0m水平线为起点的地形高度,起点和降落点图标固定绘制,途经点超过6个的时候用正方形色块表示,避免途径点过多的时候显示图标的宽度不足,地形区域使用渐变色填充。
二、Java代码实现
代码详细实现逻辑已在代码注释中说明,此处不做赘述。自定义控件的实现思路其实很简单,就是先拆分,再组合。整个剖面图拆成6个部分,布局背景、地形区域、Y轴虚线和文本标签、航线路线和航点图标,拆分之后独立绘制组合起来就是完整的剖面图。
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.CornerPathEffect;
import android.graphics.DashPathEffect;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Shader;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.List;
/**
* 航线剖面图自定义View
*
* @author chenwenguan
*/
public class NaviLineProfileView extends View implements SkinSupport {
private Paint mPaint = null;
private NaviLineProfileObj mProfileObj = null;
private Path mTerrainPath = null;
// Y轴水平虚线间距
private final int DASH_GAP = 2;
// Y轴水平虚线长度
private final int DASH_LENGTH = 2;
private final int TERRIAN_LINE_STROKE_WIDTH = 2;
private int mProfileViewWidth, mProfileViewHeight, mYMarginLeft, mProfileRouteWidth, mProfileTerrainWidth, mProfileHeight, mIconRadius, mTextSize, mWayPointRadius;
private int mColorProfileMapBackground;
private int mColorProfileMapYOriginal;
private int mColorProfileMapTerrainLine;
private int mColorProfileMapTerrainArea;
private int mColorProfileMapTerrainAreaEnd;
private int mColorProfileMapRouteLine;
private int mColorProfileMapRouteWaypoint;
public NaviLineProfileView(Context context) {
this(context, null);
}
public NaviLineProfileView(Context context, @Nullable AttributeSet attrs) {
this(context, attrs, -1);
}
public NaviLineProfileView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, -1);
}
public NaviLineProfileView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
initObj();
initColor();
}
private void initObj() {
mPaint = new Paint();
mPaint.setAntiAlias(true);
mTerrainPath = new Path();
mProfileViewWidth = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_width);
mProfileViewHeight = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_height);
mYMarginLeft = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_original_margin_left);
mProfileRouteWidth = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_route_x_width);
mProfileTerrainWidth = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_terrain_x_width);
mProfileHeight = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_height);
mIconRadius = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_lifting_down_icon_radius);
mTextSize = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_original_text_size);
mWayPointRadius = getResources().getDimensionPixelSize(R.dimen.route_naviline_profile_view_way_point_radius);
}
private void initData(int width, int height) {
}
public void setProfileData(NaviLineProfileObj profileData) {
this.mProfileObj = profileData;
invalidate();
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
SkinManager.INSTANCE.addListener(this);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
SkinManager.INSTANCE.removeListener(this);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// 获取测量模式和尺寸
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 计算出宽度和高度(默认大小,可以自己设定)
int desiredWidth = mProfileViewWidth;
int desiredHeight = mProfileViewHeight;
// 根据测量模式分别处理大小
int width;
if (widthMode == MeasureSpec.EXACTLY) {
width = MeasureSpec.getSize(widthMeasureSpec); // MATCH_PARENT 或具体值
} else if (widthMode == MeasureSpec.AT_MOST) {
width = Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec)); // WRAP_CONTENT
} else {
width = desiredWidth; // UNSPECIFIED,自定义默认尺寸
}
int height;
if (heightMode == MeasureSpec.EXACTLY) {
height = MeasureSpec.getSize(heightMeasureSpec); // MATCH_PARENT 或具体值
} else if (heightMode == MeasureSpec.AT_MOST) {
height = Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec)); // WRAP_CONTENT
} else {
height = desiredHeight; // UNSPECIFIED,自定义默认尺寸
}
// 设置测量后的宽高
setMeasuredDimension(width, height);
initData(width, height);
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
drawBackground(canvas);
drawTerrain(canvas);
drawYOriginalText(canvas);
drawYOriginal(canvas);
drawRouteLine(canvas);
drawRoutePoint(canvas);
}
/**
* 绘制剖面图背景
*
* @param canvas
*/
private void drawBackground(Canvas canvas) {
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setColor(mColorProfileMapBackground);
canvas.drawRect(0, 0, getMeasuredWidth(), getMeasuredHeight(), mPaint);
}
/**
* 绘制Y轴起始水平虚线
*
* @param canvas
*/
private void drawYOriginal(Canvas canvas) {
if (mProfileObj == null) {
return;
}
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setColor(mColorProfileMapYOriginal);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setStrokeWidth(1.5f);
mPaint.setPathEffect(new DashPathEffect(new float[]{DASH_GAP, DASH_LENGTH,}, 0));
float startX = mYMarginLeft + getPaddingLeft();
float profileDrawHeight = mProfileHeight;
float startY = profileDrawHeight + getPaddingTop();
if (mProfileObj.getOriginalY() != 0) {
NaviLineProfileData terrainStart = mProfileObj.getTerrainPointList().get(0);
double startYLength = terrainStart.getHeight() - mProfileObj.getOriginalY();
double totalYLength = mProfileObj.getYDistance() - mProfileObj.getOriginalY();
startY = (float) (profileDrawHeight * (totalYLength - startYLength) / totalYLength) + getPaddingTop();
}
float endX = getMeasuredWidth() - getPaddingRight();
float endY = startY;
canvas.drawLine(startX, startY, endX, endY, mPaint);
}
/**
* 绘制Y轴起始坐标文本标签
*
* @param canvas
*/
private void drawYOriginalText(Canvas canvas) {
if (mProfileObj == null) {
return;
}
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setColor(mColorProfileMapYOriginal);
mPaint.setTextSize(mTextSize);
float profileDrawHeight = mProfileHeight;
float startY = profileDrawHeight + getPaddingTop();
if (mProfileObj.getOriginalY() != 0) {
NaviLineProfileData terrainStart = mProfileObj.getTerrainPointList().get(0);
double startYLength = terrainStart.getHeight() - mProfileObj.getOriginalY();
double totalYLength = mProfileObj.getYDistance() - mProfileObj.getOriginalY();
startY = (float) (profileDrawHeight * (totalYLength - startYLength) / totalYLength) + getPaddingTop();
}
String yOriginalTxt = "0 m";
canvas.drawText(yOriginalTxt, getPaddingLeft(), startY, mPaint);
}
/**
* 绘制地形
*
* @param canvas
*/
private void drawTerrain(Canvas canvas) {
if (mProfileObj == null) {
return;
}
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(TERRIAN_LINE_STROKE_WIDTH);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setPathEffect(new CornerPathEffect(5));
mPaint.setColor(mColorProfileMapTerrainLine);
// 绘制地形曲线
List<NaviLineProfileData> terrainList = mProfileObj.getTerrainPointList();
double totalYLength = mProfileObj.getYDistance() - mProfileObj.getOriginalY();
double totalXLength = mProfileObj.getXTerrainDistance();
float profileDrawHeight = mProfileHeight;
float profileDrawWidth = mProfileTerrainWidth;
int size = terrainList.size();
float startX = getPaddingLeft() + mYMarginLeft, startY = profileDrawHeight + getPaddingTop();
mTerrainPath.reset();
mTerrainPath.moveTo(startX, startY);
NaviLineProfileData terrainItem = null;
// 如果有航点低于起始点,需要连接最底部两边的点形成封闭区域
float originalPointX = getPaddingLeft() + mYMarginLeft;
float finalPointX = originalPointX + profileDrawWidth;
// 记录地形渐变区域顶部参数
float gradiantTop = 0;
for (int i = 0; i < size; i++) {
terrainItem = terrainList.get(i);
double startYLength = terrainItem.getHeight() - mProfileObj.getOriginalY();
startY = (float) (profileDrawHeight * (totalYLength - startYLength) / totalYLength) + getPaddingTop();
if (gradiantTop == 0) {
gradiantTop = startY;
} else {
gradiantTop = Math.min(gradiantTop, startY);
}
if (i == 0) {
startX += mIconRadius;
mTerrainPath.lineTo(startX, startY);
} else {
startX += profileDrawWidth * terrainItem.getDistance() / totalXLength;
mTerrainPath.lineTo(startX, startY);
}
}
// 连接最左和最右两个端点
mTerrainPath.lineTo(finalPointX + mIconRadius * 2, profileDrawHeight + getPaddingTop());
mTerrainPath.lineTo(originalPointX, profileDrawHeight + getPaddingTop());
canvas.drawPath(mTerrainPath, mPaint);
// 绘制地形渐变区域
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setStyle(Paint.Style.FILL);
float gradiantX = originalPointX + (finalPointX - originalPointX) / 2;
int[] colorArray = new int[]{mColorProfileMapTerrainArea, mColorProfileMapTerrainArea, mColorProfileMapTerrainAreaEnd};
float[] positionArray = new float[]{0f, 0.7f, 1.0f};
LinearGradient terrainGradient = new LinearGradient(gradiantX, gradiantTop, gradiantX, profileDrawHeight + getPaddingTop() + 2, colorArray, positionArray, Shader.TileMode.CLAMP);
mPaint.setShader(terrainGradient);
canvas.drawPath(mTerrainPath, mPaint);
// 绘制覆盖底部线条
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(2f);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mColorProfileMapBackground);
canvas.drawLine(originalPointX, profileDrawHeight + getPaddingTop(), finalPointX + mIconRadius * 2 - 2, profileDrawHeight + getPaddingTop(), mPaint);
}
/**
* 绘制航点线段
*/
private void drawRouteLine(Canvas canvas) {
if (mProfileObj == null) {
return;
}
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setStrokeWidth(1f);
mPaint.setStyle(Paint.Style.STROKE);
mPaint.setColor(mColorProfileMapRouteLine);
// 绘制地形曲线
List<NaviLineProfileData> routeList = mProfileObj.getRoutePointList();
double totalYLength = mProfileObj.getYDistance() - mProfileObj.getOriginalY();
double totalXLength = mProfileObj.getXRouteDistance();
float profileDrawHeight = mProfileHeight;
float profileDrawWidth = mProfileRouteWidth;
float originalPointX = getPaddingLeft() + mYMarginLeft + mIconRadius;
int routeSize = routeList.size();
float startX = 0, startY = 0;
float preX = 0, preY = 0;
for (int i = 0; i < routeSize; i++) {
NaviLineProfileData routeItem = routeList.get(i);
double yLength = routeItem.getHeight() - mProfileObj.getOriginalY();
startY = (float) (profileDrawHeight * (totalYLength - yLength) / totalYLength) + getPaddingTop();
if (i == 0) {
startX = originalPointX;
} else {
startX += profileDrawWidth * routeItem.getDistance() / totalXLength;
}
if (i > 0) {
canvas.drawLine(preX, preY, startX + mIconRadius, startY, mPaint);
}
if (i == 0) {
preX = startX;
} else {
// 起点之后的点都从图标右侧开始,所以要加上半个起点图标的偏移量,避免途经点压盖
preX = startX + mIconRadius;
}
preY = startY;
}
}
/**
* 绘制航点
*/
public void drawRoutePoint(Canvas canvas) {
if (mProfileObj == null) {
return;
}
mPaint.reset();
mPaint.setAntiAlias(true);
mPaint.setColor(Color.BLACK);
// 绘制地形曲线
List<NaviLineProfileData> routeList = mProfileObj.getRoutePointList();
double totalYLength = mProfileObj.getYDistance() - mProfileObj.getOriginalY();
double totalXLength = mProfileObj.getXRouteDistance();
float profileDrawHeight = mProfileHeight;
float profileDrawWidth = mProfileRouteWidth;
float originalPointX = getPaddingLeft() + mYMarginLeft + mIconRadius;
int routeSize = routeList.size();
float startX = 0, startY = 0;
for (int i = 0; i < routeSize; i++) {
NaviLineProfileData routeItem = routeList.get(i);
double yLength = routeItem.getHeight() - mProfileObj.getOriginalY();
startY = (float) (profileDrawHeight * (totalYLength - yLength) / totalYLength) + getPaddingTop();
if (i == 0) {
startX = originalPointX;
} else {
startX += profileDrawWidth * routeItem.getDistance() / totalXLength;
}
// 绘制起终点
if (i == 0 || i == (routeSize - 1)) {
mPaint.setColor(Color.BLACK);
Bitmap pointBmp = null;
if (i == 0) {
pointBmp = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_profile_map_start);
} else {
pointBmp = BitmapFactory.decodeResource(getResources(), R.mipmap.ic_profile_map_end);
}
if (i == 0) {
canvas.drawBitmap(pointBmp, startX - mIconRadius, startY - mIconRadius, mPaint);
} else {
// 起点之后的点都从图标右侧开始,所以X轴参数要加上半个起点图标的偏移量,避免途经点压盖
canvas.drawBitmap(pointBmp, startX + mIconRadius, startY - mIconRadius, mPaint);
}
} else {
// 绘制途经点,航点超过8个就用正方形色块表示,避免显示控件不足。
if (routeList.size() > 8) {
mPaint.setColor(mColorProfileMapRouteWaypoint);
mPaint.setStyle(Paint.Style.FILL);
canvas.drawRect(mIconRadius + startX - mWayPointRadius, startY - mWayPointRadius, mIconRadius + startX + mWayPointRadius, startY + mWayPointRadius, mPaint);
} else {
String wayPointResId = "ic_profile_map_way_point_" + i;
int wayPointRes = ContextInitial.getResourceId(wayPointResId, ContextInitial.TYPE_MIPMAP);
Bitmap pointBmp = BitmapFactory.decodeResource(getResources(), wayPointRes);
// 起点之后的点都从图标右侧开始,所以X轴参数要加上半个起点图标的偏移量,避免途经点压盖
canvas.drawBitmap(pointBmp, startX, startY - mIconRadius, mPaint);
}
}
}
}
@Override
public void applySkin() {
initColor();
invalidate();
}
private void initColor() {
if (SkinManager.INSTANCE.currentSkin() == SkinManager.SKIN_SATELLITE) {
mColorProfileMapBackground = getResources().getColor(R.color.color_CCFAFBFF_skin_satellite);
mColorProfileMapYOriginal = getResources().getColor(R.color.color_CC000000_skin_satellite);
mColorProfileMapTerrainLine = getResources().getColor(R.color.color_7C8DA2_skin_satellite);
mColorProfileMapTerrainArea = getResources().getColor(R.color.color_9CB0C5_skin_satellite);
mColorProfileMapTerrainAreaEnd = getResources().getColor(R.color.color_D8D8D8_skin_satellite);
mColorProfileMapRouteLine = getResources().getColor(R.color.color_CC0D0D15_skin_satellite);
mColorProfileMapRouteWaypoint = getResources().getColor(R.color.color_0D0D15_skin_satellite);
} else {
mColorProfileMapBackground = getResources().getColor(R.color.color_CCFAFBFF_skin);
mColorProfileMapYOriginal = getResources().getColor(R.color.color_CC000000_skin);
mColorProfileMapTerrainLine = getResources().getColor(R.color.color_7C8DA2_skin);
mColorProfileMapTerrainArea = getResources().getColor(R.color.color_9CB0C5_skin);
mColorProfileMapTerrainAreaEnd = getResources().getColor(R.color.color_D8D8D8_skin);
mColorProfileMapRouteLine = getResources().getColor(R.color.color_CC0D0D15_skin);
mColorProfileMapRouteWaypoint = getResources().getColor(R.color.color_0D0D15_skin);
}
}
}
在dimens.xml 中添加尺寸参数
<item name="route_naviline_profile_view_width" type="dimen">400dp</item>
<item name="route_naviline_profile_view_height" type="dimen">226dp</item>
<item name="route_naviline_profile_view_y_original_margin_left" type="dimen">41dp</item>
<item name="route_naviline_profile_view_y_height" type="dimen">178dp</item>
<item name="route_naviline_profile_view_route_x_width" type="dimen">262dp</item>
<item name="route_naviline_profile_view_terrain_x_width" type="dimen">286dp</item>
<item name="route_naviline_profile_view_lifting_down_icon_radius" type="dimen">12dp</item>
<item name="route_naviline_profile_view_y_original_text_size" type="dimen">16dp</item>
<item name="route_naviline_profile_view_way_point_radius" type="dimen">2dp</item>
getResourceId的实现方法如下,其中typeName传图片资源所在的文件夹,如果是放在drawable-xxxx目录下就传“drawable”,如果是放在mipmap-xxxx目录下就传“mipmap”:
public static int getResourceId(String resourceName, String typeName) {
if (context == null) {
throw new IllegalArgumentException("ContextUtils sContext should not be null");
}
return context.getResources().getIdentifier(resourceName, typeName, context.getPackageName());
}
SkinManager为换肤的处理,不需要可以去掉。
NaviLineProfileObj对象包含的字段如下,routePointList为航线航点的列表数据,terrainPointList为航线地形列表数据,xRouteDistance是航线列表每个点距离相加总长度,xTerrainDistance为地形列表每个点相加总长度,yDistance为航线最高点的海拔高度,originalY为航线最低点的海拔高度。
NaviLineProfileData只需要海拔高度和距离参数,用于计算绘制剖面图在X轴和Y轴相对的偏移量。
data class NaviLineProfileObj(val routePointList: List<NaviLineProfileData>, val terrainPointList: List<NaviLineProfileData>, val xRouteDistance: Double, val xTerrainDistance: Double, val yDistance: Double, val originalY: Double)
data class NaviLineProfileData(val height: Double, val distance: Double)
三、Kotlin代码实现
class NaviLineProfileView(
context: Context?,
@Nullable attrs: AttributeSet?,
defStyleAttr: Int,
defStyleRes: Int
) :
View(context, attrs, defStyleAttr, defStyleRes), SkinSupport {
private var mPaint: Paint? = null
private var mProfileObj: NaviLineProfileObj? = null
private var mTerrainPath: Path? = null
// Y轴水平虚线间距
private val DASH_GAP = 2
// Y轴水平虚线长度
private val DASH_LENGTH = 2
private val TERRIAN_LINE_STROKE_WIDTH = 2
private var mProfileViewWidth = 0
private var mProfileViewHeight = 0
private var mYMarginLeft = 0
private var mProfileRouteWidth = 0
private var mProfileTerrainWidth = 0
private var mProfileHeight = 0
private var mIconRadius = 0
private var mTextSize = 0
private var mWayPointRadius = 0
private var mColorProfileMapBackground = 0
private var mColorProfileMapYOriginal = 0
private var mColorProfileMapTerrainLine = 0
private var mColorProfileMapTerrainArea = 0
private var mColorProfileMapTerrainAreaEnd = 0
private var mColorProfileMapRouteLine = 0
private var mColorProfileMapRouteWaypoint = 0
constructor(context: Context?) : this(context, null)
constructor(context: Context?, @Nullable attrs: AttributeSet?) : this(context, attrs, -1)
constructor(
context: Context?,
@Nullable attrs: AttributeSet?,
defStyleAttr: Int
) : this(context, attrs, defStyleAttr, -1)
init {
initObj()
initColor()
}
private fun initObj() {
mPaint = Paint()
mPaint!!.isAntiAlias = true
mTerrainPath = Path()
mProfileViewWidth =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_width)
mProfileViewHeight =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_height)
mYMarginLeft =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_original_margin_left)
mProfileRouteWidth =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_route_x_width)
mProfileTerrainWidth =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_terrain_x_width)
mProfileHeight =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_height)
mIconRadius =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_lifting_down_icon_radius)
mTextSize =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_y_original_text_size)
mWayPointRadius =
resources.getDimensionPixelSize(R.dimen.route_naviline_profile_view_way_point_radius)
}
private fun initData(width: Int, height: Int) {}
fun setProfileData(profileData: NaviLineProfileObj?) {
mProfileObj = profileData
invalidate()
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
addListener(this)
}
override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
removeListener(this)
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
// 获取测量模式和尺寸
val widthMode = MeasureSpec.getMode(widthMeasureSpec)
val heightMode = MeasureSpec.getMode(heightMeasureSpec)
// 计算出宽度和高度(默认大小,可以自己设定)
val desiredWidth = mProfileViewWidth
val desiredHeight = mProfileViewHeight
// 根据测量模式分别处理大小
val width: Int
width = if (widthMode == MeasureSpec.EXACTLY) {
MeasureSpec.getSize(widthMeasureSpec) // MATCH_PARENT 或具体值
} else if (widthMode == MeasureSpec.AT_MOST) {
Math.min(desiredWidth, MeasureSpec.getSize(widthMeasureSpec)) // WRAP_CONTENT
} else {
desiredWidth // UNSPECIFIED,自定义默认尺寸
}
val height: Int
height = if (heightMode == MeasureSpec.EXACTLY) {
MeasureSpec.getSize(heightMeasureSpec) // MATCH_PARENT 或具体值
} else if (heightMode == MeasureSpec.AT_MOST) {
Math.min(desiredHeight, MeasureSpec.getSize(heightMeasureSpec)) // WRAP_CONTENT
} else {
desiredHeight // UNSPECIFIED,自定义默认尺寸
}
// 设置测量后的宽高
setMeasuredDimension(width, height)
initData(width, height)
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
drawBackground(canvas)
drawTerrain(canvas)
drawYOriginalText(canvas)
drawYOriginal(canvas)
drawRouteLine(canvas)
drawRoutePoint(canvas)
}
/**
* 绘制剖面图背景
*
* @param canvas
*/
private fun drawBackground(canvas: Canvas) {
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.color = mColorProfileMapBackground
canvas.drawRect(
0f, 0f, measuredWidth.toFloat(), measuredHeight.toFloat(),
mPaint!!
)
}
/**
* 绘制Y轴起始水平虚线
*
* @param canvas
*/
private fun drawYOriginal(canvas: Canvas) {
if (mProfileObj == null) {
return
}
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.color = mColorProfileMapYOriginal
mPaint!!.style = Paint.Style.STROKE
mPaint!!.strokeWidth = 1.5f
mPaint!!.pathEffect =
DashPathEffect(floatArrayOf(DASH_GAP.toFloat(), DASH_LENGTH.toFloat()), 0f)
val startX = (mYMarginLeft + paddingLeft).toFloat()
val profileDrawHeight = mProfileHeight.toFloat()
var startY = profileDrawHeight + paddingTop
if (mProfileObj!!.originalY != 0.0) {
val (height) = mProfileObj!!.terrainPointList[0]
val startYLength = height - mProfileObj!!.originalY
val totalYLength = mProfileObj!!.yDistance - mProfileObj!!.originalY
startY =
(profileDrawHeight * (totalYLength - startYLength) / totalYLength).toFloat() + paddingTop
}
val endX = (measuredWidth - paddingRight).toFloat()
val endY = startY
canvas.drawLine(startX, startY, endX, endY, mPaint!!)
}
/**
* 绘制Y轴起始坐标文本标签
*
* @param canvas
*/
private fun drawYOriginalText(canvas: Canvas) {
if (mProfileObj == null) {
return
}
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.color = mColorProfileMapYOriginal
mPaint!!.textSize = mTextSize.toFloat()
val profileDrawHeight = mProfileHeight.toFloat()
var startY = profileDrawHeight + paddingTop
if (mProfileObj!!.originalY != 0.0) {
val (height) = mProfileObj!!.terrainPointList[0]
val startYLength = height - mProfileObj!!.originalY
val totalYLength = mProfileObj!!.yDistance - mProfileObj!!.originalY
startY =
(profileDrawHeight * (totalYLength - startYLength) / totalYLength).toFloat() + paddingTop
}
val yOriginalTxt = "0 m"
canvas.drawText(yOriginalTxt, paddingLeft.toFloat(), startY, mPaint!!)
}
/**
* 绘制地形
*
* @param canvas
*/
private fun drawTerrain(canvas: Canvas) {
if (mProfileObj == null) {
return
}
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.strokeWidth = TERRIAN_LINE_STROKE_WIDTH.toFloat()
mPaint!!.style = Paint.Style.STROKE
mPaint!!.pathEffect = CornerPathEffect(5f)
mPaint!!.color = mColorProfileMapTerrainLine
// 绘制地形曲线
val terrainList = mProfileObj!!.terrainPointList
val totalYLength = mProfileObj!!.yDistance - mProfileObj!!.originalY
val totalXLength = mProfileObj!!.xTerrainDistance
val profileDrawHeight = mProfileHeight.toFloat()
val profileDrawWidth = mProfileTerrainWidth.toFloat()
val size = terrainList.size
var startX = (paddingLeft + mYMarginLeft).toFloat()
var startY = profileDrawHeight + paddingTop
mTerrainPath.reset()
mTerrainPath.moveTo(startX, startY)
var terrainItem: NaviLineProfileData? = null
// 如果有航点低于起始点,需要连接最底部两边的点形成封闭区域
val originalPointX = (paddingLeft + mYMarginLeft).toFloat()
val finalPointX = originalPointX + profileDrawWidth
// 记录地形渐变区域顶部参数
var gradiantTop = 0f
for (i in 0 until size) {
terrainItem = terrainList[i]
val startYLength = terrainItem.height - mProfileObj!!.originalY
startY =
(profileDrawHeight * (totalYLength - startYLength) / totalYLength).toFloat() + paddingTop
gradiantTop = if (gradiantTop == 0f) {
startY
} else {
Math.min(gradiantTop, startY)
}
if (i == 0) {
startX += mIconRadius.toFloat()
mTerrainPath.lineTo(startX, startY)
} else {
startX += (profileDrawWidth * terrainItem.distance / totalXLength).toFloat()
mTerrainPath.lineTo(startX, startY)
}
}
// 连接最左和最右两个端点
mTerrainPath.lineTo(finalPointX + mIconRadius * 2, profileDrawHeight + paddingTop)
mTerrainPath.lineTo(originalPointX, profileDrawHeight + paddingTop)
canvas.drawPath(mTerrainPath, mPaint!!)
// 绘制地形渐变区域
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.style = Paint.Style.FILL
val gradiantX = originalPointX + (finalPointX - originalPointX) / 2
val colorArray = intArrayOf(
mColorProfileMapTerrainArea,
mColorProfileMapTerrainArea,
mColorProfileMapTerrainAreaEnd
)
val positionArray = floatArrayOf(0f, 0.7f, 1.0f)
val terrainGradient = LinearGradient(
gradiantX,
gradiantTop,
gradiantX,
profileDrawHeight + paddingTop + 2,
colorArray,
positionArray,
Shader.TileMode.CLAMP
)
mPaint!!.shader = terrainGradient
canvas.drawPath(mTerrainPath, mPaint!!)
// 绘制覆盖底部线条
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.strokeWidth = 2f
mPaint!!.style = Paint.Style.STROKE
mPaint!!.color = mColorProfileMapBackground
canvas.drawLine(
originalPointX,
profileDrawHeight + paddingTop,
finalPointX + mIconRadius * 2 - 2,
profileDrawHeight + paddingTop,
mPaint!!
)
}
/**
* 绘制航点线段
*/
private fun drawRouteLine(canvas: Canvas) {
if (mProfileObj == null) {
return
}
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.strokeWidth = 1f
mPaint!!.style = Paint.Style.STROKE
mPaint!!.color = mColorProfileMapRouteLine
// 绘制地形曲线
val routeList = mProfileObj!!.routePointList
val totalYLength = mProfileObj!!.yDistance - mProfileObj!!.originalY
val totalXLength = mProfileObj!!.xRouteDistance
val profileDrawHeight = mProfileHeight.toFloat()
val profileDrawWidth = mProfileRouteWidth.toFloat()
val originalPointX = (paddingLeft + mYMarginLeft + mIconRadius).toFloat()
val routeSize = routeList.size
var startX = 0f
var startY = 0f
var preX = 0f
var preY = 0f
for (i in 0 until routeSize) {
val (height, distance) = routeList[i]
val yLength = height - mProfileObj!!.originalY
startY =
(profileDrawHeight * (totalYLength - yLength) / totalYLength).toFloat() + paddingTop
if (i == 0) {
startX = originalPointX
} else {
startX += (profileDrawWidth * distance / totalXLength).toFloat()
}
if (i > 0) {
canvas.drawLine(preX, preY, startX + mIconRadius, startY, mPaint!!)
}
preX = if (i == 0) {
startX
} else {
// 起点之后的点都从图标右侧开始,所以要加上半个起点图标的偏移量,避免途经点压盖
startX + mIconRadius
}
preY = startY
}
}
/**
* 绘制航点
*/
fun drawRoutePoint(canvas: Canvas) {
if (mProfileObj == null) {
return
}
mPaint!!.reset()
mPaint!!.isAntiAlias = true
mPaint!!.color = Color.BLACK
// 绘制地形曲线
val routeList = mProfileObj!!.routePointList
val totalYLength = mProfileObj!!.yDistance - mProfileObj!!.originalY
val totalXLength = mProfileObj!!.xRouteDistance
val profileDrawHeight = mProfileHeight.toFloat()
val profileDrawWidth = mProfileRouteWidth.toFloat()
val originalPointX = (paddingLeft + mYMarginLeft + mIconRadius).toFloat()
val routeSize = routeList.size
var startX = 0f
var startY = 0f
for (i in 0 until routeSize) {
val (height, distance) = routeList[i]
val yLength = height - mProfileObj!!.originalY
startY =
(profileDrawHeight * (totalYLength - yLength) / totalYLength).toFloat() + paddingTop
if (i == 0) {
startX = originalPointX
} else {
startX += (profileDrawWidth * distance / totalXLength).toFloat()
}
// 绘制起终点
if (i == 0 || i == routeSize - 1) {
mPaint!!.color = Color.BLACK
var pointBmp: Bitmap? = null
pointBmp = if (i == 0) {
BitmapFactory.decodeResource(resources, R.mipmap.ic_profile_map_start)
} else {
BitmapFactory.decodeResource(resources, R.mipmap.ic_profile_map_end)
}
if (i == 0) {
canvas.drawBitmap(pointBmp, startX - mIconRadius, startY - mIconRadius, mPaint)
} else {
// 起点之后的点都从图标右侧开始,所以X轴参数要加上半个起点图标的偏移量,避免途经点压盖
canvas.drawBitmap(pointBmp, startX + mIconRadius, startY - mIconRadius, mPaint)
}
} else {
// 绘制途经点,航点超过8个就用正方形色块表示,避免显示控件不足。
if (routeList.size > 8) {
mPaint!!.color = mColorProfileMapRouteWaypoint
mPaint!!.style = Paint.Style.FILL
canvas.drawRect(
mIconRadius + startX - mWayPointRadius,
startY - mWayPointRadius,
mIconRadius + startX + mWayPointRadius,
startY + mWayPointRadius,
mPaint!!
)
} else {
val wayPointResId = "ic_profile_map_way_point_$i"
val wayPointRes =
ContextInitial.getResourceId(wayPointResId, ContextInitial.TYPE_MIPMAP)
val pointBmp = BitmapFactory.decodeResource(resources, wayPointRes)
// 起点之后的点都从图标右侧开始,所以X轴参数要加上半个起点图标的偏移量,避免途经点压盖
canvas.drawBitmap(pointBmp, startX, startY - mIconRadius, mPaint)
}
}
}
}
override fun applySkin() {
initColor()
invalidate()
}
private fun initColor() {
if (currentSkin() === SkinManager.SKIN_SATELLITE) {
mColorProfileMapBackground = resources.getColor(R.color.color_CCFAFBFF_skin_satellite)
mColorProfileMapYOriginal = resources.getColor(R.color.color_CC000000_skin_satellite)
mColorProfileMapTerrainLine = resources.getColor(R.color.color_7C8DA2_skin_satellite)
mColorProfileMapTerrainArea = resources.getColor(R.color.color_9CB0C5_skin_satellite)
mColorProfileMapTerrainAreaEnd = resources.getColor(R.color.color_D8D8D8_skin_satellite)
mColorProfileMapRouteLine = resources.getColor(R.color.color_CC0D0D15_skin_satellite)
mColorProfileMapRouteWaypoint = resources.getColor(R.color.color_0D0D15_skin_satellite)
} else {
mColorProfileMapBackground = resources.getColor(R.color.color_CCFAFBFF_skin)
mColorProfileMapYOriginal = resources.getColor(R.color.color_CC000000_skin)
mColorProfileMapTerrainLine = resources.getColor(R.color.color_7C8DA2_skin)
mColorProfileMapTerrainArea = resources.getColor(R.color.color_9CB0C5_skin)
mColorProfileMapTerrainAreaEnd = resources.getColor(R.color.color_D8D8D8_skin)
mColorProfileMapRouteLine = resources.getColor(R.color.color_CC0D0D15_skin)
mColorProfileMapRouteWaypoint = resources.getColor(R.color.color_0D0D15_skin)
}
}
}
扩展阅读:
- Android 自定义棱形样式进度条控件
- Android 弧形 RecyclerView 实现(Kotlin)
- 美图手机音乐Widget动画实现
- Android 心率动画自定义控件实现
- Android 卡片旋转切换动效实现详解
- Android 残影数字动画实现详解
- Android 自定义菱形横向滑动指示器控件
微信公众号
转载请注明出处:陈文管的博客 – Android 航线剖面图自定义控件绘制实现
发表回复