#面倒なAndroidのダイアログ
Androidのダイアログは昔はActivityクラスにonCreateDialogをオーバーライドして記述すればshowDialogメソッドなどで簡単に表示出来た。
しかし現在は非推奨APIとなっている上、見た目のカスタマイズが容易ではない。(Androidバージョンごとに違う方法でテーマを適用してやる必要がある。)
だから推奨されているFragmentを使うのが今流というわけだ。
#やっぱり面倒なダイアログ
しかしDialogFragment、これがまた面倒だった。
前時代のダイアログではちゃちゃっと記述すればデフォルトの質素なUIでダイアログを表示してくれたが、今回はDialogFragmentをめちゃくちゃオーバーライドしてやらないといけなかった。
もちろんダイアログの特性上、ダウンロード完了後などの非同期処理の完了後等に呼び出される事は考えないといけないのでアプリがバックグラウンドの状態で(アクティブじゃなかったり画面に映ってないとき)表示や非表示が呼び出されても落ちないようにしないといけない。
で、こういう面倒なコードって、せっかくDialogFragmentを継承するんだから、上記のタイミングで呼び出されても落ちないようにDialogFragmentに書いてあってほしいものだが不親切設計だった。
ちなみにDialogFragmentはFragmentのサブクラスでFragmentはiPhoneとかでいうとこのViewController的な存在。てかパクリ。
これらのメリットは対応するビューに関するコードをそのクラス内にまとめることで、ビューの外との依存を軽減し、ビュー単位で再利用しやすいものとなる。
Androidタブレットが出てきた時期に追加されたAPIだから、まさにiPad開発におけるViewControllerのあり方をそのまま持って来た感じ。
#面倒なDialogFragmentの継承クラスのコーディングにあたって
DialogFragmentに関しては、結構きっちり書いて行かないとすぐヌルポする。iOSの開発では比較的ラフにコーディングしても(ヌルポとかで)落ちないがAndroidは比較的超難儀なのだ。(Objective-CではNilに対してメッセージを送っても基本的に落ちないという言語上の性質もきっとある。)
#面倒なDialogFragmentの継承クラスのサンプル
自分も何十ものサイトを巡って重要な注意点やおいしい部分を集約し、なおかつ便利な改良を加えて行った。本当はこういうことはAPIで準備しておいて欲しいものだが・・・。
/*
* Copyright (c) Oct 2, 2013 ZYXW. All rights reserved.
*/
package (Package Name);
import android.app.Dialog;
import android.content.DialogInterface;
import android.graphics.Color;
import android.graphics.drawable.ColorDrawable;
import android.os.Bundle;
import android.support.v4.app.DialogFragment;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentTransaction;
import android.view.View;
import android.view.View.OnClickListener;
import android.view.Window;
import android.view.WindowManager;
import android.widget.TextView;
import (Package Name).R;
/**
* @author yumenosuke-k
* @category Fragment
* @since Oct 2, 2013
*/
public class MyDialogFragment extends DialogFragment implements OnClickListener {
// flags
public static final int NO_POSITIVE_BUTTON = 1 << 0;
public static final int NO_NEGATIVE_BUTTON = 1 << 1;
public static final int NO_CLOSE_BUTTON = 1 << 2;
public static final int ADD_NEUTRAL_BUTTON = 1 << 3;
private static final int LAYOUT = R.layout.dialog_content;
private static final int ID_TITLE = R.id.dialog_title_textview;
private static final int ID_MESSAGE = R.id.dialog_message_textview;
private static final int ID_POSITIVE_BUTTON = R.id.dialog_positive_button;
private static final int ID_NEGATIVE_BUTTON = R.id.dialog_negative_button;
private static final int ID_CLOSE_BUTTON = R.id.dialog_close_button;
private static final int ID_NEUTRAL_BUTTON = R.id.dialog_neutral_button;
private static final String TAG = "exclusive_dialog";
private DialogFragmentListener listener;
private int listenerId;
private boolean clickGuard = false;
private boolean dismissFlag = false;
/**
* Use this method as a constructor!!
*
* @return new instance
*/
public static final MyDialogFragment newInstance() {
return newInstance(true);
}
/**
* Use this method as a constructor!!
*
* @param cancelable
* whether the shown Dialog is cancelable.
* @return new instance
*/
public static final MyDialogFragment newInstance(boolean cancelable) {
return newInstance(0, cancelable);
}
/**
* Use this method as a constructor!!
*
* @param flags
* 0 ok, unless especially
* @return new instance
* @see #NO_POSITIVE_BUTTON
* @see #NO_NEGATIVE_BUTTON
* @see #NO_CLOSE_BUTTON
*/
public static final MyDialogFragment newInstance(int flags) {
return newInstance(flags, true);
}
/**
* Use this method as a constructor!!
*
* @param flags
* 0 ok, unless especially
* @param cancelable
* whether the shown Dialog is cancelable.
* @return new instance
* @see #NO_POSITIVE_BUTTON
* @see #NO_NEGATIVE_BUTTON
* @see #NO_CLOSE_BUTTON
*/
public static final MyDialogFragment newInstance(int flags, boolean cancelable) {
Bundle args = new Bundle();
args.putInt("flags", flags);
MyDialogFragment fragment = new MyDialogFragment();
fragment.setArguments(args);
fragment.setCancelable(cancelable);
return fragment;
}
/**
* New instance with force ok button. (cancelable = false)
*
* @param flags
* the flags (0 ok)
* @return new instance
* @see #NO_POSITIVE_BUTTON
* @see #NO_NEGATIVE_BUTTON
* @see #NO_CLOSE_BUTTON
*/
public static final MyDialogFragment newInstanceForceOkDialog(int flags) {
return newInstance(NO_NEGATIVE_BUTTON | NO_CLOSE_BUTTON | flags, false);
}
/**
* New instance with no button. (cancelable = false)
*
* @param flags
* the flags (0 ok)
* @return new instance
* @see #NO_POSITIVE_BUTTON
* @see #NO_NEGATIVE_BUTTON
* @see #NO_CLOSE_BUTTON
*/
public static final MyDialogFragment newInstanceNoButton(int flags) {
return newInstance(NO_POSITIVE_BUTTON | NO_NEGATIVE_BUTTON
| NO_CLOSE_BUTTON | flags, false);
}
/**
* Keep this constructor!!
* (Use {@link #newInstance(String, String)} as a substitute!!)
*/
public MyDialogFragment() {
// Don't do anything here!!
}
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
Bundle args = getArguments();
int flags = args.getInt("flags");
boolean noPosBtn = (flags & NO_POSITIVE_BUTTON) != 0;
boolean noNegBtn = (flags & NO_NEGATIVE_BUTTON) != 0;
boolean noClsBtn = (flags & NO_CLOSE_BUTTON) != 0;
boolean addNtrlBtn = (flags & ADD_NEUTRAL_BUTTON) != 0;
convertResIdToStringInArgs("titleResId", "title");
convertResIdToStringInArgs("messageResId", "message");
convertResIdToStringInArgs("posbtnTextResId", "posbtnText");
convertResIdToStringInArgs("negbtnTextResId", "negbtnText");
convertResIdToStringInArgs("ntrlbtnTextResId", "ntrlbtnText");
final Dialog dialog = super.onCreateDialog(savedInstanceState);
dialog.setContentView(LAYOUT);
dialog.setCanceledOnTouchOutside(false);
Window window = dialog.getWindow();
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN);
window.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
View view;
// title
view = dialog.findViewById(ID_TITLE);
if (view instanceof TextView) {
if (!args.containsKey("title")) view.setVisibility(View.GONE);
else ((TextView) view).setText(args.getString("title"));
}
// message
view = dialog.findViewById(ID_MESSAGE);
if (view instanceof TextView) {
if (!args.containsKey("message")) view.setVisibility(View.GONE);
else ((TextView) view).setText(args.getString("message"));
}
// positive button
view = dialog.findViewById(ID_POSITIVE_BUTTON);
if (view != null) {
if (noPosBtn) view.setVisibility(View.GONE);
else {
view.setOnClickListener(this);
if (view instanceof TextView) {
TextView textView = (TextView) view;
if (args.containsKey("posbtnText")) textView.setText(args.getString("posbtnText"));
else if (noNegBtn) textView.setText(R.string.dialog_ok_btn_label);
else textView.setText(R.string.dialog_positive_btn_label);
}
}
}
// negative button
view = dialog.findViewById(ID_NEGATIVE_BUTTON);
if (view != null) {
if (noNegBtn) view.setVisibility(View.GONE);
else {
view.setOnClickListener(this);
if (view instanceof TextView) {
TextView textView = (TextView) view;
if (args.containsKey("negbtnText")) textView.setText(args.getString("negbtnText"));
else textView.setText(R.string.dialog_negative_btn_label);
}
}
}
// neutral button
view = dialog.findViewById(ID_NEUTRAL_BUTTON);
if (view != null) {
if (addNtrlBtn) {
view.setVisibility(View.VISIBLE);
view.setOnClickListener(this);
if (view instanceof TextView) {
TextView textView = (TextView) view;
if (args.containsKey("ntrlbtnText")) textView.setText(args.getString("ntrlbtnText"));
else textView.setText(R.string.dialog_negative_btn_label);
}
}
}
// close button
view = dialog.findViewById(ID_CLOSE_BUTTON);
if (view != null) {
if (noClsBtn) view.setVisibility(View.GONE);
else view.setOnClickListener(this);
}
return dialog;
}
@Override
public void onResume() {
if (dismissFlag) { // dismiss a dialog
Fragment fragment = getFragmentManager().findFragmentByTag(TAG);
if (fragment instanceof DialogFragment) {
DialogFragment dialogFragment = (DialogFragment) fragment;
dialogFragment.dismiss();
getFragmentManager().beginTransaction().remove(fragment).commit();
dismissFlag = false;
}
}
super.onResume();
}
/** @deprecated Use {@link #show(FragmentManager)}!! */
@Override
@Deprecated
public final void show(FragmentManager manager, String tag) {
show(manager);
}
/**
* Display the dialog, adding the fragment to the given FragmentManager.
* This is a convenience for explicitly creating a transaction, adding the fragment to it with the given tag,
* and committing it. This does <em>not</em> add the transaction to the back stack.
* When the fragment is dismissed, a new transaction will be executed to remove it from the activity.
*
* @param manager
* The FragmentManager this fragment will be added to.
*/
public final void show(FragmentManager manager) {
deleteDialogFragment(manager);
clickGuard = false;
// super.show(manager, TAG);
FragmentTransaction transaction = manager.beginTransaction();
transaction.add(this, TAG);
transaction.commitAllowingStateLoss();
}
@Override
public void dismiss() {
if (isResumed()) super.dismiss(); // dismiss now
else dismissFlag = true; // dismiss on onResume
}
@Override
public void onDismiss(DialogInterface dialog) {
super.onDismiss(dialog);
if (listener != null) listener.onEvent(listenerId, DialogFragmentListener.ON_DISMISS);
}
@Override
public void onCancel(DialogInterface dialog) {
super.onCancel(dialog);
if (listener != null) listener.onEvent(listenerId, DialogFragmentListener.ON_CANCEL);
}
@Override
public void onClick(View v) {
if (clickGuard) return;
clickGuard = true;
int event = -1;
switch (v.getId()) {
case ID_POSITIVE_BUTTON:
event = DialogFragmentListener.ON_POSITIVE_BUTTON_CLICKED;
break;
case ID_NEGATIVE_BUTTON:
event = DialogFragmentListener.ON_NEGATIVE_BUTTON_CLICKED;
break;
case ID_NEUTRAL_BUTTON:
event = DialogFragmentListener.ON_NEUTRAL_BUTTON_CLICKED;
break;
case ID_CLOSE_BUTTON:
event = DialogFragmentListener.ON_CLOSE_BUTTON_CLICKED;
break;
}
if (listener != null) listener.onEvent(listenerId, event);
dismiss();
}
/**
* Sets dialog title.
*
* @param title
* dialog title
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setTitle(String title) {
getArguments().putString("title", title);
getArguments().remove("titleResId");
Dialog dialog = getDialog();
if (dialog == null) return this;
View view = dialog.findViewById(ID_TITLE);
if (view instanceof TextView) ((TextView) view).setText(title);
return this;
}
/**
* Sets dialog title.
*
* @param title
* resource id
* @return this {@link ProgressDialogFragment} instance
*/
public MyDialogFragment setTitle(int resId) {
if (isAdded()) setTitle(getString(resId));
else {
getArguments().putInt("titleResId", resId);
getArguments().remove("title");
}
return this;
}
/**
* Sets dialog message.
*
* @param message
* dialog message
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setMessage(String message) {
getArguments().putString("message", message);
getArguments().remove("messageResId");
Dialog dialog = getDialog();
if (dialog == null) return this;
View view = dialog.findViewById(ID_MESSAGE);
if (view instanceof TextView) ((TextView) view).setText(message);
return this;
}
/**
* Sets dialog message.
*
* @param message
* resource id
* @return this {@link ProgressDialogFragment} instance
*/
public MyDialogFragment setMessage(int resId) {
if (isAdded()) setMessage(getString(resId));
else {
getArguments().putInt("messageResId", resId);
getArguments().remove("message");
}
return this;
}
/**
* Sets text on positive button.
*
* @param text
* the text
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setPositiveButtonText(String text) {
getArguments().putString("posbtnText", text);
getArguments().remove("posbtnTextResId");
Dialog dialog = getDialog();
if (dialog == null) return this;
View view = dialog.findViewById(ID_POSITIVE_BUTTON);
if (view instanceof TextView) ((TextView) view).setText(text);
return this;
}
/**
* Sets text on positive button.
*
* @param resId
* resource id
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setPositiveButtonText(int resId) {
if (isAdded()) setPositiveButtonText(getString(resId));
else {
getArguments().putInt("posbtnTextResId", resId);
getArguments().remove("posbtnText");
}
return this;
}
/**
* Sets text on negative button.
*
* @param text
* the text
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setNegativeButtonText(String text) {
getArguments().putString("negbtnText", text);
getArguments().remove("negbtnTextResId");
Dialog dialog = getDialog();
if (dialog == null) return this;
View view = dialog.findViewById(ID_NEGATIVE_BUTTON);
if (view instanceof TextView) ((TextView) view).setText(text);
return this;
}
/**
* Sets text on negative button.
*
* @param resId
* resource id
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setNegativeButtonText(int resId) {
if (isAdded()) setNegativeButtonText(getString(resId));
else {
getArguments().putInt("negbtnTextResId", resId);
getArguments().remove("negbtnText");
}
return this;
}
/**
* Sets text on neutral button.
*
* @param text
* the text
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setNeutralButtonText(String text) {
getArguments().putString("ntrlbtnText", text);
getArguments().remove("ntrlbtnTextResId");
Dialog dialog = getDialog();
if (dialog == null) return this;
View view = dialog.findViewById(ID_NEUTRAL_BUTTON);
if (view instanceof TextView) ((TextView) view).setText(text);
return this;
}
/**
* Sets text on neutral button.
*
* @param resId
* resource id
* @return this {@link MyDialogFragment} instance
*/
public MyDialogFragment setNeutralButtonText(int resId) {
if (isAdded()) setNeutralButtonText(getString(resId));
else {
getArguments().putInt("ntrlbtnTextResId", resId);
getArguments().remove("ntrlbtnText");
}
return this;
}
private void convertResIdToStringInArgs(String resIdkey, String destKey) {
Bundle args = getArguments();
if (isAdded()) {
if (args.containsKey(resIdkey)) {
String string = getString(args.getInt(resIdkey));
args.putString(destKey, string);
args.remove(resIdkey);
}
}
}
/**
* @param fragmentManager
* {@link FragmentManager}
*/
private void deleteDialogFragment(final FragmentManager manager) {
MyDialogFragment previous = (MyDialogFragment) manager.findFragmentByTag(TAG);
if (previous == null) return;
Dialog dialog = previous.getDialog();
if (dialog == null) return;
if (!dialog.isShowing()) return;
previous.onDismissExclusiveDialog();
previous.dismiss();
}
protected void onDismissExclusiveDialog() {
}
public MyDialogFragment setListener(int id, DialogFragmentListener listener) {
this.listenerId = id;
this.listener = listener;
return this;
}
}
サポートライブラリのほうのDialogFragmentを使ったが、古い端末には対応しないアプリを作る場合とか必要なければ標準のほうを使えば良い。
サポートライブラリを使う場合、アクティビティはFragmentActivityを継承しないといけない。そのとき、getFragmentManager()ではなく、getSupportFragmentManager()を使わないといけない。これらはFragmentのshowメソッドを呼ぶときに引数で必要になる。
ダイアログであるからにはボタンと、そのリスナーを提供しなければならない。
/*
* Copyright (c) Oct 6, 2013 ZYXW. All rights reserved.
*/
package (Package Name);
/**
* The listener interface for receiving dialogFragment events.
* The class that is interested in processing a dialogFragment
* event implements this interface, and the object created
* with that class is registered with a component using the
* component's <code>setListener</code> method. When
* the dialogFragment event occurs, that object's appropriate
* method is invoked.
*
* @author yumenosuke-k
* @category Listener
* @since Oct 6, 2013
* @see #onEvent(int, int)
*/
public interface DialogFragmentListener {
public static final int ON_DISMISS = 0;
public static final int ON_CANCEL = 1;
public static final int ON_POSITIVE_BUTTON_CLICKED = 2;
public static final int ON_NEGATIVE_BUTTON_CLICKED = 3;
public static final int ON_NEUTRAL_BUTTON_CLICKED = 4;
public static final int ON_CLOSE_BUTTON_CLICKED = 5;
/**
* On event.
*
* @param id
* listener id
* @param event
* event is either of the following. {@link #ON_DISMISS} {@link #ON_CANCEL},
* {@link #ON_POSITIVE_BUTTON_CLICKED}, {@link #ON_NEGATIVE_BUTTON_CLICKED},
* {@link #ON_CLOSE_BUTTON_CLICKED}.
*/
void onEvent(int id, int event);
}
レイアウトはあくまで参考までに。
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:layout_marginTop="13dp"
android:background="@drawable/dialog_bg"
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingBottom="20dp" >
<TextView
android:id="@+id/dialog_title_textview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:layout_marginTop="6dp"
android:background="@drawable/gray_line"
android:paddingBottom="10dp"
android:paddingLeft="3dp"
android:paddingRight="6dp"
android:paddingTop="15dp"
android:text="Title is shown here!!"
android:textColor="@color/gray05"
android:textSize="18sp"
tools:ignore="HardcodedText" />
<TextView
android:id="@+id/dialog_message_textview"
android:layout_width="wrap_content"
android:layout_height="0dp"
android:layout_marginBottom="0dp"
android:layout_marginLeft="20dp"
android:layout_marginRight="20dp"
android:layout_marginTop="20dp"
android:layout_weight="1"
android:lineSpacingExtra="6dp"
android:text="Message is shown here!!"
android:textColor="@color/gray02_g"
android:textSize="14sp"
tools:ignore="HardcodedText" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="13dp"
android:layout_marginRight="13dp"
android:gravity="center_horizontal" >
<Button
android:id="@+id/dialog_negative_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginTop="13dp"
android:layout_weight="1"
android:background="@drawable/dialog_negative_button"
android:padding="10dp"
android:text="@string/dialog_negative_btn_label"
android:textColor="@color/gray02" />
<Button
android:id="@+id/dialog_neutral_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginTop="13dp"
android:layout_weight="1"
android:background="@drawable/dialog_negative_button"
android:padding="10dp"
android:text="@string/dialog_negative_btn_label"
android:textColor="@color/gray02"
android:visibility="gone" />
<Button
android:id="@+id/dialog_positive_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_marginTop="13dp"
android:layout_weight="1"
android:background="@drawable/dialog_positive_button"
android:padding="10dp"
android:text="@string/dialog_positive_btn_label"
android:textColor="@color/white" />
</LinearLayout>
</LinearLayout>
<ImageButton
android:id="@+id/dialog_close_button"
android:layout_width="44dp"
android:layout_height="44dp"
android:layout_alignParentRight="true"
android:layout_alignParentTop="true"
android:background="@drawable/dialog_close_bg"
android:contentDescription="@string/btn_label_close"
android:gravity="center"
android:src="@drawable/icon_close_btn" />
</RelativeLayout>
イマドキにAndroidは面倒くさい。
この記事で何回面倒くさいって言ったんだろう。面倒くさい。
こんなコードで良かったらコピペしてみてください。なにせ面倒くさいので。
念のため最後に使用方法
さきほどのリスナーをimplementsしたクラスで
private static final int LISTENER_ID_REALLY_END = 0;
// いろいろ
MyDialogFragment.newInstance(true)
.setMessage(R.string.___)
.setListener(LISTENER_ID_REALLY_END, this)
.show(getSupportFragmentManager());
// いろいろ
@Override
public void onEvent(int id, int event) {
if (event == DialogFragmentListener.ON_POSITIVE_BUTTON_CLICKED) switch (id) {
case LISTENER_ID_REALLY_END:
finish();
break;
}
}
匿名クラスで以下のように書いてもOK
MyDialogFragment.newInstance(MyDialogFragment.ADD_NEUTRAL_BUTTON)
.setTitle(R.string.___)
.setMessage(R.string.___)
.setPositiveButtonText(R.string.___)
.setNegativeButtonText(R.string.___)
.setNeutralButtonText(R.string.___)
.setListener(0, new DialogFragmentListener() {
@Override
public void onEvent(int id, int event) {
switch (event) {
case DialogFragmentListener.ON_POSITIVE_BUTTON_CLICKED:
String appPackage = context.getPackageName();
Intent intent = new Intent(Intent.ACTION_VIEW,
Uri.parse("https://play.google.com/store/apps/details?id=" + appPackage));
context.startActivity(intent);
break;
case DialogFragmentListener.ON_NEGATIVE_BUTTON_CLICKED:
break;
case DialogFragmentListener.ON_NEUTRAL_BUTTON_CLICKED:
case DialogFragmentListener.ON_CLOSE_BUTTON_CLICKED:
case DialogFragmentListener.ON_CANCEL:
clearSharedPreferences(context);
break;
}
}
}).show(manager);
おそまつさまでした。