16

カスタム ビューのサイズ変更とレイアウトの問題が発生し続けており、誰かが「ベスト プラクティス」のアプローチを提案できるかどうか疑問に思っています。問題は次のとおりです。コンテンツに必要な高さがビューの幅に依存するカスタム ビューを想像してみてください (複数行の TextView に似ています)。(明らかに、これは高さがレイアウト パラメーターによって固定されていない場合にのみ適用されます。) 問題は、特定の幅に対して、これらのカスタム ビューでコンテンツの高さを計算するのにかなりのコストがかかることです。特に、UI スレッドで計算するにはコストがかかりすぎるため、ある時点でワーカー スレッドを起動してレイアウトを計算し、完了したら UI を更新する必要があります。

問題は、これをどのように設計するかです。いくつかの戦略を考えました。それらはすべて、高さが計算されるたびに、対応する幅が記録されると想定しています。

最初の戦略は、次のコードに示されています。

protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int width = measureWidth(widthMeasureSpec);
    setMeasuredDimension(width, measureHeight(heightMeasureSpec, width));
}

private int measureWidth(int widthMeasureSpec) {
    // irrelevant to this problem
}

private int measureHeight(int heightMeasureSpec, int width) {
    int result;
    int specMode = MeasureSpec.getMode(measureSpec);
    int specSize = MeasureSpec.getSize(measureSpec);
    if (specMode == MeasureSpec.EXACTLY) {
        result = specSize;
    } else {
        if (width != mLastWidth) {
            interruptAnyExistingLayoutThread();
            mLastWidth = width;
            mLayoutHeight = DEFAULT_HEIGHT;
            startNewLayoutThread();
        }
        result = mLayoutHeight;
        if (specMode == MeasureSpec.AT_MOST && result > specSize) {
            result = specSize;
        }
    }
    return result;
}

レイアウト スレッドが終了すると、Runnable が UI スレッドにポストさmLayoutHeightれ、計算された高さに設定されてからrequestLayout()(and invalidate()) が呼び出されます。

2 番目の戦略は、 (レイアウト スレッドを起動せずに) その時点onMeasureの値を常に使用することです。mLayoutHeight幅の変更のテストとレイアウト スレッドの起動は、オーバーライドによって行われますonSizeChanged

3 番目の戦略は、遅延して、(必要に応じて) でレイアウト スレッドを起動するのを待つことonDrawです。

必要な高さをできるだけ早く計算しながら、レイアウト スレッドが起動および/または強制終了される回数を最小限に抑えたいと考えています。への呼び出しの数も最小限に抑えるとよいでしょうrequestLayout()

ドキュメントからonMeasure、単一のレイアウトの過程で複数回呼び出される可能性があることは明らかです。onSizeChanged数回呼び出される可能性があることはあまり明確ではありませんが (可能性は高いと思われます) 。ですから、ロジックを入れるonDraw方がより良い戦略かもしれないと考えています。しかし、それはカスタムビューのサイジングの精神に反しているように見えるので、私はそれに対して明らかに不合理な偏見を持っています.

他の人も同じ問題に直面したに違いありません。私が見逃したアプローチはありますか?最善のアプローチはありますか?

4

5 に答える 5

10

Androidのレイアウトシステムは、このような問題を解決するように設計されていなかったと思います。おそらく、問題を変更することをお勧めします。

とは言うものの、ここでの中心的な問題は、あなたのビューが実際にそれ自体の高さを計算する責任を負わないということだと思います。子の寸法を計算するのは常にビューの親です。彼らは自分たちの「意見」を表明することができますが、結局のところ、実際の生活と同じように、彼らは実際には問題について何の発言権も持っていません。

これは、ビューの親、つまり、ディメンションが子のディメンションから独立している最初の親を確認することをお勧めします。その親は、すべての子が測定フェーズを終了するまで(別のスレッドで発生します)、子のレイアウト(したがって描画)を拒否する可能性があります。すぐに、親は新しいレイアウトフェーズを要求し、子を再度測定することなく子をレイアウトします。

子の測定が親の測定に影響を与えないことが重要です。これにより、子を再測定せずに2番目のレイアウトフェーズを「吸収」して、レイアウトプロセスを解決できます。

[編集] これを少し拡張すると、実際には1つの小さな欠点しかない非常に単純なソリューションを考えることができます。AsyncViewを拡張ViewGroupして、と同様にScrollView、常にそのスペース全体を埋める1つの子のみを含むように単純に作成できます。はAsyncView、子の測定値を自分のサイズとは見なさず、理想的には利用可能なスペースを埋めるだけです。子の測定呼び出しを別のスレッドでラップするだけでAsyncView、測定が完了するとすぐにビューにコールバックします。

そのビューの中には、他のレイアウトを含め、ほとんど何でも配置できます。「問題のあるビュー」が階層内でどれほど深いかは実際には問題ではありません。唯一の欠点は、すべての子孫が測定されるまで、どの子孫もレンダリングされないことです。ただし、ビューの準備ができるまで、何らかの読み込みアニメーションを表示することをお勧めします。

「問題のあるビュー」は、マルチスレッドに関係する必要はありません。他のビューと同じように、必要なだけ時間をかけて自分自身を測定できます。

[edit2] 私は簡単な実装を一緒にハックすることさえわざわざしました:

package com.example.asyncview;

import android.content.Context;
import android.os.AsyncTask;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;

public class AsyncView extends ViewGroup {
    private AsyncTask<Void, Void, Void> mMeasureTask;
    private boolean mMeasured = false;

    public AsyncView(Context context) {
        super(context);
    }

    public AsyncView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        for(int i=0; i < getChildCount(); i++) {
            View child = getChildAt(i);
            child.layout(0, 0, child.getMeasuredWidth(), getMeasuredHeight());
        }
    }

    @Override
    protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) {
        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
        if(mMeasured)
            return;
        if(mMeasureTask == null) {
            mMeasureTask = new AsyncTask<Void, Void, Void>() {
                @Override
                protected Void doInBackground(Void... objects) {
                    for(int i=0; i < getChildCount(); i++) {
                        measureChild(getChildAt(i), widthMeasureSpec, heightMeasureSpec);
                    }
                    return null;
                }

                @Override
                protected void onPostExecute(Void aVoid) {
                    mMeasured = true;
                    mMeasureTask = null;
                    requestLayout();
                }
            };
            mMeasureTask.execute();
        }
    }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        if(mMeasureTask != null) {
            mMeasureTask.cancel(true);
            mMeasureTask = null;
        }
        mMeasured = false;
        super.onSizeChanged(w, h, oldw, oldh);
    }
}

実用的な例については、https://github.com/wetblanket/AsyncViewを参照してください

于 2013-02-07T17:14:43.510 に答える
0

この戦略のようなこともできます:

カスタム子ビューを作成:

public class CustomChildView extends View
{
    MyOnResizeListener orl = null; 
    public CustomChildView(Context context)
    {
        super(context);
    }
    public CustomChildView(Context context, AttributeSet attrs)
    {
        super(context, attrs);
    }
    public CustomChildView(Context context, AttributeSet attrs, int defStyle)
    {
        super(context, attrs, defStyle);
    }

    public void SetOnResizeListener(MyOnResizeListener myOnResizeListener )
    {
        orl = myOnResizeListener;
    }

    @Override
    protected void onSizeChanged(int xNew, int yNew, int xOld, int yOld)
    {
        super.onSizeChanged(xNew, yNew, xOld, yOld);

        if(orl != null)
        {
            orl.OnResize(this.getId(), xNew, yNew, xOld, yOld);
        }
    }
 }

そして、次のようなカスタム リスナーを作成します。

public class MyOnResizeListener
 {
    public MyOnResizeListener(){}

    public void OnResize(int id, int xNew, int yNew, int xOld, int yOld){}
 }

次のようにリスナーをインスタンス化します。

Class MyActivity extends Activity
{
      /***Stuff***/

     MyOnResizeListener orlResized = new MyOnResizeListener()
     {
          @Override
          public void OnResize(int id, int xNew, int yNew, int xOld, int yOld)
          {
/***Handle resize event and call your measureHeight(int heightMeasureSpec, int width) method here****/
          }
     };
}

そして、リスナーをカスタム ビューに渡すことを忘れないでください。

 /***Probably in your activity's onCreate***/
 ((CustomChildView)findViewById(R.id.customChildView)).SetOnResizeListener(orlResized);

最後に、次のようにして CustomChildView を XML レイアウトに追加できます。

 <com.demo.CustomChildView>
      <!-- Attributes -->
 <com.demo.CustomChildView/>
于 2013-02-07T17:58:31.217 に答える
0

OS 提供のウィジェット、OS 提供のイベント/コールバック システム、OS 提供のレイアウト/制約管理、ウィジェットへの OS 提供のデータ バインディングがあります。総称して、OS 提供の UI API と呼んでいます。OS が提供する UI API の品質に関係なく、アプリの機能を使用して効果的に解決できない場合があります。異なるアイデアを念頭に置いて設計されている可能性があるためです。したがって、OS が提供するものの上に、タスク指向の UI API を抽象化して独自に作成するための適切なオプションが常にあります。独自の自己提供 API を使用すると、さまざまなウィジェット サイズを計算して保存し、より効率的に使用できます。ボトルネック、インタースレッディング、コールバックなどの削減または除去。このようなレイヤー API の作成は数分で済む場合もあれば、プロジェクト全体の最大の部分になるほど高度な場合もあります。長期間にわたるデバッグとサポートには多少の労力がかかる場合があります。これがこのアプローチの主な欠点です。しかし、利点は、あなたの考え方が変わるということです。「制限を回避する方法を見つける方法」ではなく、「作成するのが熱い」と考え始めます。「ベストプラクティス」についてはわかりませんが、これはおそらく最もよく使用されるアプローチの 1 つです。「私たちは私たちのものを使っています」とよく耳にします。自分で作ったものと提供された大物のどちらかを選択するのは簡単ではありません. 正気を保つことが重要です。「私たちは私たちのものを使っています」とよく耳にします。自分で作ったものと提供された大物のどちらかを選択するのは簡単ではありません. 正気を保つことが重要です。「私たちは私たちのものを使っています」とよく耳にします。自分で作ったものと提供された大物のどちらかを選択するのは簡単ではありません. 正気を保つことが重要です。

于 2013-02-09T05:41:47.320 に答える
0

Android は開始時に実際のサイズを認識していないため、計算する必要があります。完了するonSizeChanged()と、実際のサイズで通知されます。

onSizeChanged()サイズが計算されると呼び出されます。イベントは常にユーザーから来る必要はありません。Androidがサイズを変更すると、onSizeChanged()呼び出されます。と同じことがonDraw()、ビューが描画されるべきときにonDraw()呼び出されます。

onMeasure()への呼び出しの直後に自動的に呼び出されますmeasure()

あなたのための他の関連リンク

ところで、ここで興味深い質問をしていただきありがとうございます。お力になれて、嬉しいです。:)

于 2013-02-07T18:08:39.197 に答える