web-dev-qa-db-ja.com

ズーム可能なViewGroupを作成するために、RelativeLayoutを拡張し、dispatchDraw()をオーバーライドします

ViewGroup全体(RelativeLayoutなど)を一度にズームする方法を尋ねる人が何人かいます。現時点では、これを達成する必要があります。私にとって最も明白なアプローチは、ズームスケールファクターを単一の変数としてどこかに保持することです。また、子ビューのonDraw()メソッドのそれぞれで、グラフィックが描画される前に、そのスケール係数がCanvasに適用されます。

ただし、それを行う前に、私は賢く(ha-通常は悪い考えです)、RelativeLayoutをZoomableRelativeLayoutというクラスに拡張しようとしました。私の考えでは、オーバーライドされたdispatchDraw()関数でCanvasにスケール変換を1回だけ適用できるため、子ビューでズームを個別に適用する必要はまったくありません。

これが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から徐々にズームインし、ズームスケールが1のときにその子ビューがある領域に架空のボックスを配置した場合の場合、invalidate()を呼び出すと、その架空のボックスのグラフィックが更新されるだけです。しかし、更新されるように見えるグラフィックスareは適切なスケールで実行されています。子ビューがスケール1の位置から完全に離れるまでズームインすると、その一部が更新されていないことがわかります。別の例を挙げましょう。私の子供のビューが、黄色と赤を切り替えることでアニメーション化するボールであると想像してください。ボールが右下に移動するように少しズームインすると、ある時点で、ボールの左上の4分の1が色をアニメーション化するのがわかります。

ズームインとズームアウトを続けると、子ビューが適切かつ完全にアニメーション化されます。これは、ViewGroup全体が再描画されているためです。

これが理にかなっていることを願っています。私はできる限り説明しようとしました。私はズーム可能なViewGroup戦略で少し敗者になっていますか?別の方法はありますか?

ありがとう、

トレブ

24
Trevor

お子様の描画に倍率を適用する場合は、タッチイベントのディスパッチ、無効化など、お子様との他のすべてのインタラクションにも適切な倍率を適用する必要があります。

したがって、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に導入している動作の複雑さが解消され、独自のコードで処理およびデバッグする必要があるものが簡素化されます。特別なズームViewGroupの子としてRelativeLayoutを配置します。

最後に、コードの1つの改善点です。dispatchDraw()で、スケーリング係数を適用した後にキャンバスの状態を保存します。これにより、子は設定した変換を変更できなくなります。

14
hackbod

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ソースコードを確認することも非常に便利でした。オーバーライドされたメソッドは多くをコピーします。相対レイアウトの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()」を呼び出すときに、それらの子のレイアウトパラメーターにもスケール係数を適用していることです。これは、クリッピングの問題を解決するための1つのステップです。また、さまざまなスケールファクターに対して、子のx、y位置を相互に正しく設定することも重要です。

さらに重要なことは、dispatchDraw()でCanvasをスケーリングしようとしないことです。代わりに、各子ビューは、getterメソッドを介して親ZoomableRelativeLayoutからスケール係数を取得した後、キャンバスをスケーリングします。

次に、ZoomableRelativeLayoutの子ビュー内で実行する必要があったことに移ります。 ZoomableRelativeLayoutに子として含まれているビューのタイプは1つだけです。これは、私がSVGViewと呼んでいるSVGグラフィックを描画するためのビューです。もちろん、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();

}

繰り返しますが、免責事項として、私がそこに投稿したものにはおそらくいくつかの疣贅があり、私はまだハックボッドの提案を注意深く調べてそれらを取り入れていません。戻ってきて、これをさらに編集するつもりです。それまでの間、ズーム可能なViewGroupを実装する方法について他の人に役立つポインタを提供し始めることができると思います。

14
Trevor