Androidのカスタマイズ:必要なことを実行するUIコンポーネントを構築する方法
公開: 2022-03-11開発者が、ターゲットとするプラットフォームによって提供されていないか、実際に提供されているが、特定のプロパティまたは動作を欠いているUIコンポーネントを必要としていることに気付くのは珍しいことではありません。 両方のシナリオに対する答えは、カスタムUIコンポーネントです。
Android UIモデルは本質的にカスタマイズ可能であり、Androidのカスタマイズ、テスト、およびさまざまな方法でカスタムUIコンポーネントを作成する機能を提供します。
既存のコンポーネント(つまり、
TextView
、ImageView
など)を継承し、必要な機能を追加/オーバーライドします。 たとえば、ImageView
を継承するCircleImageView
は、onDraw()
関数をオーバーライドして表示される画像を円に制限し、loadFromFile()
関数を追加して外部メモリから画像をロードします。複数のコンポーネントから複合コンポーネントを作成します。 このアプローチでは通常、レイアウトを利用して、コンポーネントを画面上に配置する方法を制御します。 たとえば、水平方向の
LabeledEditText
を継承し、ラベルとして機能するTextView
とテキスト入力フィールドとして機能するEditText
の両方を含むLinearLayout
。このアプローチでは、前のアプローチを利用することもできます。つまり、内部コンポーネントをネイティブまたはカスタムにすることができます。
最も用途が広く、最も複雑なアプローチは、自己描画コンポーネントを作成することです。 この場合、コンポーネントは汎用の
View
クラスを継承し、レイアウトを決定するonDraw()
、コンテンツを表示するonDrawonMeasure()
などの関数をオーバーライドします。この方法で作成されたコンポーネントは、通常、Androidの2D描画APIに大きく依存します。
Androidのカスタマイズのケーススタディ: CalendarView
Androidは、ネイティブのCalendarView
コンポーネントを提供します。 パフォーマンスが高く、カレンダーコンポーネントに期待される最小限の機能を提供し、1か月を表示し、現在の日を強調表示します。 見た目も良いと言う人もいるかもしれませんが、ネイティブな見た目を求めていて、見た目をカスタマイズすることに関心がない場合に限ります。
たとえば、 CalendarView
コンポーネントは、特定の日のマーク方法や使用する背景色を変更する方法を提供しません。 たとえば、特別な機会をマークするために、カスタムテキストやグラフィックを追加する方法もありません。 つまり、コンポーネントは次のようになり、ほとんど何も変更できません。
AppCompact.Light
テーマのCalendarView
。
自分で作る
では、どのようにして独自のカレンダービューを作成するのでしょうか。 上記のアプローチのいずれかが機能します。 ただし、実用性は通常、3番目のオプション(2Dグラフィックス)を除外し、他の2つの方法を残します。この記事では、両方を組み合わせて使用します。
フォローするために、ここでソースコードを見つけることができます。
1.コンポーネントのレイアウト
まず、コンポーネントの外観から始めましょう。 わかりやすくするために、日をグリッドに表示し、上部に月の名前と「次の月」および「前の月」ボタンを表示します。
このレイアウトは、ファイルcontrol_calendar.xml
で次のように定義されています。 一部の反復マークアップは...
:で省略されていることに注意してください。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:andro android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/white"> <!-- date toolbar --> <RelativeLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:paddingTop="12dp" android:paddingBottom="12dp" android:paddingLeft="30dp" android:paddingRight="30dp"> <!-- prev button --> <ImageView android: android:layout_width="30dp" android:layout_height="30dp" android:layout_centerVertical="true" android:layout_alignParentLeft="true" android:src="@drawable/previous_icon"/> <!-- date title --> <TextView android: android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_toRightOf="@+id/calendar_prev_button" android:layout_toLeftOf="@+id/calendar_next_button" android:gravity="center" android:textAppearance="@android:style/TextAppearance.Medium" android:textColor="#222222" android:text="current date"/> <!-- next button --> <ImageView android: ... Same layout as prev button. android:src="@drawable/next_icon"/> </RelativeLayout> <!-- days header --> <LinearLayout android: android:layout_width="match_parent" android:layout_height="40dp" android:gravity="center_vertical" android:orientation="horizontal"> <TextView android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:gravity="center_horizontal" android:textColor="#222222" android:text="SUN"/> ... Repeat for MON - SAT. </LinearLayout> <!-- days view --> <GridView android: android:layout_width="match_parent" android:layout_height="340dp" android:numColumns="7"/> </LinearLayout>
2.コンポーネントクラス
以前のレイアウトは、 Activity
またはFragment
にそのまま含めることができ、正常に機能します。 ただし、スタンドアロンUIコンポーネントとしてカプセル化すると、コードの繰り返しが防止され、各モジュールが1つの責任を処理するモジュラー設計が可能になります。
UIコンポーネントは、XMLレイアウトファイルのルートと一致するLinearLayout
になります。 重要な部分だけがコードから示されていることに注意してください。 コンポーネントの実装はCalendarView.java
にあります:
public class CalendarView extends LinearLayout { // internal components private LinearLayout header; private ImageView btnPrev; private ImageView btnNext; private TextView txtDate; private GridView grid; public CalendarView(Context context) { super(context); initControl(context); } /** * Load component XML layout */ private void initControl(Context context) { LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); inflater.inflate(R.layout.control_calendar, this); // layout is inflated, assign local variables to components header = (LinearLayout)findViewById(R.id.calendar_header); btnPrev = (ImageView)findViewById(R.id.calendar_prev_button); btnNext = (ImageView)findViewById(R.id.calendar_next_button); txtDate = (TextView)findViewById(R.id.calendar_date_display); grid = (GridView)findViewById(R.id.calendar_grid); } }
コードは非常に単純です。 作成時に、コンポーネントはXMLレイアウトを拡張し、それが完了すると、後で簡単にアクセスできるように内部コントロールをローカル変数に割り当てます。
3.いくつかのロジックが必要です
このコンポーネントを実際にカレンダービューとして動作させるには、いくつかのビジネスロジックが必要です。 最初は複雑に見えるかもしれませんが、実際にはそれほど多くはありません。 それを分解しましょう:
カレンダービューの幅は7日間で、すべての月が最初の行のどこかから始まることが保証されています。
まず、月がどの位置から始まるかを把握し、その前のすべての位置に前月の数値(30、29、28など)を入力して、位置0に到達する必要があります。
次に、当月の日数(1、2、3…など)を入力します。
その後、翌月の日が来ます(ここでも、1、2、3など)が、今回はグリッドの最後の行の残りの位置のみを埋めます。
次の図は、これらの手順を示しています。
グリッドの幅はすでに7セルに指定されており、週ごとのカレンダーを示していますが、高さはどうでしょうか。 グリッドの最大サイズは、最初の行の最後のセルである土曜日から始まる31日間の月の最悪のシナリオによって決定でき、完全に表示するにはさらに5行が必要になります。 したがって、すべてのケースを処理するには、カレンダーを6行(合計42日)で表示するように設定するだけで十分です。
しかし、すべての月に31日があるわけではありません! Androidに組み込まれている日付機能を使用することで、これに起因する複雑さを回避でき、自分で日数を計算する必要がなくなります。
前述のように、 Calendar
クラスによって提供される日付機能により、実装は非常に簡単になります。 このコンポーネントでは、 updateCalendar()
関数が次のロジックを実装しています。
private void updateCalendar() { ArrayList<Date> cells = new ArrayList<>(); Calendar calendar = (Calendar)currentDate.clone(); // determine the cell for current month's beginning calendar.set(Calendar.DAY_OF_MONTH, 1); int monthBeginningCell = calendar.get(Calendar.DAY_OF_WEEK) - 1; // move calendar backwards to the beginning of the week calendar.add(Calendar.DAY_OF_MONTH, -monthBeginningCell); // fill cells (42 days calendar as per our business logic) while (cells.size() < DAYS_COUNT) { cells.add(calendar.getTime()); calendar.add(Calendar.DAY_OF_MONTH, 1); } // update grid ((CalendarAdapter)grid.getAdapter()).updateData(cells); // update title SimpleDateFormat sdf = new SimpleDateFormat("MMM yyyy"); txtDate.setText(sdf.format(currentDate.getTime())); }
4.心の中でカスタマイズ可能
個々の日を表示するコンポーネントはGridView
であるため、日を表示する方法をカスタマイズするのに適した場所はAdapter
です。これは、個々のグリッドセルのデータを保持し、ビューを拡張する役割を果たします。
この例では、 CalendearView
から次のものが必要になります。
- 現在は太字の青いテキストである必要があります。
- 今月以外の日はグレー表示にする必要があります。
- イベントのある日には、特別なアイコンを表示する必要があります。
- カレンダーのヘッダーは、季節(夏、秋、冬、春)に応じて色が変わるはずです。
最初の3つの要件は、テキスト属性と背景リソースを変更することで簡単に実現できます。 このタスクを実行するためにCalendarAdapter
を実装しましょう。 CalendarView
のメンバークラスになることができるほど単純です。 getView()
関数をオーバーライドすることにより、上記の要件を達成できます。
@Override public View getView(int position, View view, ViewGroup parent) { // day in question Date date = getItem(position); // today Date today = new Date(); // inflate item if it does not exist yet if (view == null) view = inflater.inflate(R.layout.control_calendar_day, parent, false); // if this day has an event, specify event image view.setBackgroundResource(eventDays.contains(date)) ? R.drawable.reminder : 0); // clear styling view.setTypeface(null, Typeface.NORMAL); view.setTextColor(Color.BLACK); if (date.getMonth() != today.getMonth() || date.getYear() != today.getYear()) { // if this day is outside current month, grey it out view.setTextColor(getResources().getColor(R.color.greyed_out)); } else if (date.getDate() == today.getDate()) { // if it is today, set it to blue/bold view.setTypeface(null, Typeface.BOLD); view.setTextColor(getResources().getColor(R.color.today)); } // set text view.setText(String.valueOf(date.getDate())); return view; }
最終的な設計要件には、もう少し作業が必要です。 まず、/ res / values/ /res/values/colors.xml
に四季の色を追加しましょう。

<color name="summer">#44eebd82</color> <color name="fall">#44d8d27e</color> <color name="winter">#44a1c1da</color> <color name="spring">#448da64b</color>
次に、配列を使用して各月の季節を定義しましょう(簡単にするために北半球を想定しています。オーストラリアは申し訳ありません!)。 CalendarView
で、次のメンバー変数を追加します。
// seasons' rainbow int[] rainbow = new int[] { R.color.summer, R.color.fall, R.color.winter, R.color.spring }; int[] monthSeason = new int[] {2, 2, 3, 3, 3, 0, 0, 0, 1, 1, 1, 2};
このように、適切な色の選択は、適切な季節( monthSeason[currentMonth]
)を選択し、次に対応する色( rainbow[monthSeason[currentMonth]
)を選択することによって行われます。これは、 updateCalendar()
に追加され、適切な色が選択されていることを確認します。カレンダーが変更されるたびに。
// set header color according to current season int month = currentDate.get(Calendar.MONTH); int season = monthSeason[month]; int color = rainbow[season]; header.setBackgroundColor(getResources().getColor(color));
これにより、次の結果が得られます。
重要な注意事項HashSet
がオブジェクトを比較する方法により、updateCalendar()の上記のチェックeventDays.contains(date)
は、 updateCalendar()
に同一でない限り、日付オブジェクトに対してtrueを生成しません。 Date
データ型の特別なチェックは実行されません。 これを回避するために、このチェックは次のコードに置き換えられます。
for (Date eventDate : eventDays) { if (eventDate.getDate() == date.getDate() && eventDate.getMonth() == date.getMonth() && eventDate.getYear() == date.getYear()) { // mark this day for event view.setBackgroundResource(R.drawable.reminder); break; } }
5.デザイン時に醜く見える
デザイン時のプレースホルダーに対するAndroidの選択は疑わしい場合があります。 幸い、Androidは実際にコンポーネントをインスタンス化してUIデザイナーでレンダリングします。これを利用するには、コンポーネントコンストラクターでupdateCalendar()
を呼び出します。 このようにして、コンポーネントは実際に設計時に意味をなします。
コンポーネントの初期化で大量の処理が必要になったり、大量のデータが読み込まれたりすると、IDEのパフォーマンスに影響を与える可能性があります。 この場合、Androidは、コンポーネントがUIデザイナーで実際にインスタンス化されるときに使用されるデータの量を制限するために使用できるisInEditMode()
と呼ばれる気の利いた関数を提供します。 たとえば、 CalendarView
にロードするイベントがたくさんある場合は、 updateCalendar()
関数内でisInEditMode( isInEditMode()
を使用して、デザインモードで空の/制限されたイベントリストを提供し、それ以外の場合は実際のイベントリストをロードできます。
6.コンポーネントの呼び出し
コンポーネントはXMLレイアウトファイルに含めることができます(使用例はactivity_main.xml
にあります)。
<samples.aalamir.customcalendar.CalendarView android: android:layout_width="match_parent" android:layout_height="wrap_content"/>
そして、レイアウトがロードされると相互作用するために取得されます:
HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events);
上記のコードは、イベントのHashSet
を作成し、それに現在の日を追加してから、 CalendarView
に渡します。 その結果、 CalendarView
は当日を太字の青色で表示し、イベントマーカーもその上に配置します。
CalendarView
7.属性の追加
Androidが提供するもう1つの機能は、カスタムコンポーネントに属性を割り当てることです。 これにより、Android開発者は、コンポーネントを使用してレイアウトXMLを介して設定を選択し、実行時にCalendarView
がどのように表示されるかを待つ必要がなく、UIデザイナーで結果をすぐに確認できます。 コンポーネントの日付形式の表示を変更する機能を追加しましょう。たとえば、3文字の省略形の代わりに月のフルネームを入力します。
これを行うには、次の手順が必要です。
- 属性を宣言します。 それを
dateFormat
と呼び、string
データ型を指定しましょう。/res/values/attrs.xml
に追加します:
<resources> <declare-styleable name="CalendarDateElement"> <attr name="dateFormat" format="string"/> </declare-styleable> </resources>
- コンポーネントを使用しているレイアウトの属性を使用し、値
"MMMM yyyy"
を指定します。
<samples.aalamir.customcalendar.CalendarView xmlns:calendarNS="http://schemas.android.com/apk/res/samples.aalamir.customcalendar" android: android:layout_width="match_parent" android:layout_height="wrap_content" calendarNS:dateFormat="MMMM yyyy"/>
- 最後に、コンポーネントに属性値を使用させます。
TypedArray ta = getContext().obtainStyledAttributes(attrs, R.styleable.CalendarView); dateFormat = ta.getString(R.styleable.CalendarView_dateFormat);
]プロジェクトをビルドすると、「2015年7月」のように、月のフルネームを使用するようにUIデザイナーに表示される日付が変更されます。 さまざまな値を指定して、何が起こるかを確認してください。
CalendarView
属性を変更します。
8.コンポーネントとの相互作用
特定の日に押してみましたか? コンポーネントの内部UI要素は引き続き通常の期待どおりに動作し、ユーザーのアクションに応じてイベントを発生させます。 では、これらのイベントをどのように処理するのでしょうか。
答えには2つの部分があります。
- コンポーネント内のイベントをキャプチャし、
- コンポーネントの親にイベントを報告します(
Fragment
、Activity
、または別のコンポーネントの場合もあります)。
最初の部分は非常に簡単です。 たとえば、長押しのグリッドアイテムを処理するには、コンポーネントクラスに対応するリスナーを割り当てます。
// long-pressing a day grid.setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() { @Override public boolean onItemLongClick(AdapterView<?> view, View cell, int position, long id) { // handle long-press if (eventHandler == null) return false; Date date = view.getItemAtPosition(position); eventHandler.onDayLongPress(date); return true; } });
イベントを報告する方法はいくつかあります。 直接的で単純な方法は、Androidの方法をコピーすることです。これは、コンポーネントの親(上記のコードスニペットのeventHandler
)によって実装されるコンポーネントのイベントへのインターフェイスを提供します。
インターフェイスの関数には、アプリケーションに関連する任意のデータを渡すことができます。 この場合、インターフェースは、押された日の日付が渡される1つのイベントハンドラーを公開する必要があります。 次のインターフェイスはCalendarView
で定義されています。
public interface EventHandler { void onDayLongPress(Date date); }
親によって提供される実装は、 setEventHandler()
を介してカレンダービューに提供できます。 `MainActivity.java'の使用例を次に示します。
@Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); HashSet<Date> events = new HashSet<>(); events.add(new Date()); CalendarView cv = ((CalendarView)findViewById(R.id.calendar_view)); cv.updateCalendar(events); // assign event handler cv.setEventHandler(new CalendarView.EventHandler() { @Override public void onDayLongPress(Date date) { // show returned day DateFormat df = SimpleDateFormat.getDateInstance(); Toast.makeText(MainActivity.this, df.format(date), LENGTH_SHORT).show(); } }); }
1日を長押しすると、長押しイベントが発生します。このイベントは、 GridView
によってキャプチャおよび処理され、提供された実装でonDayLongPress()
を呼び出すことによって報告されます。これにより、押された日の日付が画面に表示されます。
これを処理するもう1つのより高度な方法は、AndroidのIntents
とBroadcastReceivers
を使用することです。 これは、複数のコンポーネントにカレンダーのイベントを通知する必要がある場合に特に役立ちます。 たとえば、カレンダーで1日を押すと、 Activity
にテキストを表示し、バックグラウンドService
によってファイルをダウンロードする必要がある場合です。
前のアプローチを使用するには、 Activity
がコンポーネントにEventHandler
を提供し、イベントを処理してからService
に渡す必要があります。 代わりに、コンポーネントにIntent
をブロードキャストさせ、 Activity
とService
の両方が独自のBroadcastReceivers
を介してそれを受け入れると、作業が楽になるだけでなく、問題のActivity
とService
を分離するのにも役立ちます。
結論
したがって、これは、いくつかの簡単な手順で独自のカスタムコンポーネントを作成する方法です。
- XMLレイアウトを作成し、ニーズに合わせてスタイルを設定します。
- XMLレイアウトに従って、適切な親コンポーネントからコンポーネントクラスを派生させます。
- コンポーネントのビジネスロジックを追加します。
- 属性を使用して、ユーザーがコンポーネントの動作を変更できるようにします。
- UIデザイナーでコンポーネントを使いやすくするには、Androidの
isInEditMode()
関数を使用します。
この記事では、例としてカレンダービューを作成しました。これは主に、ストックカレンダービューが多くの点で欠けているためです。 ただし、作成できるコンポーネントの種類に制限はありません。 同じテクニックを使って必要なものを作成できます。空が限界です。
このガイドをお読みいただきありがとうございます。コーディングにご期待ください。