【问题标题】:Extending RelativeLayout, and overriding dispatchDraw() to create a zoomable ViewGroup扩展 RelativeLayout,并覆盖 dispatchDraw() 以创建可缩放的 ViewGroup
【发布时间】:2011-09-16 18:13:38
【问题描述】:

我见过一些人问如何一次性缩放整个 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 策略是否有点失败?还有其他方法吗?

谢谢,

特雷夫

【问题讨论】:

  • 经过大量阅读,我现在的印象是,这与最初为 View 确定的显示区域/“无效区域”/剪辑区域有关。我试过使用 android:clipChildren="false" 但没有效果。
  • 放大后是否尝试在层次结构查看器中检查子视图?
  • 要确认您的无效区域理论,请尝试安装“开发工具”应用程序(com.android.development,应该已经安装在模拟器中),打开应用程序并转到“开发设置” ” > 勾选“显示屏幕更新”。这将导致由无效区域引起的任何屏幕更新以洋红色突出显示。至于您的用例,我认为在以任何方式缩放视图时为视图设置动画会很奇怪。尝试覆盖canAnimate(),如果放大则返回false?
  • Joe 和 matheeeny:非​​常感谢这些建议;非常感谢。与此同时,我已经找到了一个可行的解决方案(我在下面将其作为答案提出),但我现在将尝试使用您提到的开发工具,因为这将是值得体验的。

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


【解决方案1】:

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

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

【讨论】:

  • Hackbod : 你什么时候上线。如果您能提供帮助,我也需要您的帮助...我们可以在某个地方聊天吗?提前致谢
  • 不应该 setScaleX/Y 对视图组中的每个孩子都这样做吗?
【解决方案2】:

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 的有用指针。

【讨论】:

  • 什么是 FloorPlanLayout?当我使用你的代码但没有运行时,你能给出整个代码或一个例子吗?虽然你解释得很详细。
  • 什么是平面布置图?我也不明白。我通过阅读您的链接制作了自定义的 RelativeLayout。我的自定义相对布局表现为我的 7 个图像视图的父布局。现在面临的问题是? 1. 如果我将图像保留为 relativeLayout 的背景并且我尝试缩放它不会得到缩放,但是如果我将它作为 Imageview 并将其放置为自定义 RelativeLayout 的子级,它会使用 Why 自定义相对布局中的许多 imageViews 正确缩放不缩放设置为它的背景??
  • 对于任何偶然发现(或链接到此处)的人来说,FloorPlanLayout 是无关紧要的。它或多或少是最初设置比例的根视图容器,记录的视图是保存内容的中间视图。
猜你喜欢
  • 1970-01-01
  • 2016-07-14
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2021-10-28
  • 1970-01-01
  • 1970-01-01
  • 2020-09-09
相关资源
最近更新 更多