自定义控件——原创仿地图瓦片动态加载_阶段1_动态添加和移除View

        最近希望做一款自己的应用,其中有一个需求是可以按照用户手势滑动时,动态保存和加载闪存对应的图片单元。这个需求和地图有一点像。然后我自己推了一下,用自定义View做了一个。

       一开始我希望通过在FrameLayout中动态添加View,例如当屏幕上已经显示的View已经移除屏幕外,就通过removeView清除,然后再添加新view到反向边界坐标。但我发现这么做gc实在是弄得有点恶心,而且动态添加的代码不太好实现。

        所以我放弃了使用动态添加View的方式来实现该需求,而是通过一个二维数组,预先创建好足以缩放到最小时,面积依然可以覆盖整个父控件的View,当某一边界的View平移出屏幕外时,把数组对应边界的单元移动到反向边界上,并重设坐标即可,既可以避免GC,也方便我操作。

         该思维的图示如下:

  单轴偏向:

双轴偏向,分界为两次单轴偏向处理:

并且当超出区域的单元格数目多余1行或1列时,递归地从最外层开始迁移回到反向边界上。

 

实现代码如下:

单元格View的实现:

package cjz.project.maptry4;

import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.util.Log;
import android.view.View;
import java.util.Random;

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

public class MapUnit extends View {

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

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

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


    @Override
    protected void onDraw(Canvas canvas) {
        Random random = new Random();
        Log.i("onDraw", hashCode() + "");
        super.onDraw(canvas);
        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.setStyle(Paint.Style.FILL);
        //绘制随机色背景
        canvas.drawRect(0, 0, getWidth(), getHeight(), paint);
        Paint paintPen = new Paint();
        paintPen.setStrokeWidth(8f);
        paintPen.setStyle(Paint.Style.FILL);
        paintPen.setColor(Color.BLACK);
        paintPen.setTextSize(120f);
        paintPen.setAntiAlias(true);
        //绘制自己是第几列第几行的单元
        if(getTag() != null) {
            int position[] = (int[]) getTag();
            canvas.drawText(String.format("UnitX: %d, UnitY: %d", position[0], position[1]),  getWidth() / 2 - 100, getHeight() / 2, paintPen);
        }
    }
}

 

触摸移动View,同时也是动态移位算法的实现类:

package cjz.project.maptry4;

import android.content.Context;
import android.graphics.PointF;
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 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;

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

public class MapView extends FrameLayout {

    private PointF currentCenter = new PointF();
    private PointF prevCurrentCenter = null;
    private float prevDistance = Float.MIN_VALUE;
    private float totalScale = 1f;
    private float dx = 0, dy = 0;
    /**缩放比例上限**/
    private final float MAX_SCALE = 2f;
    /**缩放比例下限**/
    private final float MIN_SCALE = 0.5f;
    /**单元格矩阵长宽均有多少个单元**/
    private final int MATRIX_LENGTH = 4;
    /**单元格表**/
    private MapUnit mapUnitMatrix[][] = new MapUnit[MATRIX_LENGTH][MATRIX_LENGTH];
    private boolean initFinished = false;

    /*** 触摸点点距队列**/
    private Queue<Float> touchDistanceQueue = new LinkedBlockingQueue<>();

    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){
            createView();
            initFinished = true;
        }
    }

    private void createView() {
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                MapUnit mapUnit = new MapUnit(getContext());
//                mapUnit.setImageResource(R.mipmap.ic_launcher);
                mapUnit.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
                mapUnit.setTag(new int[]{xPos, yPos});
                addView(mapUnit);
                mapUnitMatrix[xPos][yPos] = mapUnit;
            }
        }
        for(int yPos = 0; yPos < MATRIX_LENGTH; yPos++) {
            for (int xPos = 0; xPos < MATRIX_LENGTH; xPos++) {
                mapUnitMatrix[xPos][yPos].setX(xPos * getMeasuredWidth());
                mapUnitMatrix[xPos][yPos].setY(yPos * getMeasuredHeight());
            }
        }
    }

    /*将触摸点的坐标平均化*/
    private float avergeX = 0, avergeY = 0;
    private int prevPointCount = 0;
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        switch (event.getAction()) {
            case MotionEvent.ACTION_DOWN:
                prevDistance = 0;
                prevPointCount = event.getPointerCount();
                //算出移动中心坐标、点间距离
                for(int i = 0; i < event.getPointerCount(); i++){
                    avergeX += event.getX(i);
                    avergeY += event.getY(i);
                    if(i + 1 < event.getPointerCount()){
                        prevDistance += Math.sqrt(Math.pow(event.getX(i + 1) - event.getX(i), 2) + Math.pow(event.getY(i + 1) - event.getY(i), 2));
                    }
                }
                avergeX /= event.getPointerCount();
                avergeY /= event.getPointerCount();
                if(prevCurrentCenter == null){
                    prevCurrentCenter = new PointF(avergeX, avergeY);
                } else {
                    prevCurrentCenter.set(avergeX, avergeY);
                }
                break;
            case MotionEvent.ACTION_MOVE:
                avergeX = 0;
                avergeY = 0;
                float nowDistance = 0;
                //算出移动中心坐标、点间距离
                for(int i = 0; i < event.getPointerCount(); i++){
                    avergeX += event.getX(i);
                    avergeY += event.getY(i);
                    if(i + 1 < event.getPointerCount()){
                        nowDistance += Math.sqrt(Math.pow(event.getX(i + 1) - event.getX(i), 2) + Math.pow(event.getY(i + 1) - event.getY(i), 2));
                    }
                }
                //现在的点间距离 除以 上次点间距离 这次得到缩放比例
                avergeX /= event.getPointerCount();
                avergeY /= event.getPointerCount();
                if((prevPointCount != event.getPointerCount()) || event.getPointerCount() <= 1 || prevPointCount <= 1){ //触摸点数突然改变 或者 触摸点不超过2,不允许缩放
                    prevDistance = nowDistance = 0;
                }
                //如果缩放数据有效,则进行平均平滑化并且进行缩放
                if(prevDistance > 0 && nowDistance > 0){
                    touchDistanceQueue.add(nowDistance / prevDistance);
                    if(touchDistanceQueue.size() >= 6) {
                        Float point[] = new Float[touchDistanceQueue.size()];
                        touchDistanceQueue.toArray(point);
                        float avergDistance = 0;
                        for(int i = 0; i < point.length; i++){
                            avergDistance += point[i];
                        }
                        avergDistance /= point.length;
                        scale((float) Math.sqrt(avergDistance), avergeX, avergeY);
                        while(touchDistanceQueue.size() > 6){
                            touchDistanceQueue.poll();
                        }
                    }
                }
                prevPointCount = event.getPointerCount();
                prevDistance = nowDistance;
                //当前坐标 - 上次坐标 = 偏移值,然后进行位置偏移
                if(prevCurrentCenter == null) {
                    prevCurrentCenter = new PointF(avergeX, avergeY);
                } else {
                    translate(avergeX - prevCurrentCenter.x, avergeY - prevCurrentCenter.y);
                    prevCurrentCenter.set(avergeX, avergeY);
                }
                break;
            case MotionEvent.ACTION_UP:
                //抬起,清理干净数据
                avergeX = 0;
                avergeY = 0;
                touchDistanceQueue.clear();
                break;
        }
        return true;
    }


    /**
     * 缩放函数
     **/
    public void scale(float scale, float px, float py) {
        if(totalScale * scale < MIN_SCALE || totalScale * scale > MAX_SCALE){
            return;
        }
        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中心向缩放中心聚拢或扩散
                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()));
            }
        }
        Log.i("缩放", String.format("百分比:%f", totalScale));
    }

    /**
     * 移动函数 (效率有点问题,但暂时不管,反正以后要用OpenGL重写的,自定义View的显示效率不是最终追求的最优选择)
     **/
    private void translate(float distanceX, float distanceY) {
        dx += distanceX;
        dy += distanceY;
        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());
                        for (int i = xPos; i < MATRIX_LENGTH - 1; i++) {
                            mapUnitMatrix[i][yPos] = mapUnitMatrix[i + 1][yPos];
                        }
                        mapUnitMatrix[MATRIX_LENGTH - 1][yPos] = (MapUnit) view;
                    }
                }
                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());
                        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;
                    }
                }
            }
        }
        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());
                        for (int i = yPos; i < MATRIX_LENGTH - 1; i++) {
                            mapUnitMatrix[xPos][i] = mapUnitMatrix[xPos][i + 1];
                        }
                        mapUnitMatrix[xPos][MATRIX_LENGTH - 1] = (MapUnit) view;
                    }
                }
                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());
                        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;
                    }
                }
            }
        }
        Log.i("移动", String.format("x位移:%f, y位移:%f", distanceX, distanceY));
//        invalidate();
    }

}

其中,view自身的缩放是向view自身中心缩放的,因此当缩放过后,在屏幕上实际显示的左上角坐标会和getX()和getY()得出来的不一样,宽度、高度也会和初始化getWidht()和getHeight()不一样,例如通过scaleX()和scaleY()缩放宽度和高度为原来的0.8倍时,则缩放后的view两边留白的宽度和高度都为原来长宽的0.2倍,所以左上角x坐标的实际位置要偏移一下,

即为getX() +  (1 - 0.8) / 2 * getWidth(),y坐标为getY() +  (1 -0.8) / 2 * getHeight(),实际长宽则直接乘以缩放比例即可得到

 

 

界面layout调用例子:

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">


    <cjz.project.maptry4.MapView
        android:layout_width="match_parent"
        android:layout_height="match_parent">

    </cjz.project.maptry4.MapView>
</FrameLayout>

 

最终效果、可“无限”延伸的单元格:

可以缩放:

下一步,就是让它可以实际的动态保存、加载对应单元格的位图内容了

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值