扩展RelativeLayout,并覆盖dispatchDraw()以创建可缩放的ViewGroup

时间:2011-06-16 21:32:47

标签: android android-layout android-relativelayout android-viewgroup

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

然而,在这之前,我试图变得聪明(哈 - 通常是一个坏主意)并将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策略,我有点失败吗?还有另一种方式吗?

谢谢,

崔佛

2 个答案:

答案 0 :(得分: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()中,您希望在应用缩放因子后保存画布状态。这可以确保孩子无法修改您设置的转换。

答案 1 :(得分: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。相反,每个子视图在通过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的有用指示。