24

我见过一些人问如何一次缩放整个 ViewGroup(例如 RelativeLayout)。目前这是我需要实现的目标。对我来说,最明显的方法是将缩放比例因子作为单个变量保存在某处。并且在每个子视图的 onDraw() 方法中,该比例因子将在绘制图形之前应用于画布。

然而,在这样做之前,我试图变得聪明(哈哈——通常是个坏主意)并将RelativeLayout扩展到一个名为ZoomableRelativeLayout的类中。我的想法是,任何缩放转换都可以在覆盖的 dispatchDraw() 函数中只应用于 Canvas 一次,因此绝对不需要在任何子视图中单独应用缩放。

这是我在 ZoomableRelativeLayout 中所做的。它只是 RelativeLayout 的一个简单扩展,其中 dispatchDraw() 被覆盖:

protected void dispatchDraw(Canvas canvas){         
    canvas.save(Canvas.MATRIX_SAVE_FLAG);       
    canvas.scale(mScaleFactor, mScaleFactor);
    super.dispatchDraw(canvas);     
    canvas.restore();       
}

mScaleFactor 由同一类中的 ScaleListener 操作。

它确实有效。我可以捏缩放 ZoomableRelativeLayout,并将所有视图正确地重新缩放在一起。

除非有问题。其中一些子视图是动画的,因此我会定期对它们调用 invalidate() 。当比例为 1 时,可以看到这些子视图定期重绘非常好。当比例不是 1 时,这些动画子视图只能在其查看区域的一部分中看到更新 - 或根本不更新 - 取决于缩放比例。

我最初的想法是,当调用单个子视图的 invalidate() 时,它可能会被系统单独重绘,而不是从父 RelativeLayout 的 dispatchDraw() 传递 Canvas,这意味着子视图最终会自行刷新没有应用缩放比例。但奇怪的是,在屏幕上重绘的子视图的元素是正确的缩放比例。就好像系统决定在支持位图中实际更新的区域保持未缩放 - 如果这有意义的话。换句话说,如果我有一个动画子视图,并且我从初始比例 1 逐渐放大,并且如果我们在缩放比例为 1 时在该子视图所在的区域放置一个假想的框, 那么对 invalidate() 的调用只会导致该假想框中的图形刷新。但是看到更新的图形正在以正确的规模进行。如果您放大到现在子视图已经完全远离它的比例为 1 的位置,那么它的任何部分都不会被刷新。我再举一个例子:想象我的子视图是一个通过在黄色和红色之间切换来动画的球。如果我稍微放大一点,使球向右和向下移动,在某个点上,您只会看到球的左上角四分之一的动画颜色。

如果我不断放大和缩小,我会看到子视图正确且完整地动画化。这是因为正在重绘整个 ViewGroup。

我希望这是有道理的; 我已经尽力解释了。我的可缩放 ViewGroup 策略是否有点失败?还有其他方法吗?

谢谢,

特雷夫

4

2 回答 2

14

hackbod 的出色回答提醒我,我需要发布我最终得出的解决方案。请注意,这个解决方案适用于我当时正在做的应用程序,可以通过 hackbod 的建议进一步改进。特别是我不需要处理触摸事件,并且在阅读 hackbod 的帖子之前,我没有想到如果我这样做了,那么我也需要扩展这些事件。

回顾一下,对于我的应用程序,我需要实现的是有一个大图(特别是建筑物的楼层布局),上面叠加了其他小的“标记”符号。背景图和前景符号都是使用矢量图形绘制的(即在 onDraw() 方法中应用于 Canvas 的 Path() 和 Paint() 对象)。想要以这种方式创建所有图形而不是仅使用位图资源的原因是因为图形是在运行时使用我的 SVG 图像转换器进行转换的。

要求是图表和相关的标记符号都将是 ViewGroup 的子项,并且都可以一起缩放。

很多代码看起来很乱(这是一个演示的匆忙工作),所以我不只是将它们全部复制进去,而是尝试解释我是如何使用引用的相关代码位来完成它的。

首先,我有一个 ZoomableRelativeLayout。

public class ZoomableRelativeLayout extends RelativeLayout { ...

此类包括扩展 ScaleGestureDetector 和 SimpleGestureListener 的侦听器类,以便可以平移和缩放布局。这里特别感兴趣的是缩放手势侦听器,它设置缩放因子变量,然后调用 invalidate() 和 requestLayout()。我目前不确定是否需要 invalidate() ,但无论如何 - 这里是:

private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener {

@Override
public boolean onScale(ScaleGestureDetector detector){
    mScaleFactor *= detector.getScaleFactor();
    // Apply limits to the zoom scale factor:
    mScaleFactor = Math.max(0.6f, Math.min(mScaleFactor, 1.5f);
    invalidate();
    requestLayout();
    return true;
    }
}

我在 ZoomableRelativeLayout 中必须做的下一件事是重写 onLayout()。为此,我发现查看其他人对可缩放布局的尝试很有用,而且我发现查看RelativeLayout 的原始Android 源代码也很有用。我重写的方法复制了RelativeLayout 的onLayout() 中的大部分内容,但做了一些修改。

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b)
{
    int count = getChildCount();
    for(int i=0;i<count;i++){
        View child = getChildAt(i); 
        if(child.getVisibility()!=GONE){
            RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams)child.getLayoutParams();
            child.layout(
                (int)(params.leftMargin * mScaleFactor), 
                (int)(params.topMargin * mScaleFactor), 
                (int)((params.leftMargin + child.getMeasuredWidth()) * mScaleFactor), 
                (int)((params.topMargin + child.getMeasuredHeight()) * mScaleFactor) 
                );
        }
    }
}

这里重要的是,当对所有孩子调用“layout()”时,我也将比例因子应用于这些孩子的布局参数。这是解决裁剪问题的一步,重要的是它正确地设置了不同比例因子的孩子相对于彼此的 x,y 位置。

另一个关键是我不再尝试在 dispatchDraw() 中缩放 Canvas。相反,每个子 View 在通过 getter 方法从父 ZoomableRelativeLayout 获取缩放因子后缩放其 Canvas。

接下来,我将继续我在 ZoomableRelativeLayout 的子视图中必须做的事情。我在 ZoomableRelativeLayout 中只包含一种类型的 View 作为子项;这是一个用于绘制 SVG 图形的视图,我称之为 SVGView。当然 SVG 的东西在这里是不相关的。这是它的 onMeasure() 方法:

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {


    int widthMode = MeasureSpec.getMode(widthMeasureSpec);
    int widthSize = MeasureSpec.getSize(widthMeasureSpec);

    int heightMode = MeasureSpec.getMode(heightMeasureSpec);
    int heightSize = MeasureSpec.getSize(heightMeasureSpec);

    float parentScale = ((FloorPlanLayout)getParent()).getScaleFactor();
    int chosenWidth, chosenHeight;
    if( parentScale > 1.0f){
        chosenWidth =  (int) ( parentScale * (float)svgImage.getDocumentWidth() );
        chosenHeight = (int) ( parentScale * (float)svgImage.getDocumentHeight() );
    }
    else{
        chosenWidth =  (int) (  (float)svgImage.getDocumentWidth() );
        chosenHeight = (int) (  (float)svgImage.getDocumentHeight() );          
    }

    setMeasuredDimension(chosenWidth, chosenHeight);
}

和 onDraw():

@Override
protected void onDraw(Canvas canvas){   
    canvas.save(Canvas.MATRIX_SAVE_FLAG);       

    canvas.scale(((FloorPlanLayout)getParent()).getScaleFactor(), 
            ((FloorPlanLayout)getParent()).getScaleFactor());       


    if( null==bm  ||  bm.isRecycled() ){
        bm = Bitmap.createBitmap(
                getMeasuredWidth(),
                getMeasuredHeight(),
                Bitmap.Config.ARGB_8888);


        ... Canvas draw operations go here ...
    }       

    Paint drawPaint = new Paint();
    drawPaint.setAntiAlias(true);
    drawPaint.setFilterBitmap(true);

    // Check again that bm isn't null, because sometimes we seem to get
    // android.graphics.Canvas.throwIfRecycled exception sometimes even though bitmap should
    // have been drawn above. I'm guessing at the moment that this *might* happen when zooming into
    // the house layout quite far and the system decides not to draw anything to the bitmap because
    // a particular child View is out of viewing / clipping bounds - not sure. 
    if( bm != null){
        canvas.drawBitmap(bm, 0f, 0f, drawPaint );
    }

    canvas.restore();

}

再说一次——作为免责声明,我在那里发布的内容可能有一些缺陷,我还没有仔细阅读 hackbod 的建议并将它们纳入其中。我打算回来进一步编辑。同时,我希望它可以开始为其他人提供有关如何实现可缩放 ViewGroup 的有用指针。

于 2011-06-28T18:24:57.253 回答
14

如果您将比例因子应用于孩子的绘图,您还需要将适当的比例因子应用于与他们的所有其他交互——调度触摸事件、无效等。

因此,除了 dispatchDraw() 之外,您还需要重写并适当调整至少这些其他方法的行为。要处理无效,您需要重写此方法以适当地调整子坐标:

http://developer.android.com/reference/android/view/ViewGroup.html#invalidateChildInParent(int[], android.graphics.Rect)

如果您希望用户能够与子视图交互,您还需要覆盖它以在将触摸坐标分配给子视图之前适当地调整触摸坐标:

http://developer.android.com/reference/android/view/ViewGroup.html#dispatchTouchEvent(android.view.MotionEvent )

此外,我强烈建议您在一个简单的 ViewGroup 子类中实现这一切,该子类具有它管理的单个子视图。这将消除 RelativeLayout 在其自己的 ViewGroup 中引入的任何复杂行为,从而简化您在自己的代码中需要处理和调试的内容。将 RelativeLayout 作为特殊缩放 ViewGroup 的子级。

最后,对代码的一项改进——在 dispatchDraw() 中,您希望在应用缩放因子后保存画布状态。这样可以确保孩子无法修改您设置的转换。

于 2011-06-27T08:53:04.110 回答