2日かけて、AndroidのDatePickerDialogのまがい物を作った。名前被らないように、DateSelectDialogって名前にした。
何で作ろうと思ったかというと、標準のDatePickerDialogでは「年だけ選択させる」とか「年と月だけ選択させる」っていうのができないから。それを実現しようと思うと、
yukiasu.blogspot.jp
この方のような方法をとることになる。昔のUIだったらこれでいけたんだけど、最近のDatePickerDialogは下の画像のようにすごい複雑化してるので、ただ単に「日付選択のViewだけ非表示にする」っていうのではうまくいかなさそう。
今では非推奨となってしまったHOLOテーマを指定する手もあるけど、マテリアルデザインが好きなので戻りたくない…。そこで、もう自分でまがい物を作ればいいんちゃうかなって思った。
現在の年選択ダイアログが
こんなん。この上半分を消して、年選択の右側に月選択と日選択のスピナーを表示させたらいいんかな?
年選択はListViewで実装されてるっぽいので、それに倣ってやってみる。
完成図は下のような感じ。
ダイアログの中身
<LinearLayout
xmlnsandroid="http://schemas.android.com/apk/res/android"
androidorientation="horizontal"
androidlayout_width="match_parent"
androidlayout_height="match_parent"
androidpaddingTop="@dimen/dialog_padding_top"
androidpaddingBottom="@dimen/dialog_padding_bottom"
androidpaddingStart="@dimen/dialog_horizontal_padding"
androidpaddingEnd="@dimen/dialog_horizontal_padding"
androidbaselineAligned="false">
<ListView
androidlayout_width="0dp"
androidlayout_height="match_parent"
androidlayout_weight="1"
androiddivider="@null"
androidid="@+id/year"/>
<ListView
androidlayout_width="0dp"
androidlayout_height="match_parent"
androidlayout_weight="1"
androiddivider="@null"
androidid="@+id/month"/>
<ListView
androidlayout_width="0dp"
androidlayout_height="match_parent"
androidlayout_weight="1"
androiddivider="@null"
androidid="@+id/date"/>
</LinearLayout>
weightを1にして、均等に並べる。
ListViewの中身
<RelativeLayout
xmlnsandroid="http://schemas.android.com/apk/res/android"
androidlayout_width="match_parent"
androidlayout_height="@dimen/date_select_dialog_item_height">
<ImageView
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidsrc="@drawable/ic_dateselect_circle"
androidlayout_centerInParent="true"
androidtint="@color/colorAccent"
androidalpha=".38"
androidid="@+id/image"/>
<TextView
androidlayout_width="wrap_content"
androidlayout_height="wrap_content"
androidlayout_centerInParent="true"
androidgravity="center"
androidtextSize="13sp"
androidid="@+id/text"/>
</RelativeLayout>
ImageViewは背景の丸で、TextViewは「2016」とかの文字。
Dialogのコード
このダイアログを実際に使うときには
DateSelectDialog dialog = new DateSelectDialog();
Bundle args = new Bundle();
args.putInt("mode", Calendar.DATE);
args.putInt("default", CalendarManager.toEpochtime(date, Calendar.DATE));
dialog.setArguments(args);
dialog.show(getActivity().getSupportFragmentManager(), "dialog");
このように、"mode"にCalendar.DATE or Calendar.MONTH or Calendar.YEARを指定することでどこまで選択させるかを決め、"default"で初期値を入れることにする。なので、そういう情報はgetArguments()から取ってくる。
ダイアログクラスのonCreateDialogは下のようになった。
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
inflater = LayoutInflater.from(getContext());
final View contentView = inflater.inflate(R.layout.date_select_dialog, null);
mode = getArguments().getInt("mode");
selected = CalendarManager.fromEpochtime(getArguments().getInt("default"), mode);
ListView yearList = (ListView) contentView.findViewById(R.id.year);
Dialog dialog = new AlertDialog.Builder(getContext())
.setView(contentView)
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
}
})
.setNegativeButton("Cancel", null)
.create();
if (mode == Calendar.MONTH || mode == Calendar.YEAR) {
dateList.setVisibility(View.GONE);
if (mode == Calendar.YEAR) {
monthList.setVisibility(View.GONE);
}
}
List<Integer> yearObjects = new ArrayList<>();
for (int i = 0; i < YEAR_LAST-YEAR_FIRST + 1 + 2; i++) {
yearObjects.add(i);
}
List<Integer> monthObjects = new ArrayList<>();
for (int i = 0; i < MONTH_COUNT + 2; i++) {
monthObjects.add(i);
}
List<Integer> dateObjects = new ArrayList<>();
for (int i = 0; i < CalendarManager.getEndOfMonth(selected) + 2; i++) {
dateObjects.add(i);
}
yearList.setAdapter(new DateSelectDialogListAdapter(getContext(), 0, yearObjects, Calendar.YEAR));
yearList.setVerticalFadingEdgeEnabled(true);
updateTitle(dialog);
return dialog;
}
Dialogの中身のListViewのアダプタ
まず、日付を変換する必要があったので、いろいろメソッドを用意した。この辺で使う数字には3タイプある。
それらを互いに変換しようと思うと、下のようになった。もうちょっと効率のいい方法あったかも…。
static int pos2Displayed(int mode, int position) {
switch (mode) {
case Calendar.DATE:
return position;
case Calendar.MONTH:
return position;
case Calendar.YEAR:
default:
return position + YEAR_FIRST - 1;
}
}
static int pos2Calendar(int mode, int position) {
switch (mode) {
case Calendar.DATE:
return position;
case Calendar.MONTH:
return position - 1;
case Calendar.YEAR:
default:
return position + YEAR_FIRST - 1;
}
}
static int calendar2Pos(int mode, Calendar calendar) {
switch (mode) {
case Calendar.DATE:
return calendar.get(mode);
case Calendar.MONTH:
return calendar.get(mode) + 1;
case Calendar.YEAR:
default:
return calendar.get(mode) - YEAR_FIRST + 1;
}
}
ここからが一番ややこしかった。
特に、リストの一番上と一番下に空白を追加するところが厄介だった。
空白を入れないと、
こんな感じで、フォーカスされてる日付を真ん中に持ってこようとしても限界がある。だから、空白を加えて下のようにする。
その空白の高さ(ここではoffsetって名前にしてる)は「(ListView全体の高さ - 一行の高さ) / 2」なんだけど、ListView全体の高さっていうのがgetViewに来てからでないと求まらんかった。DialogFragmentの方のonCreateDialogとかonCreateViewとかでは、0が返ってきたりListView自体がnullだったりする。しかもよくわからないことに、ログ出力してみたらgetViewがすごい回数呼ばれてて、初めのうちはListViewの高さは0なのに、途中からちゃんとした数字になっていくみたいだった。
class DateSelectDialogListAdapter extends ArrayAdapter<Integer> {
int mode;
List<Integer> objects;
int itemHeight;
int offset = 0;
public DateSelectDialogListAdapter(Context context, int resource, List<Integer> objects, int mode) {
super(context, resource, objects);
this.mode = mode;
this.objects = objects;
itemHeight = getResources().getDimensionPixelSize(R.dimen.date_select_dialog_item_height);
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
boolean end = position == 0 || position == objects.size() - 1;
if (mode == Calendar.DATE && objects.size() - 2 != CalendarManager.getEndOfMonth(selected)) {
int excess = objects.size() - 2 - CalendarManager.getEndOfMonth(selected);
for (int i = 0; i < Math.abs(excess); i++) {
if (excess > 0) {
objects.remove(objects.size() - 2);
} else {
objects.add(objects.size() - 1, objects.size() - 1);
}
}
notifyDataSetChanged();
}
if (convertView == null) {
convertView = inflater.inflate(R.layout.date_select_dialog_list_item, parent, false);
}
if (parent.getHeight() != 0 && offset == 0) {
offset = (parent.getHeight() - itemHeight) / 2;
((ListView) parent).setSelectionFromTop(calendar2Pos(selected), offset);
convertView.getLayoutParams().height = end ? offset : itemHeight;
}
convertView.findViewById(R.id.image).setVisibility(
selected.get(mode) == pos2Calendar(position)
? View.VISIBLE
: View.GONE
);
final int finalPosition = position;
convertView.setOnClickListener(end ? null : new View.OnClickListener() {
@Override
public void onClick(View v) {
v.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
selected.set(mode, pos2Calendar(finalPosition));
updateTitle(getDialog());
DateSelectDialogListAdapter.this.notifyDataSetChanged();
}
});
return convertView;
}
}
setSelectionFromTopやりすぎって思ったけど、一回セットしただけでは元に戻ってしまったので何回もやってる。
クリックリスナーでは、標準のDatePickerDialogと同じように、クリックされた際に短くバイブさせる。それがperformHapticFeedbackっていうメソッドなんだけど、ここでは引数であるfeedbackConstantにHapticFeedbackConstants.LONG_PRESSを指定してる。本当はCLOCK_TICKがよかったんだけど、APIレベルが足りないので仕方なく…。CLOCK_TICKが「ビッ」って感じだとすると、LONG_PRESSは「ズォンッ」って勢いでバイブレートしよる。
クリックが遅れて反応する問題
たまに、リストの中身をクリックしたら反応が遅れることがあった。
stackoverflow.com
ここの、LayoutParamsについて語ってる人の回答を見て解決!
convertViewに毎回新しく作ったLayoutParamsをセットしてたのが原因だった。
convertView.getLayoutParams().height = ○;
こうやって設定すればよかったみたい。
ダイアログの選択が完了したときのリスナー
インターフェースの中にメソッド入れといて、
public interface OnDateSelectedListener {
void onSelect(Calendar calendar);
}
それをフィールドとしておいて、
OnDateSelectedListener onDateSelectedListener;
setOnDateSelectedListenerが呼び出されたときに登録する。
public void setOnDateSelectedListener(OnDateSelectedListener l) {
this.onDateSelectedListener = l;
}
そしたら、さっき「あとで書く」って書いたOKボタンのリスナーには下のように書ける。
.setPositiveButton("OK", new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
if (onDateSelectedListener != null) {
onDateSelectedListener.onSelect(今選択されてる日付のCalendarオブジェクト);
}
}
})
ダイアログを呼び出す側はこうなる↓
dialog.setOnDateSelectedListener(new DateSelectDialog.OnDateSelectedListener() {
@Override
public void onSelect(Calendar calendar) {
}
});
いろいろと効率悪いところもあるけど、これで上に貼った完成図のようなダイアログができた(*‘▽’)
横向けてもちゃんと表示されてる。