自定义控件——原创仿地图瓦片动态加载_阶段3_实现绘图、缩放、移动功能,取代不好用的Canvas

20240612:当前Demo效果

高分辩率瓦片栅格化效果

地图上的瓦片技术在绘图app中应用于缩放、移动的效果,制作的瓦片是以视口大小5倍宽高渲染后进行切片预处理的,因此放大5倍以内都不会失真。同时栅格化之后因为无需反复渲染矢量对象,因此不会随着矢量内容的堆积而使得移动缩放变卡。

20190729  修复了部分手机绘图后移动后闪退的问题        

    在《自定义控件——原创仿地图瓦片动态加载_阶段1_动态添加和移除View》和《自定义控件——原创仿地图瓦片动态加载_阶段2_动态增加减少单元格坐标》中,我已经实现了一个自定义控件,可以无限地拖放、可以缩放,并且睡着拖放和缩放过程赋予不同的标记tag的。现在,我们将继续深化该自定义控件的建设,实现绘图功能,以及在Unit移出时保存到对应tag代表的两层文件夹中,Unit移入时加载对应tag的内容。

一、文件结构:

如下图所示,tag中的x坐标为第一层文件夹,tag的y坐标为第二层文件夹。当遍历到某一Unit时,即创建对应坐标的文件夹。在代表某Unit的tag移入到可见区域时,则加载对应文件夹路径的图块;当代表某Unit的tag移出可见区域时,则把当前Unit中的图块覆盖到对应路径的文件中去。

二、控件图层结构:

      本控件分为两层构成,在静止时——即没有触发移动、缩放时,所有绘制的Path都写入到最表面的“暂时绘制图层”。当要执行触发移动、缩放时,则计算出Unit所在的可见区域,并使用该可见区域切割“暂时绘制图层”对应区域作为图块,并拉伸、偏移图块写到Unit对应区域中,和Unit原本的像素进行覆盖和融合,从而刷新Unit的图像,然后清除“暂时绘制图层”画布。

三、画面分割和偏移计算:

       首先计算出Unit可见部分的准确范围,具体计算公式可以参考我以前的文章《自定义控件——原创仿地图瓦片动态加载_阶段1_动态添加和移除View》的末尾部分,如果有Unit部分可见区域越过了父控件的边界,例如Unit左边框比父控件左边框坐标值还要小,那么截图块的区域横坐标就是父控件左边框了,同样的推导也可以用于确定右边、上边、底边的位置,从而最终确定对绘制图层的截图图块的准确区域。具体运算代码如下,targetRect即是要目标区域,getX()得到的是Unit控件初始化时的左上角坐标,getWidth()、getHeight()是控件初始化后的宽度、高度,getScaleX()、getScaleY()分别是当前宽度、高度缩放到原来的比例:

            //获取本控件在屏幕可见区域对应的位图像素,并制作成Bitmap显示
            Rect targetRect = new Rect(0, 0, 0, 0);
            //计算本Unit需要截取父控件固定画布Bitmap的范围Start:
            if (getX() + (1 - getScaleX()) / 2 * getWidth() < parent.getLeft()) { //左边缘小于0
                targetRect.left = parent.getLeft();
            } else {
                targetRect.left = (int)(getX() + (1 - getScaleX()) / 2 * getWidth());
            }

            if (getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX() > parent.getRight()) { //单元的右边缘大于父控件右边缘
                targetRect.right = parent.getRight();
            } else {
                targetRect.right = (int)(getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX());
            }

            if (getY() + (1 - getScaleY()) / 2 * getHeight() < parent.getTop()) {
                targetRect.top = parent.getTop();
            } else {
                targetRect.top = (int)(getY() + (1 - getScaleY()) / 2 * getHeight());
            }

            if (getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY() > parent.getBottom()) {
                targetRect.bottom = parent.getBottom();
            } else {
                targetRect.bottom = (int)(getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY());
            }

       我的Unit控件是继承于ImageView的,所以如果此时把截取下来的像素块生成Bitmap之后直接使用setBitmap的话,会使得原来的图像被覆盖为该像素块的画面的同时,该像素块的画面的画面可能会和原来在绘图图层时产生了位置或大小上的误差。所以当某个Unit的实际可见区域的左上角坐标在父控件左边界之外时,要把图块左边界右移到父控件的左边界对齐,即向右移动“父控件左边界 - Unit左边界”那么长的距离,而且由于该距离是视觉上的距离,而不是控件内部参考系的距离,所以还要除以“缩放比例”得到控件内部参考系的距离。而越过父控件右边界时,则Unit实际显示的横向范围是“父控件右边界 - Unit可见区域左边界”,然后再除以缩放比例得到控件内部参考系的长度(不是可见区域多长就是多长,可以理解控件按比例缩小放大其实只是拉远拉近了,不代表控件内部长度发生了变化,所以一定要除以缩放比例才能把可见长度变成控件内部长度),上边和底边的运算也类似,然后得到图块的拉伸的具体范围。然后再把该范围的宽高除以图片的宽高,以偏移值即其实的x、y值为中心缩放图块,再使用matrix工具缩放图块后写到原本的cacheBitmap中叠加起来,并保存到外存中。换算方法可能比较绕口,可以参考我之前的文章《Android View跟随手势漫游缩放方法》、Bitmap无图像损失的截取和保存方法:《Android开发中一种原封不动地保存Bitmap数据的办法》。具体代码如下:

   

//如果Unit的做边框在父控件左侧外面,则需要把像素块移动到Unit可见区域的骑士位置再拉伸。所以这里计算左边框要偏移多少
            float dx = 0;
            float dy = 0;
            if(getX()  + (1 - getScaleX()) / 2 * getWidth() < 0){
                dx = -(getX() + (1 - getScaleX()) / 2 * getWidth()) / getScaleX(); //左偏了多少,图片就要右偏多少,而且即使格子缩小了也只是视觉上缩少了,格子的像素量不变,所以截出来的图的坐标还要反向放大进行偏移
            }
            if(getY()  + (1 - getScaleY()) / 2 * getHeight() < 0){
                dy = -(getY() + (1 - getScaleY()) / 2 * getHeight()) / getScaleY();
            }
            matrix.postTranslate(dx, dy);
            //之前的步骤计算了图片覆盖的左起点和顶起点,现在计算图片覆盖的右终点和底终点
            float parentWidth = ((FrameLayout) getParent()).getRight(); //父控件右边终点
            float parentHeight = ((FrameLayout) getParent()).getBottom();

            float unitRightBoarder = (getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX());  //本Unit在屏幕实际的右边界位置
            /**如果Unit右边界位置超出父控件右边界,则“Unit可见区域右边界 = 父控件右边界 - Unit实际可见左边界”并反向放大至和图片等效比例尺。
            /否则直接用Unit真实右边界**/
            float rightBoarder =  unitRightBoarder > parentWidth ?
                    (parentWidth - (getX() + (1 - getScaleX()) / 2 * getWidth())) / getScaleX() :
                    getWidth() ;//右边框
            //原理同上
            float unitBottomBoarder = (getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY());
            float bottomBoarder = unitBottomBoarder > parentHeight ?
                    (parentHeight - (getY() + (1 - getScaleY()) / 2 * getHeight())) / getScaleY():  //(屏幕边界 - View实际起始边界) / 缩放率 反向放大
                    getHeight() ;
            //图片在Unit中实际应显示范围
            RectF visibleRect = new RectF(dx, dy, rightBoarder, bottomBoarder);
            //以实际应显示范围的左上角作为缩放中心,长、宽分别除以像素块的长、宽,得到像素块应该拉伸或者收缩的比例:
            matrix.postScale(visibleRect.width() / newBitmap.getWidth(), visibleRect.height() / newBitmap.getHeight(), dx, dy);
            canvas.drawBitmap(newBitmap, matrix, null);
            setImageBitmap(cacheBitmap);
        //保存本次绘图
            saveUnitBitmap();

四、运作结果展示与Demo源代码下载地址:

MapView源代码:

package cjz.project.maptry4;

import android.annotation.NonNull;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.os.Handler;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;

import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;

import cjz.project.maptry.R;

/**
 * 以地图的方式取代SurfaceView,避免写进去的Path之类的东西老是要做偏移之类的麻烦操作
 * Created by cjz on 2019/4/30.
 */

public class MapView extends FrameLayout {


    /**缩放比例下限**/
    private final float MIN_SCALE = 0.8f;
    /**缩放比例上限**/
    private final float MAX_SCALE = 1.2f;
    /**缩放比例**/
    private float totalScale = 1;
    /**
     * 单元格矩阵长宽均有多少个单元
     **/
    private final int MATRIX_LENGTH = 12;
    /**单元格表**/
    private MapUnit mapUnitMatrix[][] = new MapUnit[MATRIX_LENGTH][MATRIX_LENGTH];
    private boolean initFinished = false;
    /**画布位图**/
    private Bitmap canvasBitmap = null;
    /**画布**/
    private Canvas canvas = null;
    /**是否需要把刚刚写到画布上的画面刷新到啊单元格上,在漫游或者缩放时需要这么做**/
    private boolean isNeedRefresh = false;
    /**是否已经刷新了前景画布**/
    private boolean frontCanvasAlreadyClear = true;
    /**表面图层**/
    private Surface surface;

    /**
     * 表面图层,用于在保存之前显示每一步的修改
     * Created by cjz on 2019/7/15.
     */
    private class Surface extends View{

        private Bitmap bitmap;

        public Surface(Context context) {
            super(context);
        }

        public Surface(Context context, AttributeSet attrs) {
            super(context, attrs);
        }

        public Surface(Context context, AttributeSet attrs, int defStyleAttr) {
            super(context, attrs, defStyleAttr);
        }

        protected void drawBitmap(Bitmap bitmap){
            this.bitmap = bitmap;
            invalidate();
//        setBackgroundColor(Color.RED);
        }


        @Override
        protected void onDraw(Canvas canvas) {
            if(bitmap != null){
                canvas.drawBitmap(bitmap, 0, 0, null);
            }
            Log.i("表面绘制", "");
            super.onDraw(canvas);

        }
    }

    public MapView(Context context) {
        super(context);
    }

    public MapView(Context context, AttributeSet attrs) {
        super(context, attrs);

    }

    public MapView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);

    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(!initFinished){
            int width = (int)(MeasureSpec.getSize(widthMeasureSpec) * 1.25f);
            int height = (int)(MeasureSpec.getSize(heightMeasureSpec) * 1.25f);
            this.setLayoutParams(new LayoutParams(width, height));
            createView(width, height);
            initFinished = true;
        }
    }

    private void createView(int width, int height) {
        final int widthSplit = MATRIX_LENGTH - 3;
        //创建
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                MapUnit mapUnit = new MapUnit(getContext());
                mapUnit.setLayoutParams(new ViewGroup.LayoutParams(width / widthSplit, height / widthSplit));
                addView(mapUnit);
                mapUnitMatrix[xPos][yPos] = mapUnit;
                mapUnit.setTag(new int[]{xPos, yPos});
            }
        }
        //排列
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                mapUnitMatrix[xPos][yPos].setX(xPos * width / widthSplit);
                mapUnitMatrix[xPos][yPos].setY(yPos * height / widthSplit);
            }
        }
        //缩放
//        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
//            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
//                View view = mapUnitMatrix[xPos][yPos];
//                //以本View中心点为缩放中心缩放
//                view.setScaleX(view.getScaleX() * 0.5f);
//                view.setScaleY(view.getScaleY() * 0.5f);
//                //求本view中心点在屏幕中的坐标
//                float centerX = view.getX() + view.getWidth() / 2;
//                float centerY = view.getY() + view.getHeight() / 2;
//                /**向缩放中心靠拢,例如缩放为原来的80%,那么缩放中心x到view中心x的距离则为0.8*(缩放中心x - view中心x),
//                 * 那么view的x距离屏幕左边框的距离则 为   view中心x + (1 - 0.8) * (缩放x - view中心x)  ****/
//                float centerXAfterScale = centerX + (0 - centerX) * (1 - 0.5f); //view中心向缩放中心聚拢或扩散,例如scale为0.8,那么收缩0.2,现在的宽度就是之前宽度的0.8了,得到收缩目的
//                float centerYAfterScale = centerY + (0 - centerY) * (1 - 0.5f);
//                view.setX(centerXAfterScale - view.getWidth() / 2); //setXY是set左上角的x,y,所以view中心点要减去宽度/高度的一般来重新得到应该去的左上角坐标
//                view.setY(centerYAfterScale - view.getHeight() / 2);
            viewFind(view, this.scale);
//                Log.i("View" + view.hashCode() + "的信息", String.format("长度:%d, 宽度:%d, 坐标x:%f, 坐标y:%f", view.getWidth(), view.getHeight(), view.getX(), view.getY()));
//            }
//        }
        if(canvasBitmap != null && !canvasBitmap.isRecycled()){
            canvasBitmap.recycle();
            canvasBitmap = null;
        }
        canvasBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
        canvas = new Canvas(canvasBitmap);
        //创建表面图层
        surface = new Surface(getContext());
        addView(surface);
    }

    /**
     * 缩放函数
     **/
    public void scale(float scale, float px, float py) {
        refreshUnitImage();
        if(totalScale * scale < MIN_SCALE || totalScale * scale > MAX_SCALE){
            return;
        }
        Log.i("缩放", "缩放成功");
        totalScale *= scale;
//        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
//            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
//                View view = mapUnitMatrix[xPos][yPos];
//                //以本View中心点为缩放中心缩放
//                view.setScaleX(view.getScaleX() * scale);
//                view.setScaleY(view.getScaleY() * scale);
//                //求本view中心点在屏幕中的坐标
//                float centerX = view.getX() + view.getWidth() / 2;
//                float centerY = view.getY() + view.getHeight() / 2;
//                /**向缩放中心靠拢,例如缩放为原来的80%,那么缩放中心x到view中心x的距离则为0.8*(缩放中心x - view中心x),
//                 * 那么view的x距离屏幕左边框的距离则 为   view中心x + (1 - 0.8) * (缩放x - view中心x)  ****/
//                float centerXAfterScale = centerX + (px - centerX) * (1 - scale); //view中心向缩放中心聚拢或扩散,例如scale为0.8,那么收缩0.2,现在的宽度就是之前宽度的0.8了,得到收缩目的
//                float centerYAfterScale = centerY + (py - centerY) * (1 - scale);
//                view.setX(centerXAfterScale - view.getWidth() / 2); //setXY是set左上角的x,y,所以view中心点要减去宽度/高度的一般来重新得到应该去的左上角坐标
//                view.setY(centerYAfterScale - view.getHeight() / 2);
            viewFind(view, this.scale);
//                Log.i("View" + view.hashCode() + "的信息", String.format("长度:%d, 宽度:%d, 坐标x:%f, 坐标y:%f", view.getWidth(), view.getHeight(), view.getX(), view.getY()));
//            }
//        }
        setScaleX(getScaleX() * scale);
        setScaleY(getScaleY() * scale);
        //求本view中心点在屏幕中的坐标
        float centerX = getX() + getWidth() / 2;
        float centerY = getY() + getHeight() / 2;
        /**向缩放中心靠拢,例如缩放为原来的80%,那么缩放中心x到view中心x的距离则为0.8*(缩放中心x - view中心x),
         * 那么view的x距离屏幕左边框的距离则 为   view中心x + (1 - 0.8) * (缩放x - view中心x)  ****/
        float centerXAfterScale = centerX + (0 - centerX) * (1 - scale); //view中心向缩放中心聚拢或扩散,例如scale为0.8,那么收缩0.2,现在的宽度就是之前宽度的0.8了,得到收缩目的
        float centerYAfterScale = centerY + (0 - centerY) * (1 - scale);
        setX(centerXAfterScale - getWidth() / 2); //setXY是set左上角的x,y,所以view中心点要减去宽度/高度的一般来重新得到应该去的左上角坐标
        setY(centerYAfterScale - getHeight() / 2);
        Log.i("缩放", String.format("百分比:%f", totalScale));
    }

    /**
     * 移动函数 (效率有点问题,但暂时不管,反正以后要用OpenGL重写的,自定义View的显示效率不是最终追求的最优选择)
     **/
    public void translate(float distanceX, float distanceY) {
//        setScaleX(getScaleX() * 1.01f);
//        setScaleY(getScaleY() * 1.01f);
        refreshUnitImage();
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                View view = mapUnitMatrix[xPos][yPos];
                view.setX(view.getX() + (distanceX));
                view.setY(view.getY() + (distanceY));
            }
        }
        //x轴,y轴要分开两个循环处理,否则会引发混乱
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                View view = mapUnitMatrix[xPos][yPos];
                //移除去的部分添加到未显示的部分的末尾
                if(view.getX()  + (1 - view.getScaleX()) / 2 * view.getWidth() + view.getWidth() * view.getScaleX() < 0  && getWidth() > 0) { //单元格溢出到了屏幕左边,移动到当前对应行最右边
                    if(xPos == 0) {
                        //重设位置
                        view.setX(mapUnitMatrix[MATRIX_LENGTH - 1][yPos].getX() + mapUnitMatrix[MATRIX_LENGTH - 1][yPos].getWidth() * mapUnitMatrix[MATRIX_LENGTH - 1][yPos].getScaleX());
                        int targetPos[] = (int[])mapUnitMatrix[MATRIX_LENGTH - 1][yPos].getTag();
                        view.setTag(new int[]{targetPos[0] + 1, targetPos[1]}); //重设单元格标记
                        for (int i = xPos; i < MATRIX_LENGTH - 1; i++) {
                            mapUnitMatrix[i][yPos] = mapUnitMatrix[i + 1][yPos];
                        }
                        mapUnitMatrix[MATRIX_LENGTH - 1][yPos] = (MapUnit) view;
//                        view.invalidate(); //申请重绘
                    }
                }
                else if (view.getX() + (1 - view.getScaleX()) / 2 * view.getWidth() > getWidth() && getWidth() > 0) {
                    if(xPos == MATRIX_LENGTH - 1){ //因为初始化时显示的Unit是最左上角的Unit,有可能导致非最后一列的内容被平移,这违反自动补充的逻辑,会出bug,所以要加判断
                        //重设位置(设置和最后一个的左上角坐标直接重合(setx用于设定左上角坐标),再减去控件宽度*缩放量使得目标控件右上角和最后一个控件左上角对齐)
                        view.setX(mapUnitMatrix[0][yPos].getX() - mapUnitMatrix[0][yPos].getWidth() * mapUnitMatrix[0][yPos].getScaleX());
                        int targetPos[] = (int[])mapUnitMatrix[0][yPos].getTag();
                        view.setTag(new int[]{targetPos[0] - 1, targetPos[1]}); //重设单元格标记
                        MapUnit temp = mapUnitMatrix[MATRIX_LENGTH - 1][yPos];
                        for(int i = MATRIX_LENGTH - 1; i > 0 ; i--){
                            mapUnitMatrix[i][yPos] = mapUnitMatrix[i - 1][yPos];
                        }
                        mapUnitMatrix[0][yPos] = temp;
//                        view.invalidate(); //申请重绘
                    }
                }
            }
        }
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                View view = mapUnitMatrix[xPos][yPos];
                if (view.getY() + (1 - view.getScaleY()) / 2 * view.getHeight() + view.getHeight() * view.getScaleY() < 0 && getHeight() > 0) {
                    if (yPos == 0) {
                        //重设位置
                        view.setY(mapUnitMatrix[xPos][MATRIX_LENGTH - 1].getY() + mapUnitMatrix[xPos][MATRIX_LENGTH - 1].getHeight() * mapUnitMatrix[xPos][MATRIX_LENGTH - 1].getScaleY());
                        int targetPos[] = (int[])mapUnitMatrix[xPos][MATRIX_LENGTH - 1].getTag();
                        view.setTag(new int[]{targetPos[0], targetPos[1] + 1}); //重设单元格标记
                        for (int i = yPos; i < MATRIX_LENGTH - 1; i++) {
                            mapUnitMatrix[xPos][i] = mapUnitMatrix[xPos][i + 1];
                        }
                        mapUnitMatrix[xPos][MATRIX_LENGTH - 1] = (MapUnit) view;
//                        view.invalidate(); //申请重绘
                    }
                }
                else if (view.getY() + (1 - view.getScaleY()) / 2 * view.getHeight() > getHeight() && getHeight() > 0) {
                    if (yPos == MATRIX_LENGTH - 1) {
                        //Log.i("越位", "到了屏幕下边界");
                        //重设位置(设置和最后一个的左上角坐标直接重合(setx用于设定左上角坐标),再减去控件宽度*缩放量使得目标控件右上角和最后一个控件左上角对齐)
                        view.setY(mapUnitMatrix[xPos][0].getY() - view.getHeight() * view.getScaleY());
                        int targetPos[] = (int[])mapUnitMatrix[xPos][0].getTag();
                        view.setTag(new int[]{targetPos[0], targetPos[1] - 1}); //重设单元格标记
                        MapUnit temp = mapUnitMatrix[xPos][MATRIX_LENGTH - 1];
                        for (int i = MATRIX_LENGTH - 1; i > 0; i--) {
                            mapUnitMatrix[xPos][i] = mapUnitMatrix[xPos][i - 1];
                        }
                        mapUnitMatrix[xPos][0] = temp;
//                        view.invalidate(); //申请重绘
                    }
                }
            }
        }
//        Log.i("移动", String.format("x位移:%f, y位移:%f", distanceX, distanceY));
//        invalidate();
    }

    /**绘制Path**/
    public void drawPath(@NonNull Path path, @NonNull Paint paint){
        canvas.drawPath(path, paint);
        surface.drawBitmap(canvasBitmap);
        isNeedRefresh = true;
    }


    /****/




    /**刷新单元图案**/
    private void refreshUnitImage(){
        if(isNeedRefresh){
            for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
                for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                    MapUnit mapUnit = mapUnitMatrix[xPos][yPos];
                    mapUnit.drawBitmap(canvasBitmap); //分配给各单元进行切图操作
                }
            }
            //清空主画布
            canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR);
        }
        isNeedRefresh = false;
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        //贴前景画布
//        canvas.drawBitmap(canvasBitmap, new Rect(0, 0, canvas.getWidth(), canvas.getHeight()), new Rect(0, 0, canvas.getWidth(), canvas.getHeight()), null);

    }

    @Override
    protected boolean drawChild(Canvas canvas, View child, long drawingTime ) {
        boolean result =  super.drawChild(canvas, child, drawingTime);
        return result;
    }

}

MapUnit单元格源代码:

package cjz.project.maptry4;

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.ImageView;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Random;

/**
 * Created by cjz on 2019/4/30.
 */

public class MapUnit extends ImageView {

    private int beforeTag[] = new int[]{Integer.MIN_VALUE, Integer.MIN_VALUE};

    /**状态位:判断是否需要刷新**/
    public boolean isNeedRefresh = false;

    private Bitmap tempBitmap = null;
    private Bitmap bm = null;

    /**默认保存缓存图的路径**/
    private String cachePath = "";

    public Bitmap cacheBitmap;
    private Bitmap newBitmap;

    /**开启debug界面**/
    private boolean isDebug = true;
    private byte[] readPixels;
    private int mWidth, mHeight;

    /**
     * 是否初始化完成
     **/
    private boolean isInitFinished = false;

    public MapUnit(Context context) {
        super(context);
        init();
    }

    public MapUnit(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MapUnit(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init();
    }

    private void init(){
        cachePath = getContext().getFilesDir().getAbsolutePath() /*+ File.separator + pos[0] + File.separator + pos[1]*/;
        //Log.i("缓存路径", cachePath);
    }

    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        int width = MeasureSpec.getSize(widthMeasureSpec);
        int height = MeasureSpec.getSize(heightMeasureSpec);
//        if(width > 0 && height > 0){
        mWidth = width;
        mHeight = height;
//        }
//        if(getWidth() > 0 && getHeight() > 0){
//            isInitFinished = true;
//            Log.i("初始化完成","asdasdasd");
//        }
    }

    @Override
    public void setLayoutParams(ViewGroup.LayoutParams params) {
        if(mWidth == 0 && mHeight == 0){
            mWidth = params.width;
            mHeight = params.height;
        }
        //Log.i("长度宽度setLayoutParams", String.format("mWidth : %d, mHeight : %d", mWidth, mHeight));
        super.setLayoutParams(params);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        super.onLayout(changed, left, top, right, bottom);
    }

    /**读取该单元格对应的Bitmap**/
    private Bitmap readUnitBitmap(){
        File file = new File(cachePath);
        if(file.exists()){
            try {
                FileInputStream fileInputStream = new FileInputStream(file);
//                if(!isInitFinished){
//                    if (mWidth == 0 || mHeight == 0) {
//                        Log.i("长度宽度异常", "");
//                        return null;
//                    } else {
//                        isInitFinished = true;
//                    }
//                } else {
                if (mWidth <= 0 || mHeight <= 0) {
                    Log.i("长度宽度异常", "");
                    return null;
                }
//                }
                Bitmap unitPixelBitmap = Bitmap.createBitmap(mWidth, mHeight, Bitmap.Config.ARGB_8888);
                if(readPixels == null){
                    readPixels = new byte[fileInputStream.available()];
                } else if(readPixels.length < fileInputStream.available()){
                    readPixels = new byte[fileInputStream.available()];
                }
                fileInputStream.read(readPixels);
                unitPixelBitmap.copyPixelsFromBuffer(ByteBuffer.wrap(readPixels));
                fileInputStream.close();
                //Log.i("正常返回图片", "");
                return unitPixelBitmap;
            } catch (FileNotFoundException e) {
                Log.i("读图出错了", "读图出错了0");
                e.printStackTrace();
            } catch (IOException e) {
                Log.i("读图出错了", "读图出错了1");
                e.printStackTrace();
            }
        }
        return null;
    }

//    /**读取该单元格对应的Bitmap**/
//    private Bitmap readUnitBitmap2(){
//        File file = new File(cachePath);
//        if(file.exists()){
//            try {
//                FileInputStream fileInputStream = new FileInputStream(file);
//                if (getWidth() == 0 || getHeight() == 0) {
//                    Log.i("长度宽度异常", "");
//                    return null;
//                }
//                Bitmap bitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
//                if(readPixels == null){
//                    readPixels = new byte[fileInputStream.available()];
//                } else if(readPixels.length < fileInputStream.available()){
//                    readPixels = new byte[fileInputStream.available()];
//                }
//                fileInputStream.read(readPixels);
//                bitmap.copyPixelsFromBuffer(ByteBuffer.wrap(readPixels));
//                fileInputStream.close();
//                //Log.i("正常返回图片", "");
//                return bitmap;
//            } catch (FileNotFoundException e) {
//                e.printStackTrace();
//            } catch (IOException e) {
//                e.printStackTrace();
//            }
//        }
//        return null;
//    }

    /**换坐标之前保存该单元格对应的Bitmap**/
    private void saveUnitBitmap(){
        File file = new File(cachePath);
        try {
            if (!file.exists()) {
                file.createNewFile();
            }
            FileOutputStream fileOutputStream = new FileOutputStream(file);
            Bitmap cacheBitmap = this.cacheBitmap;
            if(cacheBitmap == null){
                buildDrawingCache();
                cacheBitmap = getDrawingCache();
            }
            byte pixels[] = new byte[cacheBitmap.getWidth() * cacheBitmap.getHeight() * 4];
            cacheBitmap.copyPixelsToBuffer(ByteBuffer.wrap(pixels));
            fileOutputStream.write(pixels);
            fileOutputStream.close();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    public void setCachePath(String cachePath){
        this.cachePath = cachePath;
    }

    @Override
    public void setTag(Object tag) {
        super.setTag(tag);
        //设置标签的同时创建缓存文件夹和缓存文件
        int pos[] = (int[]) tag;
        File file = new File(getContext().getFilesDir().getAbsolutePath() + File.separator + pos[0]);
        if(!file.exists()){
            file.mkdir();
        }
        file = new File(getContext().getFilesDir().getAbsolutePath() + File.separator + pos[0] + File.separator + pos[1]);
        if(!file.exists()){
            file.mkdir();
        }
        cachePath = getContext().getFilesDir().getAbsolutePath() + File.separator + pos[0] + File.separator + pos[1]  + File.separator + "unit.dat";
//        Log.i("缓存路径", cachePath);
        //用Tag对应图片刷新Unit
        if(pos[0] != beforeTag[0] || pos[1] != beforeTag[1]){
            Bitmap bitmap = readUnitBitmap();
            if(bitmap != null){
                //Log.i("刷新图片", "refreshBitmap:" + cachePath);
                setImageBitmap(bitmap);
            } else {
                Log.i("刷新图片", "refreshNull:" + cachePath);
                setImageBitmap(null);
            }
        }
        beforeTag[0] = pos[0];
        beforeTag[1] = pos[1];
    }

    @Override
    public void setImageBitmap(Bitmap bm) {
        super.setImageBitmap(bm);
        if(this.bm != null){
            this.bm.recycle();
            this.bm = null;
        }
        this.bm = bm;
    }

    /**绘制图片**/
    public void drawBitmap(Bitmap bitmap){
        //先读取本Unit对应的Tag对应的文件夹是否有图片,有的话需要现在的图叠加起来
//        if(cacheBitmap != null && !cacheBitmap.isRecycled()){
//            cacheBitmap.recycle();
//            cacheBitmap = null;
//        }
//        if(newBitmap != null && !newBitmap.isRecycled()){
//            newBitmap.recycle();
//            newBitmap = null;
//        }
//        System.gc();
        cacheBitmap = readUnitBitmap();
        if(cacheBitmap == null){
            cacheBitmap = Bitmap.createBitmap(getWidth(), getHeight(), Bitmap.Config.ARGB_8888);
        }
        Canvas canvas = new Canvas(cacheBitmap);
        if(bitmap != null && !bitmap.isRecycled()){
            FrameLayout parent = (FrameLayout) getParent();
            //获取本控件在屏幕可见区域对应的位图像素,并制作成Bitmap显示
            Rect targetRect = new Rect(0, 0, 0, 0);
            //计算本Unit需要截取父控件固定画布Bitmap的范围Start:
            if (getX() + (1 - getScaleX()) / 2 * getWidth() < parent.getLeft()) { //左边缘小于0
                targetRect.left = parent.getLeft();
            } else {
                targetRect.left = (int)(getX() + (1 - getScaleX()) / 2 * getWidth());
            }

            if (getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX() > parent.getRight()) { //单元的右边缘大于父控件右边缘
                targetRect.right = parent.getRight();
            } else {
                targetRect.right = (int)(getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX());
            }

            if (getY() + (1 - getScaleY()) / 2 * getHeight() < parent.getTop()) {
                targetRect.top = parent.getTop();
            } else {
                targetRect.top = (int)(getY() + (1 - getScaleY()) / 2 * getHeight());
            }

            if (getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY() > parent.getBottom()) {
                targetRect.bottom = parent.getBottom();
            } else {
                targetRect.bottom = (int)(getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY());
            }

            if(targetRect.width() <= 0 || targetRect.height() <= 0){
                return;
            }
            //计算本Unit需要截取父控件固定画布Bitmap的范围End;
            //生成位图
            int pixels[] = new int[targetRect.width() * targetRect.height()];
            if(targetRect.right > bitmap.getWidth()){
                targetRect.right = bitmap.getWidth();
            }
            if(targetRect.bottom > bitmap.getHeight()){
                targetRect.bottom = bitmap.getHeight();
            }
            bitmap.getPixels(pixels, 0, targetRect.width(), targetRect.left, targetRect.top, targetRect.width(), targetRect.height());
            newBitmap = Bitmap.createBitmap(pixels, targetRect.width(), targetRect.height(), Bitmap.Config.ARGB_8888);
            Log.i("显示范围", targetRect.toString());
            //由于Unit无论缩放之后视觉大小多大,初始大小依然是满屏的,所以切出来的屏幕画布一部分的像素块,无法铺满Unit,需要对像素块拉伸:
            setScaleType(ScaleType.MATRIX);
            Matrix matrix = new Matrix();
            //如果Unit的做边框在父控件左侧外面,则需要把像素块移动到Unit可见区域的骑士位置再拉伸。所以这里计算左边框要偏移多少
            float dx = 0;
            float dy = 0;
            if(getX()  + (1 - getScaleX()) / 2 * getWidth() < 0){
                dx = -(getX() + (1 - getScaleX()) / 2 * getWidth()) / getScaleX(); //左偏了多少,图片就要右偏多少,而且即使格子缩小了也只是视觉上缩少了,格子的像素量不变,所以截出来的图的坐标还要反向放大进行偏移
            }
            if(getY()  + (1 - getScaleY()) / 2 * getHeight() < 0){
                dy = -(getY() + (1 - getScaleY()) / 2 * getHeight()) / getScaleY();
            }
            matrix.postTranslate(dx, dy);
            //之前的步骤计算了图片覆盖的左起点和顶起点,现在计算图片覆盖的右终点和底终点
            float parentWidth = ((FrameLayout) getParent()).getRight(); //父控件右边终点
            float parentHeight = ((FrameLayout) getParent()).getBottom();

            float unitRightBoarder = (getX() + (1 - getScaleX()) / 2 * getWidth() + getWidth() * getScaleX());  //本Unit在屏幕实际的右边界位置
            /**如果Unit右边界位置超出父控件右边界,则“Unit可见区域右边界 = 父控件右边界 - Unit实际可见左边界”并反向放大至和图片等效比例尺。
             /否则直接用Unit真实右边界**/
            float rightBoarder =  unitRightBoarder > parentWidth ?
                    (parentWidth - (getX() + (1 - getScaleX()) / 2 * getWidth())) / getScaleX() :
                    getWidth() ;//右边框
            //原理同上
            float unitBottomBoarder = (getY() + (1 - getScaleY()) / 2 * getHeight() + getHeight() * getScaleY());
            float bottomBoarder = unitBottomBoarder > parentHeight ?
                    (parentHeight - (getY() + (1 - getScaleY()) / 2 * getHeight())) / getScaleY():  //(屏幕边界 - View实际起始边界) / 缩放率 反向放大
                    getHeight() ;
            //图片在Unit中实际应显示范围
            RectF visibleRect = new RectF(dx, dy, rightBoarder, bottomBoarder);
            //以实际应显示范围的左上角作为缩放中心,长、宽分别除以像素块的长、宽,得到像素块应该拉伸或者收缩的比例:
            matrix.postScale(visibleRect.width() / newBitmap.getWidth(), visibleRect.height() / newBitmap.getHeight(), dx, dy);
            canvas.drawBitmap(newBitmap, matrix, null);
            setImageBitmap(cacheBitmap);
//            setImageMatrix(matrix);
            //保存本次绘图
            saveUnitBitmap();
        } else {
            setImageBitmap(cacheBitmap);
        }
    }

    //todo x作为一级文件夹,y作为二级文件夹,然后分别放对应(x,y)的bitmap文件
    @Override
    protected void onDraw(Canvas canvas) {
        //传入的是空白的canvas,因此绘制过的内容并不能不断叠加
        super.onDraw(canvas);
        if(isDebug){
            drawTest(canvas);
        }
    }

    private void drawTest(Canvas canvas){
        Random random = new Random();
        //Log.i("onDraw", hashCode() + "");
        Paint paint = new Paint();
        paint.setStrokeWidth(8f);
//        paint.setColor((0xFF000000 | (random.nextInt(255) & 0xFF) << 16 | (random.nextInt(255) & 0xFF) << 8 | (random.nextInt(255) & 0xFF)));
        paint.setColor(Color.GRAY);
        paint.setStyle(Paint.Style.STROKE);
        //绘制背景
        canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
        Paint paintPen = new Paint();
        paintPen.setStrokeWidth(4f);
        paintPen.setStyle(Paint.Style.FILL);
        paintPen.setColor(Color.RED);
        paintPen.setTextSize(32f);
        paintPen.setAntiAlias(true);
        //绘制自己是第几列第几行的单元
        if(getTag() != null) {
            int position[] = (int[]) getTag();
            canvas.drawText(String.format("UnitX: %d, \nUnitY: %d", position[0], position[1]),  getWidth() / 2 - 100, getHeight() / 2, paintPen);
        }

        //绘制边界点
        paint.setColor(Color.BLUE);
        canvas.drawPoint(0, 0, paint);
        canvas.drawPoint(0, getHeight(), paint);
        canvas.drawPoint(getWidth(), 0, paint);
        canvas.drawPoint(getWidth(), getHeight(), paint);

//        paint.setColor(Color.WHITE);
//        Path path = new Path();
//        path.moveTo(0, 0);
//        path.lineTo(getWidth(), getHeight());
//        canvas.drawPath(path, paint);
    }
}

全部源代码:

链接:https://pan.baidu.com/s/1pPyShpf9oW69wBSEUlXkMg 
提取码:wka3 

  • 4
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值