Android Code : สร้าง App Widget เบื้องต้น ตอนที่ 1
สวัสดีครับ บทความนี้ได้มีโอกาสลองเล่นพวก Widget ลองเล่นแล้ว สนุกดี เลยเขียนบทความสรุปตามความเข้าใจไว้สักหน่อย
โดยจะขอแบ่งเป็น 2 ตอน
ตอนที่ 1 จะพามารู้จัก widget แล้วก็ลองสร้าง widget ง่ายๆสักตัว
ตอนที่ 2 จะทำ Multi widget แล้วก็ทำ Widget collection ListView
รู้จักกับ App Widget
App Widget คือ การทำส่วนหนึ่งของแอปเราไปแสดงในหน้า Home screen ที่เห็นชัดๆเลยคือพวก นาฬิกา ประโยชน์ของมันคือ User ไม่ต้องเปิดแอป แต่ก็สามารถเห็นข้อมูลหรือทำอะไรบางอย่างได้ เช่น สามารถควบคุมเครื่องเล่นเพลงได้ สามารถดูข้อมูลการเงินได้ทันที ถ้าเป็นแอปสภาพอากาศก็แสดงสภาพอากาศด้วย Widget ได้
รองรับเฉพาะ API level 17 ขึ้นไป
ประเภทของ Widget
มี 4 ประเภท
Information Widgets คือ พวกการแสดงข้อมูล เช่น แสดงสภาพอากาศ แสดงข้อมูลล
Collection Widgets คือ การแสดงพวก listView gridView ใน widget ให้นึกถึง email
Controls Widgets คือ widgets ที่เอาไว้ควบคุม เช่น ปุ่มเปิดปิด wifi
Hybrid Widgets คือ ลูกผสม อย่างเช่น เครื่องเล่นเพลง ก็จะมีการแสดงข้อมูลเพลง และก็มีปุ่มควบคุมเพลง เป็นต้น
ข้อจำกัดของ Widget
ใน Widget ใช้ได้แค่ Touch และ เลื่อน scroll ขึ้น-ลงเท่านั้นนะ พวก behavior อื่นๆไม่ support
จบ lecture จ้า
เริ่มต้น
มาลองทำกันเลย ไม่พูดถึงเรื่องสร้างโปรเจคนะ
สร้างคลาส WidgetProvider
ให้สร้างคลาสมาคลาสนึง สำหรับเขียนการทำงานของ Widget โดยให้ extends AppWidgetProvider
ผมขอใช้ชื่อคลาสว่า MyWidgetProvider.java
การทำงานเดี๋ยวมาเขียนอีกที เว้นไว้ก่อน
public class MyWidgetProvider extends AppWidgetProvider { // implement widget. }
สร้าง Layout สำหรับ Widget
สร้าง layout สำหรับแสดง widget โดยตอนนี้ ผมจะลองใส่แค่ TextView ตัวเดียวก่อน
ขอตั้งชื่อ layout ว่า app_widget_default.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Hello" android:textSize="28sp"/> </LinearLayout>
สร้าง AppWidgetProviderInfo (MetaData)
AppWidgetProviderInfo คือ ตัว MetaData (สิ่งที่ใช้อธิบายบางสิ่ง) ในที่นี้คือการสำหรับกำหนดคุณสมบัติของ Widget โดยเขียนเป็น xml
ก่อนอื่นสร้าง Resource directory สำหรับเก็บไฟล์ metadata ที่เป็น xml
คลิกขวาที่ res > New > Android resource directory
Metadata เขียนเป็น xml ดังนั้น เลือก Resource type เป็น xml แล้วกด ok
สร้าง XML resource file
คลิกขวา xml > New > XML resource file
ตั้งชื่อได้ตามต้องการ
ผมขอใช้เป็น app_widget_info.xml
ภายใน app_widget_info.xml มีลักษณะดังนี้
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="250dp" android:minHeight="100dp" android:updatePeriodMillis="86400000" android:previewImage="@mipmap/ic_launcher" android:initialLayout="@layout/app_widget_default" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"> </appwidget-provider>
กำหนดขนาดด้วย minWidth และ minHeight
updatePeriodMillis คือ การกำหนด เวลาให้มันอัพเดทข้อมูล วงรอบเป็น ms
previewImage คือ รูปตัวอย่างที่แสดงในหน้า ตอนเลือก widget
initialLayout คือ layout ที่เตรียมไว้แสดง widget
กำหนด WidgetProvider เป็น Receiver
ที่ AndroidManifest.xml เพิ่ม MyWidgetProvider เป็น receiver
และที่ meta-data android:resource ให้ใส่ ไฟล์ metadata xml ที่เตรียมไว้
ตรงนี้ควรกำหนด label ด้วย มันคือ ชื่อ widget ที่แสดงให้ผู้ใช้เห็นตอนเลือก widget
AndroidManifest.xml
.. <receiver android:name=".MyWidgetProvider" android:label="My Widget" > <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/app_widget_info" /> </receiver> </application> </manifest>
สรุปคือ สิ่งที่ผู้ใช้เห็นตอนหน้าจอเลือก Widget มี 2 ตัวคือ label , preview image ซึ่งกำหนดคนละที่กัน
กำหนด onUpdate ของ Widget
คราวนี้เราจะมาเขียนส่วนที่เราเว้นไว้ตอนแรกคือ MyWidgetProvider
ให้เรา override onUpdate() โดยมันจะถูกเรียกเพื่อ update widget ตามเวลาที่ตั้งไว้ใน metadata
การกำหนด layout ใน widget จะใช้ RemoteViews (คงอารมณ์คล้ายๆการส่ง view ไปแสดงที่แอปของชาวบ้านละมั้ง)
public class MyWidgetProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // loop each App Widget. for (int i = 0; i < appWidgetIds.length; i++) { int appWidgetId = appWidgetIds[i]; RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.app_widget_default); // update widget. appWidgetManager.updateAppWidget(appWidgetId, views); } } }
ดูผลลัพธ์
มาดูผลลัพธ์กัน ให้รันแอป จากนั้นปิดแอปแล้วเข้าหน้า Widget ซึ่งในแต่ละเครื่อง หน้าเลือก widget ก็จะต่างกันไป ตาม luancher ด้วย อาจจะต้องกดค้าง หรือไม่ก็ต้อง หนีบนิ้วที่หน้า Homescreen
หา Widget ของเราให้เจอ แล้วก็ลากมันออกมา
View ที่สามารถใช้ใน Widget
Widget ในการแสดง layout จะใช้ view ได้บางตัวเท่านั้น เพราะข้อจำกัดของ RemoteViews ซึ่งก็ครอบคลุมการใช้งานแล้วละ ตัวที่ใช้ได้มีดังนี้
FrameLayout
LinearLayout
RelativeLayout
GridLayout
AnalogClock
Button
Chronometer
ImageButton
ImageView
ProgressBar
TextView
ViewFlipper
ListView
GridView
StackView
AdapterViewFlipper
การกำหนด event ให้กับ View ใน Widget
เราจะลองทำปุ่มกดใน widget กัน เป็นตัวอย่างการกำหนด event ให้กับ view ใน Widget
เพื่อความสวยงามขอทำพื้นหลังขอบโค้ง เป็น drawble (อันนี้ข้ามไปก็ได้)
dialog_rounded_corner_bg.xml
<?xml version="1.0" encoding="utf-8"?> <shape xmlns:android="http://schemas.android.com/apk/res/android"> <solid android:color="#ff5757"/> <corners android:radius="10dp" /> <padding android:left="10dp" android:top="10dp" android:right="10dp" android:bottom="10dp" /> </shape>
มาที่ layout ของ Widget ให้ทำการใส่ปุ่มลงไป 1 ปุ่ม กำหนด id ให้เรียบร้อย
app_widget_default.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp" android:background="@drawable/dialog_rounded_corner_bg"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Are you ready for sending your phone to the moon?" android:textSize="28sp" android:id="@+id/textView" android:textColor="#FFFFFF" android:textStyle="bold"/> <Button android:id="@+id/btn_start" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Start now !!" android:textSize="20sp" android:padding="30dp"/> </LinearLayout>
ที่ MyWidgetProvider ให้เพิ่มโค้ดลงไปนิดหน่อย คือ ประกาศ intent ที่ต้องการให้ไปเปิด Activity
กำหนด intent ลงไปใน PendingIntent
กำหนด event onclick ที่ RemoteViews
public class MyWidgetProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // loop each App Widget. for (int i = 0; i < appWidgetIds.length; i++) { int appWidgetId = appWidgetIds[i]; // Create an Intent to launch. Intent intent = new Intent(context, MainActivity.class); PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0); // to the button RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.app_widget_default); views.setOnClickPendingIntent(R.id.btn_start, pendingIntent); // update widget. appWidgetManager.updateAppWidget(appWidgetId, views); } } }
รันดูผลลัพธ์
รันแอป แล้วมาดูที่ widget ให้กดปุ่มที่กำหนดไว้ สังเกตว่า ปุ่มของเราไม่ได้กำหนดสีดำนะ ตัวหนังสือของปุ่มก็ไม่ได้กำหนดสีขาว แต่มันแสดงสีขาว เป็นเพราะว่าเราไม่ได้กำหนด มันจึงใช้ theme ของแอปนั้นแทน แอปในที่นี้คือ luancher ที่รับ layout ผ่าน RemoteViews แล้วไปแสดง widget นั่นเอง ดังนั้นควรกำหนดไว้ดีกว่า เพราะไม่รู้แต่ละเครื่องมันจะต่างกันมัย
ระวังเรื่องขนาด
ถ้าเรากำหนด minWidth , minHeight ไม่เหมาะสม widget จะแสดงไม่สมบูรณ์ รับรองผู้ใช้มี งง แน่นอน
การ update Widget ด้วยตัวเอง
ตัว metadata มีการกำหนดเวลา update ไว้ก็จริง แต่เราก็ควร update widget เมื่อเรารู้ว่ามันควรจะ update
วิธีการคือ แค่หาทางให้ MyWidgetProvider เรียก onUpdate()
โค้ดสำหรับเรียกให้ provider ทำ onUpdate
Intent intent = new Intent(this, MyWidgetProvider.class); intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE); int[] ids = AppWidgetManager.getInstance(getApplication()) .getAppWidgetIds(new ComponentName(getApplication(), MyWidgetProvider.class)); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids); sendBroadcast(intent);
การทำ Widget ให้ support หลายขนาด
หรือเรียกว่า Widget Resizing ตรงนี้เราจะเขียนไว้ที่คลาส MyWidgetProvider
ฟีเจอร์ปรับขยายขนาด จะ support เฉพาะ API level 16 ขึ้นไป (Jelly bean)
ผมขอเริ่มจากสร้าง layout มา 3 ตัว สำหรับขนาด 3 ขนาด
คือสำหรับ 1 column , 2 columns และ 3 columns
ตัวอย่าง layout/app_widget_default_2_column.xml
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:padding="16dp" android:background="@drawable/dialog_rounded_corner_bg"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="This is 2 column. " android:textSize="24sp" android:id="@+id/textView" android:textColor="#FFFFFF" android:textStyle="bold"/> </LinearLayout>
ที่ MyWidgetProvider ก็ให้ override method onAppWidgetOptionsChanged()
โดยตัวนี้จะถูกเรียกเมื่อผู้ใช้แก้ไขขนาดของ Widget
หลักการ คือ get ขนาดของ Widget ออกมา แล้วไปคำนวณหา row , column ที่ มันใช้ (1 column ใช้ 110 dp)
พอเรารู้ row , column ที่ widget ใช้แล้วก็ทำการ สร้าง layout remoteView ให้ update มันซะ
@Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { Bundle options = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { options = appWidgetManager.getAppWidgetOptions(appWidgetId); // Get min width and height. int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); // Obtain appropriate widget and update it. RemoteViews remoteViews = getRemoteViews(context, minWidth, minHeight); appWidgetManager.updateAppWidget(appWidgetId, remoteViews); } super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); } private RemoteViews getRemoteViews(Context context, int minWidth, int minHeight) { int rows = getCellsForSize(minHeight); int columns = getCellsForSize(minWidth); // Now you changing layout base on you column count switch (columns) { case 2: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_2_columns); case 3: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_3_columns); default: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_4_columns); } } private static int getCellsForSize(int size) { int n = 2; while (70 * n - 30 < size) { n++; } return n - 1; }
การทำงานใน onUpdate() ก็ต้องตรวจสอบขนาดด้วย ดังนั้นก็ย้ายมาทำใน onUpdate ไปเลย
เวลา user เปลี่ยนขนาดก็เรียก onUpdate()
public class MyWidgetProvider extends AppWidgetProvider { @Override public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, Bundle newOptions) { int[] appWidgetIds = {appWidgetId}; onUpdate(context,appWidgetManager, appWidgetIds); super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); } private RemoteViews getRemoteViews(Context context, int minWidth, int minHeight) { // First find out rows and columns based on width provided. int rows = getCellsForSize(minHeight); int columns = getCellsForSize(minWidth); // Now you changing layout base on you column count // In this code from 1 column to 4 // you can make code for more columns on your own. switch (columns) { case 2: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_2_columns); case 3: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_3_columns); default: return new RemoteViews(context.getPackageName(), R.layout.app_widget_default_4_columns); } } private static int getCellsForSize(int size) { int n = 2; while (70 * n - 30 < size) { n++; } return n - 1; } public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // loop each App Widget. for (int i = 0; i < appWidgetIds.length; i++) { Bundle options = null; if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { options = appWidgetManager.getAppWidgetOptions(appWidgetIds[i]); // Get min width and height. int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); int minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT); // Obtain appropriate widget and update it. RemoteViews remoteViews = getRemoteViews(context, minWidth, minHeight); appWidgetManager.updateAppWidget(appWidgetIds[i], remoteViews); } Toast.makeText(context, "Update appWidgetId = " + appWidgetIds[i], Toast.LENGTH_SHORT).show(); } } }
ดูผลลัพธ์
ซึ่งในความเป็นจริง การแสดงข้อมูลก็อาจจะแตกต่างกันไปตามขนาด ถ้าขนาดใหญ่ก็แสดงรายละเอียดมากเป็นต้น
ตอนนี้เราเข้าใจการทำงานของ Widget แล้ว สามารถสร้าง Widget ง่ายๆ ให้กับแอปได้ บทความตอนหน้าจะไปลองเล่นในส่วนของการทำหลายๆ Widget และทำ Widget แบบมี ListView กันซึ่งจะยากไปอีกนิดครับ อย่าพึ่งเบื่อกันนนะ
Reference
https://developer.android.com/guide/topics/appwidgets/index.html
https://developer.android.com/design/patterns/widgets.html
http://stackoverflow.com/questions/17138191/android-widget-resizing