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);
    }
}


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

2012年6月20日水曜日

[Android][入門] DialogにViewを設定する際にDialog規定のテーマを適応する

タイトルだけだと何を言ってるかわからないと思います。すみません、日本語が不得手なので。

サクッと本題。

ダイアログにレイアウトファイルから生成したビューを設定するとします。

【レイアウトファイル】
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" >
    <TextView
        android:layout_height="wrap_content"
        android:layout_width="wrap_content"
        android:text="Hello !" />
</LinearLayout>

【MyActivity クラス】
public class MyActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        LayoutInflater inflater =
                (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        View view = inflater.inflate(R.layout.sample, null);
        new AlertDialog.Builder(this)
        .setView(view)
        .create()
        .show();
    }
}

【AndroidManifest.xml】
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.goodfornothing.android.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".MyActivity"
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Black">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>


出力結果はこうなります。

そりゃそうだという感じ。

ではThemeを @android:style/Theme.Light に変更した場合はどうなるでしょうか。

【AndroidManifest.xml】
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.goodfornothing.android.test"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" />

    <application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name" >
        <activity
            android:name=".MyActivity"
            android:label="@string/app_name"
            android:theme="@android:style/Theme.Light">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>
</manifest>



思ったダイアログと別物です。誰ですか。
文字色が暗すぎるため、なんだかとても見辛くなりました。

これはActivityのThemeの文字色に引きずられているためです。
AlertDialog#getLayoutInflater() より取得した LayoutInflater を通して View を生成すると、期待通りのDialogが表示されます。


【MyActivity クラス】
public class MyActivity extends Activity {
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        AlertDialog dialog = new AlertDialog.Builder(this).create();

        LayoutInflater inflater = dialog.getLayoutInflater();
        View view = inflater.inflate(R.layout.sample, null);

        dialog.setView(view);
        dialog.show();
    }
}



期待通りですね。

2012年6月13日水曜日

DPI値を取得する方法

// DPI値を取得する
TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 6, this.getResources().getDisplayMetrics())

2012年6月11日月曜日

AndroidでSilverlightのWrapPanel風のViewGroupが欲しい その2

前回作ったコントロールでは、動的にadd もしくは remove した時に正常に動作しませんでした。


今回は ViewGroup を直接継承し実装してみました。
子要素の配置を行うためいに ViewGroup#onLayout をオーバーライドします。
ここで View#getWidth の戻り値は 0 となってしまうため、 View#getMeasuredWidthを使用します。
ですが、素の状態では View#getMeasuredWidth の戻り値も 0 となり、コントロールのサイズを取得できません。
そこでまず ViewGroup#onMeasure をオーバーライドし子要素に対してView#measureを呼び出します。


以下の呪文を記述
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = View.resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        final int height = View.resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        this.setMeasuredDimension(width, height);

        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
        int childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).measure(childWidthSpec, childHeightSpec);
        }
    }

これを書けば、View#getMeasuredWidthで値が取得できるようになります。
ちゃんと理解してはいない。

あとは前回 View#onWindowFocusChangedのなかに書いてた事をView#onLayoutに記述してやればOK。

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int count = this.getChildCount();

        // 行の高さ
        int rowHeight = 0;

        // 現在の行の幅
        int currentRowWidth = 0;

        int rowMaxWidth = this.getWidth();

        int top = 0;

        for (int i = 0; i < count; i++) {
            View child = this.getChildAt(i);
            if (child.getVisibility() != View.GONE) {

                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                if (rowMaxWidth < currentRowWidth + childWidth) {
                    // はみ出すので演算をやり直す。
                    // 次行の位置を設定
                    top += rowHeight < childHeight ? childHeight : rowHeight;
                    // 現在行の高さを初期化
                    rowHeight = 0;
                    // 現在行の幅を初期化
                    currentRowWidth = 0;
                }

                // コントロールを適切な位置に配置
                child.layout(
                    currentRowWidth,
                    top,
                    currentRowWidth + childWidth,
                    top + childHeight);

                // 行の高さを更新する必要があるか
                rowHeight =
                    rowHeight < childHeight ? childHeight : rowHeight;

                // 現在のコントロール幅を合計値に加算する
                currentRowWidth += childWidth;
            }
        }
    }

良い感じ良い感じ。

実際にはMarginなんかが設定されるハズ。
なの以下の クラス をInner Classとして作成。
    public static class LayoutParams extends MarginLayoutParams {

        public LayoutParams(Context context, AttributeSet attr) { super(context, attr); }

        public LayoutParams(int width, int height) { super(width, height); }

        public LayoutParams(ViewGroup.LayoutParams source) { super(source); }

        public LayoutParams(MarginLayoutParams source) { super(source); }
    }

MarginLayoutParams を継承しただけ。今後を考えてInner Classとした。
このクラスを定義しただけで、(WrapLayout.LayoutParams)child.getLayoutParams() なんてした日には楽しい結末(ClassCastException)が待っているので、 以下の Method を Override
・ ViewGroup#generateLayoutParams
・ ViewGroup#generateDefaultLayoutParams
・ ViewGroup#checkLayoutParams
・ ViewGroup#generateLayoutParams
    @Override
    public WrapLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new WrapLayout.LayoutParams(getContext(), attrs);
    }

    @Override
    protected WrapLayout.LayoutParams generateDefaultLayoutParams() {
        return new WrapLayout.LayoutParams(WrapLayout.LayoutParams.WRAP_CONTENT, WrapLayout.LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof WrapLayout.LayoutParams;
    }

    @Override
    protected WrapLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new WrapLayout.LayoutParams(p);
    }

設定された margin を使って配置を行うように ViewGroup#onLayout に修正を加える。
    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int count = this.getChildCount();

        // 行の高さ
        int rowHeight = 0;

        // 現在の行の幅
        int currentRowWidth = 0;

        int rowMaxWidth = this.getWidth();

        int top = 0;

        for (int i = 0; i < count; i++) {
            View child = this.getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                WrapLayout.LayoutParams lp =
                        (WrapLayout.LayoutParams)child.getLayoutParams();

                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                int childTotaWidth = childWidth + lp.rightMargin + lp.leftMargin;
                int childTotalHeight = childHeight + lp.topMargin + lp.bottomMargin;

                if (rowMaxWidth < currentRowWidth + childTotaWidth) {
                    // はみ出すので演算をやり直す。
                    // 高さを設定
                    top += rowHeight < childTotalHeight ? childTotalHeight : rowHeight;
                    // 現在行の高さを初期化
                    rowHeight = 0;
                    // 現在行の幅を初期化
                    currentRowWidth = 0;
                }

                child.layout(
                    currentRowWidth + lp.leftMargin,
                    top + lp.topMargin,
                    currentRowWidth + childWidth + lp.rightMargin,
                    top + childHeight + lp.bottomMargin);

                rowHeight =
                    rowHeight < childTotalHeight ? childTotalHeight : rowHeight;
                currentRowWidth += childTotaWidth;
            }
        }
    }

これでMarginが有効になったWrapLayoutの出来上がり。

ソースコード全文(import等は割愛)

public class WrapLayout extends ViewGroup {

    //==================================================================
    // constructor
    //==================================================================
    public WrapLayout(Context context) { super(context); }

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

    public WrapLayout(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); }

    //==================================================================
    // public methods
    //==================================================================

    @Override
    public WrapLayout.LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new WrapLayout.LayoutParams(getContext(), attrs);
    }

    //==================================================================
    // protected methods
    //==================================================================
    @Override
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        final int width = View.resolveSize(getSuggestedMinimumWidth(), widthMeasureSpec);
        final int height = View.resolveSize(getSuggestedMinimumHeight(), heightMeasureSpec);
        this.setMeasuredDimension(width, height);

        int childWidthSpec = MeasureSpec.makeMeasureSpec(width, MeasureSpec.UNSPECIFIED);
        int childHeightSpec = MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED);

        int childCount = getChildCount();
        for (int i = 0; i < childCount; i++) {
            getChildAt(i).measure(childWidthSpec, childHeightSpec);
        }
    }

    @Override
    protected void onLayout(boolean changed, int l, int t, int r, int b) {

        int count = this.getChildCount();

        // 行の高さ
        int rowHeight = 0;

        // 現在の行の幅
        int currentRowWidth = 0;

        int rowMaxWidth = this.getWidth();

        int top = 0;

        for (int i = 0; i < count; i++) {
            View child = this.getChildAt(i);
            if (child.getVisibility() != View.GONE) {
                WrapLayout.LayoutParams lp =
                        (WrapLayout.LayoutParams)child.getLayoutParams();

                int childWidth = child.getMeasuredWidth();
                int childHeight = child.getMeasuredHeight();

                int childTotaWidth = childWidth + lp.rightMargin + lp.leftMargin;
                int childTotalHeight = childHeight + lp.topMargin + lp.bottomMargin;

                if (rowMaxWidth < currentRowWidth + childTotaWidth) {
                    // はみ出すので演算をやり直す。
                    // 高さを設定
                    top += rowHeight < childTotalHeight ? childTotalHeight : rowHeight;
                    // 現在行の高さを初期化
                    rowHeight = 0;
                    // 現在行の幅を初期化
                    currentRowWidth = 0;
                }

                child.layout(
                    currentRowWidth + lp.leftMargin,
                    top + lp.topMargin,
                    currentRowWidth + childWidth + lp.rightMargin,
                    top + childHeight + lp.bottomMargin);

                rowHeight =
                    rowHeight < childTotalHeight ? childTotalHeight : rowHeight;
                currentRowWidth += childTotaWidth;
            }
        }
    }

    @Override
    protected WrapLayout.LayoutParams generateDefaultLayoutParams() {
        return new WrapLayout.LayoutParams(WrapLayout.LayoutParams.WRAP_CONTENT, WrapLayout.LayoutParams.WRAP_CONTENT);
    }

    @Override
    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
        return p instanceof WrapLayout.LayoutParams;
    }

    @Override
    protected WrapLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
        return new WrapLayout.LayoutParams(p);
    }

    //==================================================================
    // inner class
    //==================================================================

    public static class LayoutParams extends MarginLayoutParams {

        public LayoutParams(Context context, AttributeSet attr) { super(context, attr); }

        public LayoutParams(int width, int height) { super(width, height); }

        public LayoutParams(ViewGroup.LayoutParams source) { super(source); }

        public LayoutParams(MarginLayoutParams source) { super(source); }
    }
}

2012年6月6日水曜日

AndroidでSilverlightのWrapPanel風のViewGroupが欲しい

AndroidでSilverlightのWrapPanelのように自動で折り返すViewGroupが欲しいです。
デフォルトじゃないみたいなので、いろいろ漁ってると以下の記事を発見。

Androidでボタンを横に並べて自動で折り返す

ほほー。RelativeLayoutでござるか。

使 っ た こ と な い ん で す よ ね 。

上記の記事を使いまわしが効くようにコントロール化してみました。
かなり適当。
public class WrapLayout extends RelativeLayout {

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

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

    public WrapLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
    }

    @Override
    public void addView(View child) {
        // idが未設定の場合は乱数でどうにかする。
        if (child.getId() == -1) { child.setId(new Random(System.currentTimeMillis()).nextInt(Integer.MAX_VALUE)); }

        super.addView(child);
    }

    @Override
    public void addView(View child, int width, int height) {
        // idが未設定の場合は乱数でどうにかする。
        if (child.getId() == -1) { child.setId(new Random(System.currentTimeMillis()).nextInt(Integer.MAX_VALUE)); }

        super.addView(child, width, height);
    }

    @Override
    public void addView(View child, ViewGroup.LayoutParams params) {
        // idが未設定の場合は乱数でどうにかする。
        if (child.getId() == -1) { child.setId(new Random(System.currentTimeMillis()).nextInt(Integer.MAX_VALUE)); }

        super.addView(child, params);
    }

    @Override
    public void addView(View child, int index) {
        // idが未設定の場合は乱数でどうにかする。
        if (child.getId() == -1) { child.setId(new Random(System.currentTimeMillis()).nextInt(Integer.MAX_VALUE)); }

        super.addView(child, index);
    }

    @Override
    public void addView(View child, int index, ViewGroup.LayoutParams params) {
        // idが未設定の場合は乱数でどうにかする。
        if (child.getId() == -1) { child.setId(new Random(System.currentTimeMillis()).nextInt(Integer.MAX_VALUE)); }

        super.addView(child, index, params);
      }

    @Override
    public void onWindowFocusChanged(boolean hasWindowFocus) {

        super.onWindowFocusChanged(hasWindowFocus);

        // 子供の数を取得
        int l = this.getChildCount();
        // 無いなら何もしない
        if (l == 0) {
          return;
        }

        // ディスプレイの横幅を取得
        WindowManager wm = (WindowManager) this.getContext().getSystemService(Context.WINDOW_SERVICE);
        Display display = wm.getDefaultDisplay();

        // 1行最大長
        int max = display.getWidth();

        // 一番最初は基点となるので何もしない
        View pline = this.getChildAt(0);
        // 現在の1行の長さ
        int currentTotal = pline.getWidth();
        for (int i = 1; i < l; i++) {
            int w = this.getChildAt(i).getWidth();
            RelativeLayout.LayoutParams layoutParams =
                    (RelativeLayout.LayoutParams) this.getChildAt(i).getLayoutParams();

            // 横幅を超えないなら前のボタンの右に出す
            if (max > currentTotal + w) {
                // 現在長に処理対象コントロール長を加算
                currentTotal += w;
                layoutParams.addRule(RelativeLayout.ALIGN_TOP, this.getChildAt(i - 1).getId());
                layoutParams.addRule(RelativeLayout.RIGHT_OF, this.getChildAt(i - 1).getId());
            } else {
                // 横最大長を超過した場合は折り返す
                layoutParams.addRule(RelativeLayout.BELOW, pline.getId());

                // 基点を変更
                pline = this.getChildAt(i);

                // 長さをリセット
                currentTotal = pline.getWidth();
            }
        }
    }
}


わかってます。onWindowFocusChangedにaddViewしたらダメじゃね?ってのはわかってます。
とりあえずです。
[[Activity]]
public class MainActivity extends Activity {
    /** Called when the activity is first created. */

    String[] strings = { "てst1", "てst2", "てst3", "てst4", "てst5", "てst6", "てst7", "てst8", "てst9", "てst10", "てst111111a", "てst111", "てst234"};
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        WrapLayout layout = (WrapLayout)this.findViewById(R.id.FixedGridLayout);
        for (String string : strings) {
            Button btn = new Button(this);
            btn.setText(string);
            //btn.setId(random.nextInt(Integer.MAX_VALUE));
            layout.addView(btn);
        }
    }
}
[[出力結果]]

良い感じ。

2012年6月5日火曜日

[Silverlight] UIスレッド上で処理を実行する。

System.Windows.Deployment.Current.Dispatcher.BeginInvoke(() =>{ .... });

Triggerでシーケンス値を自動採番


以下のようなテーブルが存在したとする。
テーブル名 : [MST_Magazine]
フィールド

項目名備考
ID数値PK 連番でデータを格納
Publisher_ID数値
Name文字列



このようなテーブルではIDはシーケンスでの採番となる。
しかし、OracleではSQLServerのように、型として自動採番の型が存在しない。

そこで、トリガを用いて自動採番を行う。

CREATE OR REPLACE TRIGGER TRG_MST_Magazine_ID
BEFORE Insert
ON MST_Magazine
FOR EACH ROW
BEGIN
    IF :NEW.ID  IS NULL THEN
        SELECT SEQ_Magazine_ID.NEXTVAL INTO :NEW.ID FROM DUAL;
    END IF;
END;

上記のトリガは、MST_MagazineテーブルへのInsert時に起動し、
InsertされたMST_Magazine.ID項目がNULLの場合、SEQ_Magazine_ID.Nextvalの値を使用する。

これにより、IDを指定せずMST_MagazineテーブルへのInsertを行った場合は自動で数値が採番される。