2013年2月28日木曜日

[Android] ViewPager で、追加・削除を動的にこなしてみる。

以下の動作を実現したい!

① FragmentAをFragmentStatePagerAdapterに追加
② FragmentBをFragmentStatePagerAdapterに追加
③ FragmentBをFragmentStatePagerAdapterから削除
④ FragmentCをFragmentStatePagerAdapterに追加

サンプルなんかを見ながら、FragmentStatePagerAdapterを実装してみましたが、
③までは見た目上は上手くいくが、④を実行すると③で削除したインスタンスが復元してしまいます。


いろいろ調べた結果、以下のことがわかりました。(勘違いなら指摘ください)
1. FragmentStatePagerAdapterはFragmentStatePagerAdapter#getItemで生成されたFragmentのインスタンスをメンバフィールドのListに保持している。
2. FragmentStatePagerAdapter#getItemでいくら別のインスタンスを返却しようとも、置き換わることはない
3. FragmentStatePagerAdapter#instantiateItemの実装として、メンバフィールドの値を返却する。

上記を踏まえた場合、PagerAdapterから実装しないと無理ゲー。
仕方なく PagerAdapter を実装することにします。

PagerAdapterの実装
public class SectionsPagerAdapter  extends PagerAdapter {

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * Private instance fileds
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    private final Context context;

    private final List<Section> sections;

    private final FragmentManager fragmentManager;

    private FragmentTransaction currentTransaction = null;

    private ArrayList<Fragment.SavedState> savedStatuses = new ArrayList<Fragment.SavedState>();

    private ArrayList<Fragment> fragments = new ArrayList<Fragment>();

    private Fragment currentPrimaryItem = null;

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * public constructors
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    /**
     *
     * @param context
     */
    public SectionsPagerAdapter(Context context, FragmentManager fragmentManager) {
        this.context = context;
        this.sections = new ArrayList<Section>();
        this.fragmentManager = fragmentManager;
    }

    /**
     *
     * @param context
     */
    public SectionsPagerAdapter(Context context, FragmentManager fragmentManager, List<Section> fragmentPages) {
        this.context = context;
        this.sections = fragmentPages;
        this.fragmentManager = fragmentManager;
    }

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * FragmentStatePagerAdapter methods
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    public boolean isViewFromObject(View view, Object object) {
        return (((Fragment)object).getView() == view);
    }

    /** */
    @Override
    public int getCount() {
        return this.sections.size();
    }

    /** */
    @Override
    public CharSequence getPageTitle(int position) {
        return this.sections.get(position).getTitle();
    }

    @Override
    public int getItemPosition(Object object) {
        android.util.Log.v("SectionsPageAdapter", "getItemPosition");
        int index = this.fragments.indexOf(object);
        return index != -1 ? index : PagerAdapter.POSITION_NONE;
    }

    @Override
    public void finishUpdate(ViewGroup container) {
        if (this.currentTransaction != null) {
            this.currentTransaction.commitAllowingStateLoss();
            this.currentTransaction = null;
            this.fragmentManager.executePendingTransactions();
        }
    }

    @Override
    public void setPrimaryItem(ViewGroup container, int position, Object object) {
        Fragment fragment = (Fragment) object;
        if (fragment != currentPrimaryItem) {
            if (currentPrimaryItem != null) {
                currentPrimaryItem.setMenuVisibility(false);
            }

            if (fragment != null) {
                fragment.setMenuVisibility(true);
            }

            currentPrimaryItem = fragment;
        }
    }

    @Override
    public Object instantiateItem(ViewGroup container, int position) {
        android.util.Log.v("SectionsPageAdapter", "instantiateItem:" + position);
        android.util.Log.v("SectionsPageAdapter", "this.fragments.size():" + this.fragments.size());
        if (this.currentTransaction == null) {
            this.currentTransaction = this.fragmentManager.beginTransaction();
        }

        if (this.fragments.size() > position) {
            Fragment fragment = (Fragment)this.fragments.get(position);
            if (fragment != null) {
                return fragment;
            }
        }

        Fragment fragment = this.instantiateFragment(position);
        if (this.savedStatuses.size() > position) {
            Fragment.SavedState fss = this.savedStatuses.get(position);
            if (fss != null) {
                try {
                    fragment.setInitialSavedState(fss);
                } catch (Exception ex) {
                    // Schon aktiv (kA was das heißt xD)
                    ex.printStackTrace();
                }
            }
        }

        while (this.fragments.size() <= position) {
            this.fragments.add(null);
        }

        fragment.setMenuVisibility(false);
        this.fragments.set(position, fragment);
        this.currentTransaction.add(container.getId(), fragment);

        return fragment;
    }

    @Override
    public void destroyItem(ViewGroup container, int position, Object object) {
        android.util.Log.v("SectionsPageAdapter", "destroyItem:" + position);

        Fragment fragment = (Fragment)object;

        if (this.currentTransaction == null) {
            this.currentTransaction = this.fragmentManager.beginTransaction();
        }

        //
        /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
         * 削除対象のFragmentがsectionsに存在するかを確認する。
         * 同一であると識別するのは、
         * ・クラス名が一致する
         * ・argumentsが一致する
         * の二点を満たした場合。
         * - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */
        String fullName = fragment.getClass().getName();
        Bundle arguments = fragment.getArguments();

        boolean isExist = false;
        for (Section section : this.sections) {
            // フラグメントのクラス名が一致するか
            if (section.getFragmentClassName().equals(fullName)) {
                if (section.getArguments() == null && arguments == null) {
                    // 一致
                    isExist = true;
                } else if (section.getArguments() == null){
                    // NULL で 個数が0なら一致
                    isExist = arguments.keySet().size() == 0;
                } else if (arguments == null){
                    // NULL で 個数が0なら一致
                    isExist = section.getArguments().keySet().size() == 0;
                } else {
                    boolean isKeyMatch = true;

                    // 引数のキーを確認し、一致しない項目を探します。
                    for(String key : section.getArguments().keySet()) {
                        if (!arguments.containsKey(key)) {
                            // キーが欠落
                            isKeyMatch = false;
                            break;
                        }

                        if (!section.getArguments().get(key).equals(arguments.get(key))) {
                            // 値が不一致
                            isKeyMatch = false;
                            break;
                        }
                    }

                    isExist = isKeyMatch;
                }
            }

            // 存在した為、検索を中断する。
            if (isExist) {
                break;
            }
        }


        if (isExist) {
            while (this.savedStatuses.size() <= position) {
                this.savedStatuses.add(null);
            }

            this.savedStatuses.set(position, this.fragmentManager.saveFragmentInstanceState(fragment));

            while (this.fragments.size() <= position) {
                this.fragments.add(null);
            }

            this.fragments.set(position, null);
        } else {
        }

        this.currentTransaction.remove(fragment);
    }

    @Override
    public Parcelable saveState() {
        Bundle state = null;
        if (savedStatuses.size() > 0) {
            state = new Bundle();
            Fragment.SavedState[] fss = new Fragment.SavedState[savedStatuses.size()];
            savedStatuses.toArray(fss);
            state.putParcelableArray("states", fss);
        }

        for (int i = 0; i < fragments.size(); i++) {
            Fragment f = fragments.get(i);
            if (f != null) {
                if (state == null) {
                    state = new Bundle();
                }
                String key = "f" + i;
                fragmentManager.putFragment(state, key, f);
            }
        }

        return state;
    }

    @Override
    public void restoreState(Parcelable state, ClassLoader loader) {
        if (state != null) {
            Bundle bundle = (Bundle) state;
            bundle.setClassLoader(loader);
            Parcelable[] fss = bundle.getParcelableArray("states");
            savedStatuses.clear();
            fragments.clear();
            if (fss != null) {
                for (int i = 0; i < fss.length; i++) {
                    savedStatuses.add((Fragment.SavedState) fss[i]);
                }
            }
            Iterable<string> keys = bundle.keySet();
            for (String key : keys) {
                if (key.startsWith("f")) {
                    int index = Integer.parseInt(key.substring(1));
                    Fragment fragment = fragmentManager.getFragment(bundle, key);
                    if (fragment != null) {
                        while (fragments.size() <= index) {
                            fragments.add(null);
                        }
                        fragment.setMenuVisibility(false);
                        fragments.set(index, fragment);
                    } else {
                        android.util.Log.w("SectionsPageAdapter", "Bad fragment at key " + key);
                    }
                }
            }
        }
    }

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * public methods
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    /** */
    public Fragment instantiateFragment(int position) {
        if (this.sections.size() < position) {
            return null;
        }

        return Fragment.instantiate(
                this.context,
                this.sections.get(position).getFragmentClassName(),
                this.sections.get(position).getArguments());
    }

    /**
     * Sectionを追加する。
     *
     * @param resId タイトルに使う文字列リソースのID
     * @param fragmentClass フラグメントのクラス
     * @param arguments フラグメントを初期化するためのバンドル
     */
    public void addSection(int resId, Class fragmentClass, Bundle arguments) {
        this.addSection(this.context.getString(resId), fragmentClass, arguments);
    }

    /**
     * Sectionを追加する。
     *
     * @param title タイトル
     * @param fragmentClass フラグメントのクラス
     * @param arguments フラグメントを初期化するためのBundle
     */
    public void addSection(String title, Class fragmentClass, Bundle arguments) {
        this.sections.add(new Section(title, fragmentClass, arguments));

        // 新しくタブが追加されたことを通知しておく
        this.notifyDataSetChanged();
    }

    /**
     * Sectionを削除する
     * @param section 削除するSection
     */
    public void removePage(Section section) {
        if (this.getCount() == 1) { return; }

        int position = this.sections.indexOf(section);

        this.sections.remove(section);

        while (this.savedStatuses.size() <= position) {
            this.savedStatuses.add(null);
        }

        this.savedStatuses.remove(position);

        while (this.fragments.size() <= position) {
            this.fragments.add(null);
        }

        this.fragments.remove(position);

        this.notifyDataSetChanged();
    }

    /**
     * Sectionを削除する
     * @param position 削除するインデックス
     */
    public void removePage(int position) {
        if (this.getCount() == 1) { return; }

        this.sections.remove(position);

        while (this.savedStatuses.size() <= position) {
            this.savedStatuses.add(null);
        }

        this.savedStatuses.remove(position);

        while (this.fragments.size() <= position) {
            this.fragments.add(null);
        }

        this.fragments.remove(position);

        this.notifyDataSetChanged();
    }

    /**
     * 指定したSectionを取得する
     * @param position index
     * @return
     */
    public Section getSection(int position) {
        return this.sections.get(position);
    }

    /**
     * Section情報を全て取得する。
     * @return
     */
    public ArrayList<Section> getSections() {
        int size = this.getCount();
        ArrayList>Section< result = new ArrayList>Section<(size);
        for (int index = 0; index < size; index++) {
            result.add(this.sections.get(index));
        }

        return result;
    }

    /**
     * 指定されたSectionのindexを取得する。
     * @param section indexを取得するSection
     * @return
     */
    public int getSectionPosition(Section section) {
        return this.sections.indexOf(section);
    }
}

上記で使用しているデータクラス

public class Section  implements Parcelable {

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

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

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * Private instance fields
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    /**
     * 表示するタイトル
     */
    private String title;

    /**
     * フラグメントのクラス名
     */
    private String fragmentClassName;

    /**
     * フラグメントに設定する引数情報
     */
    private Bundle arguments;

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * Public Getter/Setter
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    /**
     * タイトルを取得します。
     * @return
     */
    public String getTitle() {
        return this.title;
    }

    /**
     * フラグメントに設定する引数情報を取得します。
     * @return
     */
    public Bundle getArguments() {
        return this.arguments;
    }

    /**
     * フラグメントのクラス名を取得します。
     */
    public String getFragmentClassName() {
        return this.fragmentClassName;
    }

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * Private constructors
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    private Section(Parcel in) {
        this.title = in.readString();
        this.fragmentClassName = in.readString();
        this.arguments = in.readBundle();
    }

    /*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*
     * public constructors
     *-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*-*/

    /**
     *
     * @param title
     * @param fragmentClass
     * @param arguments
     */
    public Section(String title, Class fragmentClass, Bundle arguments) {
        this.title = title;
        this.fragmentClassName = fragmentClass.getName();
        this.arguments = arguments;
    }

    /*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*
     * Parcelable methods
     *=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*=*/

    /** */
    @Override
    public int describeContents() {
        return 0;
    }

    /** */
    @Override
    public void writeToParcel(Parcel out, int flags) {
        out.writeString(this.title);
        out.writeString(this.fragmentClassName);
        out.writeBundle(this.arguments);
    }
}


これで追加削除が自由自在!ひゃっはー!
もっと楽な方法がある気がします。