Android Code : สร้าง App Widget เบื้องต้น ตอนที่ 2
จากบทความที่แล้ว ได้พูดถึงการสร้าง App Widget แบบง่าย การทำให้รองรับการปรับขนาดได้ บทความนี้จะพามาลองทำเพิ่มเติมเกี่ยวกับ Multi widget แล้วก็การใช้งาน ListView ใน Widget
ตอนที่ 1
https://benzneststudios.com/blog/android/how-to-create-app-widget-1/
การทำ Multi Widget
ใน 1 แอปสามารถมีได้หลาย Widget เราจะมาลองสร้าง Widget เพิ่มอีก 1 ตัวกัน
วิธีการก็ใช้หลักการเดียวกับในบทความตอนที่ 1 คือ
สร้าง WidgetProvider
สร้าง Layout
สร้าง Metadata และกำหนด layout ใน metadata
กำหนด WidgetProvider เป็น Receiver ใน AndroidManifest.xml
Implement ตัว WidgetProvider
เราจะมาลองเพิ่ม Widget ListView ตัวใหม่กันอีก 1 ตัว
ถ้าอ่านบทความตอนที่ 1 มาแล้วก็ลุยเลย
MyWidgetListViewProvider.java
public class MyWidgetListViewProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { // implement here. } }
layout/app_widget_listview.xml
เพื่อเตรียมทำ listView ขอใส่ listView ไว้เลย ใส่ id ให้มันด้วย
<?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:background="@drawable/dialog_rounded_corner_bg" android:orientation="vertical" android:padding="16dp"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="My favorite food" android:textColor="#FFFFFF" android:textSize="24sp"/> <ListView android:id="@+id/listView" android:layout_width="match_parent" android:layout_height="match_parent"/> </LinearLayout>
xml/app_widget_listview.xml
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android" android:minWidth="250dp" android:minHeight="200dp" android:updatePeriodMillis="86400000" android:previewImage="@mipmap/ic_launcher" android:initialLayout="@layout/app_widget_listview" android:resizeMode="horizontal|vertical" android:widgetCategory="home_screen"> </appwidget-provider>
AndroidManifest.xml
.. <receiver android:name=".MyWidgetListViewProvider" android:label="My WidgetListView"> <intent-filter> <action android:name="android.appwidget.action.APPWIDGET_UPDATE" /> </intent-filter> <meta-data android:name="android.appwidget.provider" android:resource="@xml/app_widget_listview" /> </receiver> </application> </manifest>
ทำการ implement ส่วนของ onUpdate() ใน MyWidgetListViewProvider
ให้สร้าง remoteView จาก layout ที่เตรียมไว้ แล้วสั่ง updateWidget เพื่อดูว่า widget ที่สองทำงานมัย
public class MyWidgetListViewProvider 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_listview); // update widget. appWidgetManager.updateAppWidget(appWidgetId, views); } } }
รันดูผลลัพธ์
จะเห็นว่าในหน้าเลือก Widget ที่แอปของเราจะมี widget ให้เลือก 2 อัน คืออันเก่าและอันใหม่นั่นเอง
สร้าง Layout ของแถวใน ListView
ต่อมาเราจะเตรียมส่วนของแถวใน ListView กัน
สร้าง layout ของแถว แล้วใส่ id ให้เรียบร้อย
layout/row_food.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="wrap_content" android:orientation="horizontal" android:padding="16dp"> <ImageView android:layout_width="50dp" android:layout_height="50dp" android:src="@mipmap/ic_launcher"/> <TextView android:id="@+id/tv_food_name" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="Food name" android:textSize="20sp" android:layout_gravity="center_vertical" android:layout_marginLeft="16dp" android:textColor="#FFFFFF"/> </LinearLayout>
สร้างคลาส MyRemoteViewFactory
คลาสนี้จะทำงานคล้ายกับ Adapter มันมีหน้าที่สร้าง RemoteView ไปแสดงใน ListView
การ implement ขอเว้นไว้ก่อน เดี๋ยวกลับมาเขียน
public class MyRemoteViewFactory implements RemoteViewsService.RemoteViewsFactory { }
สร้างคลาส MyWidgetListViewService
คลาสนี้เป็นคลาส service ที่คอย blinding ตัว Factory กับตัว MyWidgetProvider
เวลาเรา scroll ใน listView มันก็จะทำงานเหมือนเชื่อมตัว ListView ใน MyWidgetProvider กับข้อมูลที่จะสร้างใน factory
public class MyWidgetListViewService extends RemoteViewsService { @Override public RemoteViewsFactory onGetViewFactory(Intent intent) { return new MyRemoteViewFactory(this.getApplicationContext(), intent); } }
กำหนด MyWidgetListViewService เป็น service
ไปที่ AndroidManifest.xml ให้ไปกำหนดคลาส MyWidgetListViewService เป็น service
และ permission BIND_REMOTEVIEWS
.. <service android:name=".MyWidgetListViewService" android:permission="android.permission.BIND_REMOTEVIEWS"/> </application> </manifest>
เขียนส่วน onUpdate() ใน MyWidgetListViewProvider
ให้ลบ onUpdate() อันเดิมออก เราจะมาเขียนใหม่ ให้เป็นแบบ listView
หลักการจะคล้ายของเดิม คือ ต้องสร้าง Intent ก่อนซึ่งในที่นี้คือ MyWidgetListViewService ซึ่งทำหน้าที่เชื่อม Factory (Adapter) จากนั้นก็ setRemoteAdapter โดยใส่เจ้า listView กับ intent นี้ลงไป
public class MyWidgetListViewProvider extends AppWidgetProvider { public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int i = 0; i < appWidgetIds.length; i++) { Intent intent = new Intent(context, MyWidgetListViewService.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.app_widget_listview); rv.setRemoteAdapter(appWidgetIds[i], R.id.listView, intent); rv.setEmptyView(R.id.listView, R.id.empty_view); appWidgetManager.updateAppWidget(appWidgetIds[i], rv); } super.onUpdate(context, appWidgetManager, appWidgetIds); } }
MyRemoteViewFactory
ตัว remoteViewFactory ที่เราเว้นไว้ ให้ override พวก method ที่ต้อง implement ออกมาให้หมด
จริงๆมัน เหมือน BaseAdapter เลย มี getCount() , getViewAt()
อันนี้ผมสมุติว่าที่ onCreate ก็สร้างลิสอาหารขึ้นมา แล้วที่ getViewAt() ก็ทำการสร้าง RemoteView จาก layout แถวที่เตรียมไว้
ใส่ข้อมูลให้เรียบร้อย จบพิธี
คำเตือน ที่ getViewTypeCount() ไม่ควรเป็น 0 เพราะเดี๋ยวมันจะไม่ขึ้น ระวังด้วย
สรุปที่ส่วนที่จำเป็น คือ getViewAt() , getCount() , getViewTypeCount()
public class MyRemoteViewFactory implements RemoteViewsService.RemoteViewsFactory { private static final int mCount = 20; private List<String> mFoodName = new ArrayList<String>(); private Context mContext; private int mAppWidgetId; public MyRemoteViewFactory(Context context, Intent intent) { mContext = context; mAppWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID); } @Override public void onCreate() { for (int i = 0; i < mCount; i++) { mFoodName.add("Padthai "+i); } } public RemoteViews getViewAt(int position) { // Construct a remote views item based on the app widget item XML file, // and set the text based on the position. RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.row_food); rv.setTextViewText(R.id.tv_food_name, mFoodName.get(position)); return rv; } @Override public RemoteViews getLoadingView() { return null; } @Override public int getViewTypeCount() { return 1; } @Override public long getItemId(int position) { return 0; } @Override public boolean hasStableIds() { return false; } @Override public void onDataSetChanged() { } @Override public void onDestroy() { } @Override public int getCount() { return mCount; } }
รันดูผลลัพธ์
ใส่ event Click ให้กับ รายการใน ListView
เริ่มต้นจากการกำหนด PendingIntentTemplate คือ การกำหนดว่าทุก รายการจะมี pendingIntent แบบนี้นะ
กดแล้วจะไป Activity ไหน โดยใส่ไว้ใน onUpdate()
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) { for (int i = 0; i < appWidgetIds.length; ++i) { Intent intent = new Intent(context, MyWidgetListViewService.class); intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetIds[i]); intent.setData(Uri.parse(intent.toUri(Intent.URI_INTENT_SCHEME))); RemoteViews rv = new RemoteViews(context.getPackageName(), R.layout.app_widget_listview); rv.setRemoteAdapter(R.id.listView, intent); rv.setEmptyView(R.id.listView, R.id.empty_view); // Add event click. Intent startActivityIntent = new Intent(context, MainActivity.class); PendingIntent startActivityPendingIntent = PendingIntent.getActivity(context, 0, startActivityIntent, PendingIntent.FLAG_UPDATE_CURRENT); rv.setPendingIntentTemplate(R.id.listView, startActivityPendingIntent); // update appWidgetManager.updateAppWidget(appWidgetIds[i], rv); } super.onUpdate(context, appWidgetManager, appWidgetIds); }
ต่อมา ก็มาเรียกการใช้งาน pendingIntent ที่ MyRemoteViewFactory
ใน getViewAt(position)
ตรงนี้เราจะทำการ สร้าง Intent และใส่ bundle ให้เรียบร้อย เตรียมส่งไปยัง activity ที่กำหนด
จากนั้นเรียก setOnClickFillIntent แล้ว pendingIntent ก็จะทำงาน
public RemoteViews getViewAt(int position) { RemoteViews rv = new RemoteViews(mContext.getPackageName(), R.layout.row_food); rv.setTextViewText(R.id.tv_food_name, mFoodName.get(position)); // add onclick. Bundle extras = new Bundle(); extras.putString("MY_KEY", mFoodName.get(position)); Intent fillInIntent = new Intent(); fillInIntent.putExtras(extras); rv.setOnClickFillInIntent(R.id.my_row, fillInIntent); return rv; }
แล้วที่ Activity ก็ไปรอรับ intent และ bundle ได้เลย
public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); Bundle bundle = getIntent().getExtras(); if (bundle != null) { String str = bundle.getString("MY_KEY"); TextView tv = (TextView) findViewById(R.id.tv_title); tv.setText(str); } } }
รันดูผลลัพธ์
คลิกที่รายการใน widget
สร้าง App Widget ด้วย Android Studio
เมื่อเข้าใจถึงโครงสร้างของ app widget แล้ว ตัว Android studio มีเครื่องมือช่วยในการสร้างอย่างง่ายดาย
คลิกขวาที่ app > New > Widget > App Widget
จากนั้นก็สามารถสร้าง Widget ได้ง่ายๆเลย ถ้าติ๊ก configuration screen มันจะ generate configuration activity ให้
ซึ่งในบทความนี้ไม่ได้กล่าวถึง มันก็คือการตั้งค่า widget โดยผู้ใช้ในแอป ไม่ต้องติ๊กก็ได้
สิ่งที่มัน generate ให้มีทั้ง Widget Class , Layout ของ widget , Metadata xml , ใส่ receiver ให้ใน AndroidManifest ด้วย
และสามารถใช้งาน Widget ได้ทันที เริ่ดจีจี
แต่ทั้งนี้ทั้งนั้น เราก็ต้องเข้าใจที่มาที่ไป หลักการทำงานก่อนจะดีกว่านะ
Source code
https://gist.github.com/anonymous/3fa540cc1b7450b7e5b63c5f39563276
จบแล้ว
การทำ Widget แบบแสดงข้อมูลทั่วไป ไม่มีอะไรซับซ้อนมากนัก แต่การทำ Widget แบบ Collection มีความซับซ้อนขึ้นมาหน่อยตรงที่มันใช้งานต่างจากพวก Adapter เพราะมันต้องใช้ RemoteViews นั่นเอง หากใช้งาน ListView ได้ พวก GridView ,StackView ก็ไม่ต่างกัน และหัวใจสำคัญคือ onUpdate() ที่ต้องเช็คพวกขนาดของ widget สร้าง RemoteView แล้วอัพเดทข้อมูลให้ถูกต้อง
หากมีคำถามหรือ คำแนะนำสามารถคอมเม้นไว้ได้เลยครับ
ขอบคุณครับ
Reference
https://developer.android.com/guide/topics/appwidgets/index.html
http://stackoverflow.com/questions/14299187/android-start-activity-when-pressing-widget-listview-item