首页 Android 正文
  • 本文约10453字,阅读需52分钟
  • 53
  • 0

Android 航线剖面图自定义控件绘制实现

摘要

本文介绍Android平台飞行航线剖面图自定义控件绘制的实现,给出实现效果截图,Java和Kotlin具体实现代码。 一、实现效果 0m水平线为起点的地形高度,起点和降落点图标固定绘制,途经点超过6个的时候用正方形色块表示,避免途径点过多的时候显示图标的宽度不足,地形区域使用渐变色填充。 二、Java代码实现代码详细实现逻辑已在代码注释中说明,此处不做赘述。...

本文介绍Android平台飞行航线剖面图自定义控件绘制的实现,给出实现效果截图,Java和Kotlin具体实现代码。

一、实现效果

0m水平线为起点的地形高度,起点和降落点图标固定绘制,途经点超过6个的时候用正方形色块表示,避免途径点过多的时候显示图标的宽度不足,地形区域使用渐变色填充。

Android 航线剖面图自定义控件绘制实现

Android 航线剖面图自定义控件绘制实现

Android 航线剖面图自定义控件绘制实现

二、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);
        }
    }
}
Java

在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>
XML

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());
}
Java

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

三、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)
        }
    }
}
Kotlin

扫描二维码,在手机上阅读


评论