如何在地图应用程序中实现稳定的缩放

2024-03-31

我正在实现一个具有点击、拖动和缩放功能的应用程序,类似于 Google 地图。我已经设法实现平移和缩放,但是缩放点当前位于坐标处0,0。放大和缩小时,网格在坐标处的位置0,0保持固定,而所有其他坐标都距离该点更近/更远。

相反,我希望能够实现稳定的缩放,其中缩放点是当前鼠标位置下的位置。要了解我要查找的内容,请打开 Google 地图,将鼠标放在特定点上方,然后使用鼠标滚轮滚动。请注意鼠标下方的位置如何保持固定。

我该如何修改zoomGrid本例中的函数来实现尊重鼠标位置的鼠标滚轮缩放?

function zoomGrid(mouseEvent) {
  var delta = mouseEvent.deltaY;
  if (mouseEvent.deltaMode == 1) { //Firefox scrolls by line instead of by pixel so multiply the delta by 20
    delta *= 20;
  }
  zoom += delta;
  zoom = Math.min(zoom, 3000);
  zoom = Math.max(zoom, -1000);
  scale = Math.pow(2,(zoom / 1000));

  var mousePos = {x: mouseEvent.offsetX, y: mouseEvent.offsetY};
  //gridPos = ???

  drawGrid();
  drawShapes();
}

完整演示:http://codepen.io/alexspurling/pen/jApazY http://codepen.io/alexspurling/pen/jApazY

(PS我已经看过了paper.js 教程 http://matthiasberth.com/articles/stable-zoom-and-pan-in-paperjs/但无法将其中的逻辑转换为可行的代码)。


这是一些可以实现您想要的功能的代码..

这个答案有点过头了,但我的时间有点短,无法删除不需要的东西。

该演示允许您缩放、平移和旋转。它首先加载一个图像(大小合理,因此 GPU RAM 较低的设备可能不喜欢它并且运行缓慢)。

汽车图像加载完毕后。

  • 左键拖动进行平移
  • 滚轮可放大和缩小
  • 向下的右键设置旋转原点,然后左右拖动即可旋转。
  • 中键将重置视图

仅使用绝对坐标进行渲染。如果您有一个大型数据集,则可以在剔除渲染调用时使用显示变换角作为世界坐标视图边界。

重要的功能位于顶部。

  • display每帧调用一次并处理所有渲染
  • displayTransform是负责平移、缩放和旋转的对象。它有一些评论,但没有解释,所以如果您遇到问题,请随时提问
  • onResize在去抖调整大小事件中调用
  • startup在启动时调用一次并设置 displayTransform
  • 底部的样板代码处理鼠标和画布,可以忽略。

mouse是保存鼠标状态的全局对象。mouse.x, mouse.y是画布坐标mouse.buttonRaw是位字段,如果按钮按下,则位打开。参见下一段mouse.w轮子是-120、0还是120

displayTransform.mouseWorldX and displayTransform.mouseWorldY保存鼠标在图像上的位置(因为这与画布坐标不匹配);displayTransform.corners是一个长度为 8 的数组,画布角的坐标为 [x1,y1,...,x4,y4],从左上角沿顺时针方向投影到世界(图像)坐标上。您可以使用它们来绘制网格并剔除视图外部的渲染调用。

我没有添加任何鼠标按钮常量,因此当您看到与鼠标按钮有关的任何内容时,0 为左侧,1 为中间,2 为右侧作为 ID。检查鼠标时单击mouse.buttonRaw是一个位字段,其中位 1 位于左侧,位 2 位于中间,位 3 位于右侧。仅屏蔽您感兴趣的按钮 mouse.buttonRaw & 1is left&2middle and&4` 对了。

var startup = function(){
    displayTransform.ctx = ctx;
    displayTransform.mouse = mouse;
    displayTransform.setMouseRotate(2); // set rotate funtion to button 3
    displayTransform.setMouseTranslate(0); 
    displayTransform.setWheelZoom();     
    img = new Image();
    img.src = "https://upload.wikimedia.org/wikipedia/commons/e/e5/Fiat_500_in_Emilia-Romagna.jpg"
}
var img;
var onResize = function(){
    ctx.font = "14px verdana";
    ctx.textAlign = "center";
    ctx.textBaseline = "middle";
    if(img.complete){
        displayTransform.fitView(0,0,img.width,img.height,"fit");
    }
}


var stillTime = 0;
const MOUSE_STILL_TIME = 1000;

function display(){
    displayTransform.update();// update the transform
    displayTransform.setDefault();// set home transform to clear the screem
    ctx.clearRect(0,0,canvas.width,canvas.height);
    // if the image loaded show it
    if(img.complete){
        if(displayTransform.quiet || (mouse.buttonRaw & 2)){
            stillTime += 1;
            if(stillTime > MOUSE_STILL_TIME || (mouse.buttonRaw & 2)){
                stillTime = 0;
                displayTransform.fitView(0,0,img.width,img.height,"fit")
            }
        }else{
            stillTime = 0;
        }
        displayTransform.setTransform();
        ctx.drawImage(img,0,0);
    }else{
        // waiting for image to load
        displayTransform.setTransform();
        ctx.fillText("Loading image...",100,100);
    }
}


var displayTransform = (function(){
        
    const buttons = [1, 2, 4];
    
    // create a location description.
    // x and y is the position of the (where on the canvas the transformed point 0,0 will end up)
    // origin x,y is the location that zooms, rotations will be centered on.
    // scale is the scale (zoom) large numbers are zooming in small is zoom out. 1 is 1pixel = 1pixel
    // rotation is rotation. 0 id From left to right across the screen with positives values rotation
    // clockwise. Values are in radians
    var location = function (x, y, originX, originY, scale, rotation){
        return {
            x      : x,
            y      : y,
            ox     : originX,
            oy     : originY,
            scale  : scale,
            rotate : rotation,
        };
    }
    
    // returns an array to hold the transformation matrix
    // if a is undefined then returns the Identity (default) matrix
    var matrix = function (a, b, c, d, e, f){
        if(a === undefined){
            return [1, 0, 0, 1, 0, 0];
        }
        return [a, b, c, d, e, f];
    }
    
    // set the ctx transformation 
    var setTransform = function(){ 
        var m, i;
        m = this.matrix;
        i = 0;
        this.ctx.setTransform(m[i ++], m[i ++], m[i ++], m[i ++], m[i ++], m[i ++]);
    }
    
    // uses chase values to smooth out transformations and then sets the matrix and invMatrix
    // The inverMatrix is used to transform a point from world space to screen space.
    var smoothTransform = function(){
        var a, g, d, c, l, cross, m, im;
        // create short vars for code clarity
        a  = this.acceleration;
        g  = this.drag;
        l  = this.location;
        c  = this.locationChaser;
        d  = this.locationDelta;
        m  = this.matrix;
        im = this.invMatrix;
        
        // update the chasing value. Explination of code below
        // d += (l - c) * a;  // accelerate the delta
        // d *= g;            // apply the drag
        // c += d;            // add the new delta to the chasing value
        
        c.x      += (d.x      = (d.x      += (l.x      - c.x      ) * a ) * g);
        c.y      += (d.y      = (d.y      += (l.y      - c.y      ) * a ) * g);
        c.ox     += (d.ox     = (d.ox     += (l.ox     - c.ox     ) * a ) * g);
        c.oy     += (d.oy     = (d.oy     += (l.oy     - c.oy     ) * a ) * g);
        c.scale  += (d.scale  = (d.scale  += (l.scale  - c.scale  ) * a ) * g);
        c.rotate += (d.rotate = (d.rotate += (l.rotate - c.rotate ) * a ) * g);
        
        
        // use x and y movement to determin if the display has reached its position
        this.quiet = false;
        if(Math.abs(c.x - l.x) < 0.1 && Math.abs(c.y - l.y) < 0.1 && Math.abs(c.rotate - l.rotate) < 0.001 ){
            if(Math.abs(d.x) < 0.1 && Math.abs(d.y) < 0.1 && Math.abs(d.rotate) < 0.001){
                this.quiet = true;
            }
        }

        // calculate the matrix which is two vectors representing the X and Y axis
        // the Y axis is 90Deg counter clockwise from the X
        // To rotate a vector (v1) 90deg to (v2)
        // v2.x = -v1.y;  
        // v2.y =  v1.x;
        // m[0],m[1] is the X axies vector and m[2],m[3] is the Y axis vector
        m[3] =   m[0] = Math.cos(c.rotate) * c.scale;
        m[2] = -(m[1] = Math.sin(c.rotate) * c.scale);
        
        // transform the x,y position around the origin and add to the matrix
        m[4] = -(c.x * m[0] + c.y * m[2]) + c.ox;
        m[5] = -(c.x * m[1] + c.y * m[3]) + c.oy;
        
        // caculate the invers transformation
        
        // first get the cross product of x axis and y axis
        cross = m[0] * m[3] - m[1] * m[2];
        
        // now get the inverted axies
        im[0] =  m[3] / cross;
        im[1] = -m[1] / cross;
        im[2] = -m[2] / cross;
        im[3] =  m[0] / cross;
        im[4] = (m[1] * m[5] - m[3] * m[4]) / cross;
        im[5] = (m[2] * m[4] - m[0] * m[5]) / cross;        
        
        // all done for mow
    }

    // Activates mouse translate on button mouseButton 0 = main (left click) 1 = middle 2 = right
    var setUpMouseTranslate = function(mouseButton){
        this.mouseAction[mouseButton] = this.mouseTranslate.bind(this);
        this.mouseActionOff[mouseButton] = undefined;
    }
    // Does mouse drag translation 
    var mouseTranslate = function (mouse) {
        var mdx, mdy;
        
        // get the mouse delta
        var mdx = mouse.x - this.mouseLastX; // get the mouse movement
        var mdy = mouse.y - this.mouseLastY; // get the mouse movement
        
        // Transform the mouse delta to world space and move the 
        // world position
        this.location.x -= (mdx * this.invMatrix[0] + mdy * this.invMatrix[2]);
        this.location.y -= (mdx * this.invMatrix[1] + mdy * this.invMatrix[3]);   
    }
    
    // Set up mouse rotation on mouseButton 0 = main (left click) 1 = middle 2 = right
    // User clicks and drags. When a distance 14 pixels is reached the angle from the
    // start to that positoin is the referance. The user then drags around the
    // start point to rotate the world
    var setUpMouseRotate = function(mouseButton){
        // extra data needed to do the rotation
        this.rotationData = {
            rotateStart : false,      // the rotation has just started
            rotateOX    : 0,          // the screen start location of the rottae
            rotateOY    : 0,
            startAng    : undefined,  // the starting world rotatoin
            lastAng     : undefined,  // last angle input. Used to track cyclic rotation
            rotFrom     : undefined,  // the starting draged angle. 
        }
        this.mouseAction[mouseButton] = this.mouseRotate.bind(this);
        this.mouseActionOff[mouseButton] = (function(){
            this.rotationData.rotateStart = true;
        }).bind(this);
    }
    
    // Does the mouse drag rotation
    var mouseRotate = function (mouse) {
        var loc, mbx, mby, dist, rot, rd;
        loc = this.location;
        rd = this.rotationData;
        // is this the start of a rotation gesture
        // set the start location and the current rotation
        if(rd.rotateStart){
            rd.rotateStart = false;
            rd.rotateOX = mouse.x;
            rd.rotateOY = mouse.y;
            loc.ox = mouse.x;
            loc.oy = mouse.y;
            loc.x = this.mouseWorldX;
            loc.y = this.mouseWorldY;   
            rd.startAng = loc.rotate;
            rd.lastAng = undefined;
            rd.rotFrom = undefined;

            
        }
        // get mouse movement since start
        mdx = mouse.x - rd.rotateOX; 
        mdy = mouse.y - rd.rotateOY;
        dist = Math.hypot(mdy, mdx);
        if(dist > 14){   // tollerance (too close and the rotation goes all over thr plavce)
            rot = Math.atan2(mdy, mdx);  // get the angle from the start of the geusture to the mouse
            if(rd.lastAng === undefined){  // if the last ang is not avalible us the current angle
                rd.lastAng = rot;
                rd.rotFrom = rot;
            }
            // need to compensate for where atan2 goes from -Math.PI to Math.PI
            // adds 360 or subtracts 360 depending on which way around the user is draggin the mouse
            // can fail but I have been using this method for over 5 years
            // and have never had a problem
            if(rd.lastAng < -Math.PI / 2 && rot > Math.PI / 2 ){
                rd.startAng -= Math.PI * 2;
            }
            if(rd.lastAng > Math.PI / 2 && rot < -Math.PI / 2 ){
                rd.startAng += Math.PI * 2
            }
            loc.rotate = (rot-rd.rotFrom) + rd.startAng;
            rd.lastAng = rot; 
        }
        
    }
    

    // turns on wheel zoom
    var setWheelZoom = function(){
        this.mouseWheel = this.mouseWheelZoom;
    }
    
    // does wheel zoom
    var mouseWheelZoom = function (mouse) {
        var loc;
        loc = this.location;
        loc.ox = mouse.x;
        loc.oy = mouse.y;
        loc.x = this.mouseWorldX;
        loc.y = this.mouseWorldY;
        if(mouse.w > 0){ // zoom in
            loc.scale *= this.scaleSpeed;
            mouse.w -= 20;
            if(mouse.w < 0){
                mouse.w = 0;
            }
        }
        if(mouse.w < 0){ // zoom out
            loc.scale *= this.invScaleSpeed;
            mouse.w += 20;
            if(mouse.w > 0){
                mouse.w = 0;
            }
        }
    }
    



    // fits a location bound by x1,y1 and x2,y2 to fit within the 
    // canvas display
    // type "fit" will ensure that all the area is displayed. There my be gaps 
    //            above and below or left and right
    //      "fill" will ensure that the area fills the canva. there may be some 
    //             cliping to the sides of top. The image will be centered
    var setLocation = function (x1, y1, x2, y2, type){
        var w,h, vw, vh, loc;
        loc = this.location;
        w = this.ctx.canvas.width;
        h = this.ctx.canvas.height;
        loc.ox = w/2;
        loc.oy = h/2;
        vw = x2 - x1;
        vh = y2 - y1;
        if(type === "fit"){
            loc.scale = Math.min( w / vw, h / vh);
        }else{
            loc.scale = Math.max( w / vw, h / vh);
        }
        loc.x = (x1 + x2) / 2;// - (1 / loc.scale) * (w / 2);
        loc.y = (y1 + y2) / 2;// - (1 / loc.scale) * (h / 2);

        loc.rotate = Math.round(loc.rotate / (Math.PI * 2)) * Math.PI * 2;
    }
    // fits a location defined by x,y center and dx,dy the direction anddistance
    // to the right side

    var setOrientation = function (x, y, dx, dy){
        var w,h, vx, vy, loc, ang, size;
        loc = this.location;
        w = this.ctx.canvas.width;
        h = this.ctx.canvas.height;
        loc.ox = w/2;
        loc.oy = h/2;
        vx = dx - x;
        vy = dy - y;
        loc.rotate =- Math.atan2(vy, vx);
        size = Math.hypot(vx ,vy);
        loc.scale = w / (size*2);
        vx /= (size*2);
        vy /= (size*2);
        w = (1/loc.scale) * (w );
        h = (1/loc.scale) * (h );
        loc.x = x;// - w * vx - h * -vy;
        loc.y = y;// - w * vy - h * vx;
    }


    // update transformation should be called once per frame
    // Smooths and sets the transform on the current context (ctx).
    // if There is a mouse avalilble then get the mouse world position
    // and apply mouse gestures to update the world space.
    var updateWorld = function () {
        var msx, msy, im, m, loc, mouse, but, i, im0, im1, im2, im3, im4, im5, cor;
        but = buttons;
        m   = this.matrix;
        im  = this.invMatrix;
        loc = this.locationChaser;
        cor = this.corners;
        
        this.transform(); // update and set matrix
        
        im0 = im[0];
        im1 = im[1];
        im2 = im[2];
        im3 = im[3];
        im4 = m[4];
        im5 = m[5];
        
        
        if(this.mouse !== undefined){
            mouse = this.mouse;
            // caculate the mouse world coordinates
            msx = mouse.x - im4;
            msy = mouse.y - im5;
            this.mouseWorldX = (msx * im0 + msy * im2);
            this.mouseWorldY = (msx * im1 + msy * im3);                 
            i = 0;
            // do any mouse actions
            while( i < 3){
                if(this.mouseAction[i] !== undefined){
                    if((mouse.buttonRaw & but[i]) === but[i]){
                        this.mouseAction[i](mouse);
                    }else
                    if(this.mouseActionOff[i] !== undefined){
                        this.mouseActionOff[i](mouse);
                    }
                }
                i++;
            }
            if(this.mouseWheel !== undefined){
                if(mouse.w !== 0){
                    this.mouseWheel(mouse);
                }
            }
    
            // caculate the mouse world coordinates
            msx = mouse.x - im4;
            msy = mouse.y - im5;
            this.mouseWorldX = (msx * im0 + msy * im2);
            this.mouseWorldY = (msx * im1 + msy * im3);     

            
            
            
            // save old mouse position as the mouse events may occure more
            // offtent than the frame update. As we need the last position 
            // we used we stash the values here
            this.mouseLastX = mouse.x;
            this.mouseLastY = mouse.y;        
            
        }

        msx = -im4;
        msy = -im5;
        cor[0] = (msx * im0 + msy * im2);
        cor[1] = (msx * im1 + msy * im3);     
        msx =  this.ctx.canvas.width - im4;
        msy =  this.ctx.canvas.height - im5;
        cor[4] = (msx * im0 + msy * im2);
        cor[5] = (msx * im1 + msy * im3);     
        msx =   - im4;
        msy =  this.ctx.canvas.height - im5;
        cor[6] = (msx * im0 + msy * im2);
        cor[7] = (msx * im1 + msy * im3);         
        msx =  this.ctx.canvas.width - im4;
        msy =  - im5;
        cor[2] = (msx * im0 + msy * im2);
        cor[3] = (msx * im1 + msy * im3);   
        this.invScale = 1/loc.scale;
        this.pixelXx = im0;
        this.pixelXy = im1;     
    }

    // terms.
    // Real space, real, r (prefix) refers to the transformed canvas space.
    // c (prefix), chase is the value that chases a requiered value
    var displayTransform = {
        mode             : "smooth",
        location         : location(0, 0, 0, 0, 1, 0),
        locationChaser   : location(0, 0, 0, 0, 1, 0),
        locationDelta    : location(0, 0, 0, 0, 1, 0),
        corners          : [0, 0, 0, 0, 0, 0, 0, 0],  // corners x,y start from top left to top right
        pixelXx          : 0,                         // the bot right to bot left
        pixelXy          : 0,
        transform        : smoothTransform,
        drag             : 0.1,  // drag for movements
        acceleration     : 0.7, // acceleration
        quiet            : false,   // this is true when most of the movement scaling and rotation have stopped
        matrix           : matrix(), // main matrix
        invMatrix        : matrix(), // invers matrix;
        mouseWorldX      : 0, // the mouse location in world space
        mouseWorldY      : 0, // the mouse location in world space
        mouseLastX       : 0, // the last mouse position in screen space
        mouseLastY       : 0,
        mouseAction      : [undefined, undefined, undefined],
        mouseActionOff   : [undefined, undefined, undefined],
        mouseWheel       : undefined,
        scaleSpeed       : 1.1,
        invScaleSpeed    : 1 / 1.1,
        mouseTranslate   : mouseTranslate,
        mouseRotate      : mouseRotate,
        mouseWheelZoom   : mouseWheelZoom,
        setMouseRotate   : setUpMouseRotate,
        setMouseTranslate: setUpMouseTranslate,
        setWheelZoom     : setWheelZoom,
        setTransform     : setTransform,
        setDefault       : function(){ this.ctx.setTransform(1, 0, 0, 1, 0, 0); },
        update           : updateWorld,
        fitView          : setLocation,
        orientView       : setOrientation,
        ctx              : undefined,
        mouse            : undefined,
        
    } 
    return displayTransform;
})();






//==================================================================================================
// The following code is support code that provides me with a standard interface to various forums.
// It provides a mouse interface, a full screen canvas, and some global often used variable 
// like canvas, ctx, mouse, w, h (width and height), globalTime
// This code is not intended to be part of the answer unless specified and has been formated to reduce
// display size. It should not be used as an example of how to write a canvas interface.
// By Blindman67
const U = undefined;const RESIZE_DEBOUNCE_TIME = 100;
var w,h,cw,ch,canvas,ctx,mouse,createCanvas,resizeCanvas,setGlobals,globalTime=0,resizeCount = 0; 
var L = typeof log === "function" ? log : function(d){ console.log(d); }
createCanvas = function () { var c,cs; cs = (c = document.createElement("canvas")).style; cs.position = "absolute"; cs.top = cs.left = "0px"; cs.zIndex = 1000; document.body.appendChild(c); return c;}
resizeCanvas = function () {
    if (canvas === U) { canvas = createCanvas(); } canvas.width = window.innerWidth; canvas.height = window.innerHeight; ctx = canvas.getContext("2d"); 
    if (typeof setGlobals === "function") { setGlobals(); } if (typeof onResize === "function"){ resizeCount += 1; setTimeout(debounceResize,RESIZE_DEBOUNCE_TIME);}
}
function debounceResize(){ resizeCount -= 1; if(resizeCount <= 0){ onResize();}}
setGlobals = function(){ cw = (w = canvas.width) / 2; ch = (h = canvas.height) / 2; mouse.updateBounds(); }
mouse = (function(){
    function preventDefault(e) { e.preventDefault(); }
    var mouse = {
        x : 0, y : 0, w : 0, alt : false, shift : false, ctrl : false, buttonRaw : 0, over : false, bm : [1, 2, 4, 6, 5, 3], 
        active : false,bounds : null, crashRecover : null, mouseEvents : "mousemove,mousedown,mouseup,mouseout,mouseover,mousewheel,DOMMouseScroll".split(",")
    };
    var m = mouse;
    function mouseMove(e) {
        var t = e.type;
        m.x = e.clientX - m.bounds.left; m.y = e.clientY - m.bounds.top;
        m.alt = e.altKey; m.shift = e.shiftKey; m.ctrl = e.ctrlKey;
        if (t === "mousedown") { m.buttonRaw |= m.bm[e.which-1]; }  
        else if (t === "mouseup") { m.buttonRaw &= m.bm[e.which + 2]; }
        else if (t === "mouseout") { m.buttonRaw = 0; m.over = false; }
        else if (t === "mouseover") { m.over = true; }
        else if (t === "mousewheel") { m.w = e.wheelDelta; }
        else if (t === "DOMMouseScroll") { m.w = -e.detail; }
        if (m.callbacks) { m.callbacks.forEach(c => c(e)); }
        if((m.buttonRaw & 2) && m.crashRecover !== null){ if(typeof m.crashRecover === "function"){ setTimeout(m.crashRecover,0);}}        
        e.preventDefault();
    }
    m.updateBounds = function(){
        if(m.active){
            m.bounds = m.element.getBoundingClientRect();
        }
        
    }
    m.addCallback = function (callback) {
        if (typeof callback === "function") {
            if (m.callbacks === U) { m.callbacks = [callback]; }
            else { m.callbacks.push(callback); }
        } else { throw new TypeError("mouse.addCallback argument must be a function"); }
    }
    m.start = function (element, blockContextMenu) {
        if (m.element !== U) { m.removeMouse(); }        
        m.element = element === U ? document : element;
        m.blockContextMenu = blockContextMenu === U ? false : blockContextMenu;
        m.mouseEvents.forEach( n => { m.element.addEventListener(n, mouseMove); } );
        if (m.blockContextMenu === true) { m.element.addEventListener("contextmenu", preventDefault, false); }
        m.active = true;
        m.updateBounds();
    }
    m.remove = function () {
        if (m.element !== U) {
            m.mouseEvents.forEach(n => { m.element.removeEventListener(n, mouseMove); } );
            if (m.contextMenuBlocked === true) { m.element.removeEventListener("contextmenu", preventDefault);}
            m.element = m.callbacks = m.contextMenuBlocked = U;
            m.active = false;
        }
    }
    return mouse;
})();

// Clean up. Used where the IDE is on the same page.
var done = function(){
    window.removeEventListener("resize",resizeCanvas)
    mouse.remove();
    document.body.removeChild(canvas);    
    canvas = ctx = mouse = U;
    L("All done!")
}

resizeCanvas(); 
mouse.start(canvas,true); 
window.addEventListener("resize",resizeCanvas); 

function update(timer){ // Main update loop
    globalTime = timer;
    display();  // call demo code
    requestAnimationFrame(update);
}
requestAnimationFrame(update);
startup();

/** SimpleFullCanvasMouse.js end **/
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何在地图应用程序中实现稳定的缩放 的相关文章

随机推荐