我见过有人问过如何一次性缩放整个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策略,我有点失败吗?还有另一种方式吗?
谢谢,
崔佛
答案 0 :(得分:14)
如果您正在为孩子的绘图应用比例因子,您还需要将适当的比例因子应用于与他们的所有其他交互 - 调度触摸事件,无效等。
因此除了dispatchDraw()之外,您还需要覆盖并适当调整至少这些其他方法的行为。要处理无效,您需要覆盖此方法以适当调整子坐标:
如果您希望用户能够与子视图进行交互,您还需要覆盖它以在调整触摸坐标之前适当调整它们:
此外,我强烈建议您在一个简单的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的有用指示。