Android

右端から始まる横スクロールView

右端から始まる横スクロールView

・やりたいこと


横スクロール可能なViewで初期位置一番右から表示したい

・最初にやったこと


HorizontalScrollViewの子にしてfullScroll(FOCUS_RIGHT)を使う。

※問題点
子Viewが可変幅のViewであり、遅延して0⇒通常サイズに変わる場合、一瞬左端が表示される
解決するにはレイアウトが成立してからDrawされる間にスクロールさせる必要がある

解決方法


HorizontalScrollViewをカスタムしてスクロール位置を割合で覚えさせて初期化する。

public class HorizontalRTLScrollView extends HorizontalScrollView {

    private static Map<String, Integer> idMap = new HashMap<>();
    private int instanceId;
    private String TAG;

    private float scrolledPos = 0.0f;

    private SavedState mSavedState;

    /**
     * コンストラクタ
     *
     * @param context
     * @param attrs
     */
    public HorizontalRTLScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);

        TAG = "SCROLL";
        Log.d(TAG, "SampleScrollView");
    }

    public void setIdKey(String keyId) {
        if (!idMap.containsKey(keyId)) {
            instanceId = View.generateViewId();
            idMap.put(keyId, instanceId);
            Log.d(TAG, "SetId:" + keyId + "generate");
        } else {
            Log.d(TAG, "SetId:" + keyId);
        }
        TAG = "SCROLL(" + keyId + ")";
        setId(instanceId);
    }

    @Override
    protected void onScrollChanged(int l, int t, int oldl, int oldt) {
        super.onScrollChanged(l, t, oldl, oldt);

        int childWidth = 0;
        // 子Viewがいればポジション管理
        if (getChildCount() > 0) {
            childWidth = getChildAt(0).getMeasuredWidth();
            // 右端からの移動位置を割合で保持
            scrolledPos = ((float) childWidth - (float) l) / (float) childWidth;
            // 復元値はもう使わない
            mSavedState = null;
        }
        Log.d(TAG, "onScrollChanged:" + oldl + ">" + l + ":" + scrolledPos + "" + (childWidth * scrolledPos) + "/" + childWidth);
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {
        super.onLayout(changed, l, t, r, b);
        Log.d(TAG, "onLayout" + l + "," + t + "," + r + "," + b);

        int childWidth = 0;
        // 子Viewがいればポジション管理
        if (getChildCount() > 0) {
            childWidth = getChildAt(0).getMeasuredWidth();
            // 復元値があれば思い出す。
            if (mSavedState != null) {
                scrolledPos = mSavedState.scrollOffsetPercentFromStart;
            }
            // 元の位置に移動
            this.scrollTo((int) (childWidth - (childWidth * scrolledPos)), 0);

        }
        Log.d(TAG, "CHANGE!!:" + scrolledPos + ":" + (childWidth * scrolledPos) + "/" + childWidth);

    }


    @Override
    protected void onRestoreInstanceState(Parcelable state) {
        if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            // Some old apps reused IDs in ways they shouldn't have.
            // Don't break them, but they don't get scroll state restoration.
            super.onRestoreInstanceState(state);
            return;
        }
        SavedState ss = (SavedState) state;
        super.onRestoreInstanceState(ss.getSuperState());
        mSavedState = ss;
        requestLayout();
    }

    @Override
    protected Parcelable onSaveInstanceState() {
        if (getContext().getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
            // Some old apps reused IDs in ways they shouldn't have.
            // Don't break them, but they don't get scroll state restoration.
            return super.onSaveInstanceState();
        }
        Parcelable superState = super.onSaveInstanceState();
        SavedState ss = new SavedState(superState);
        int childWidth = 0;
        ss.scrollOffsetPercentFromStart = 0;
        if (getChildCount() > 0) {
            childWidth = getChildAt(0).getMeasuredWidth();
            ss.scrollOffsetPercentFromStart = scrolledPos;
        }
        return ss;
    }

    static class SavedState extends BaseSavedState {
        public float scrollOffsetPercentFromStart;
        public String beforeName = "";

        SavedState(Parcelable superState) {
            super(superState);
        }

        public SavedState(Parcel source) {
            super(source);
            scrollOffsetPercentFromStart = source.readInt();
        }

        @Override
        public void writeToParcel(Parcel dest, int flags) {
            super.writeToParcel(dest, flags);
            dest.writeFloat(scrollOffsetPercentFromStart);
        }

        @Override
        public String toString() {
            return "HorizontalScrollView.SavedState{"
                    + Integer.toHexString(System.identityHashCode(this))
                    + " scrollPosition=" + scrollOffsetPercentFromStart
                    + "}";
        }

        public static final Parcelable.Creator<SavedState> CREATOR
                = new Parcelable.Creator<SavedState>() {
            public SavedState createFromParcel(Parcel in) {
                return new SavedState(in);
            }

            public SavedState[] newArray(int size) {
                return new SavedState[size];
            }
        };
    }
}

・途中経過


1.Viewには右から左への表示モードがある。
RTL言語用の表示モードがある。
HorizontalScrollViewのソースを除いているとRTLモードをisLayoutRtl()を参照して右端から開始してるので使えそう!
指定できそうなsetLayoutDirection(LAYOUT_DIRECTION_RTL)を親から呼んでみるも右から始まらない。なぜか?面倒なので追わない。

2.子Viewのサイズが変わると右端へ移動するので「移動量」を覚えておく
このカスタムHorizontalScrollViewのonLayoutfullScroll(FOCUS_RIGHT)をすると子Viewのサイズが変わると右端へ移動する。
ここで可変幅であることがネック。
スクロール位置はもともとHorizontalScrollViewが管理してくれているが左端からの位置。
サイズ変動しても右端から始めたいので、右端からの「移動量」を覚えておく必要がある。

3.画面回転したときに位置が初期位置に戻ってしまう。
画面回転で親Viewごとインスタンスが再作成されるため移動量も初期化される。
標準のScrollViewとか保持してるけどどうしてるんだろうと調べたらInstanceStateとやらを保存しているらしい。

参考:アクティビティの再作成

onRestoreInstanceStateonSaveInstanceStateを使ってViewの状態を保存できるっぽい。
HorizontalScrollViewがスクロール位置の保持に使っているのでコードをほとんどそのまま引用。

これでいいだろと思ったら、よくよく見ると回転での復元位置が不安定。
回転で幅が変わってるのに移動量で覚えてたらそりゃずれる。


この保存方法ってViewのIDを使って保存しているらしい。
このカスタムViewをViewPagerのフラグメント内の子Viewとして扱うとinfrateのたびに同じIDのViewが複数生まれることになるが、
きちんとpositionごとのフラグメントがそれぞれ復元してくれるらしい。

4.移動量の割合化
まあ最終的には親View側でコンテンツの位置を探して指定できるようにすると思うけどある程度は追従するようにする。
なので内部で割合で持たせる。
割合でもコンテンツのサイズ変更の比率がまちまちなのでずれはでる。

最終的には親Viewが現在の位置からコンテンツの位置を取得し、
そのコンテンツのレイアウト変更後の位置をこのカスタムHorizontalScrollViewに教えてスクロールしてもらうしかないだろう。
今回は対応しない。

・他のやり方の検討


・View自体にスクロール機能を実装する
GestureDetector mGestureDetectorでスクロール処理を入れる方法がある
だが毎回OnDrawで描画することになるので負荷が高い

⇒ScrollViewとかはどうやっているのか?
OnDrawは実装されておらず下位Viewの位置をずらすだけ
下位ViewはOnDrawを一回だけしか呼ばれない
androidフレームワークが下位ViewのCanvasを保持してるので必要な部分だけを表示してくれてるようだ

⇒canvasを二重に持っておく
⇒canvasからcanvasへのコピーが出来ないので無意味
ならばあらかじめ画像として保持しておく
⇒当然スクロール領域全部の画像は大きすぎてOutOfMemoryになる。