web analytics

Android Code : สร้าง App Widget เบื้องต้น ตอนที่ 2

cover-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>

11

 

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 อัน คืออันเก่าและอันใหม่นั่นเอง

a4

 

สร้าง 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>

12

 

สร้างคลาส 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;
    }

}

 

รันดูผลลัพธ์

a5

 

ใส่ 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

a6

 

สร้าง App Widget ด้วย Android Studio

เมื่อเข้าใจถึงโครงสร้างของ app widget แล้ว ตัว Android studio มีเครื่องมือช่วยในการสร้างอย่างง่ายดาย
คลิกขวาที่ app > New > Widget > App Widget

13

 

จากนั้นก็สามารถสร้าง Widget ได้ง่ายๆเลย ถ้าติ๊ก configuration screen มันจะ generate configuration activity ให้
ซึ่งในบทความนี้ไม่ได้กล่าวถึง มันก็คือการตั้งค่า widget โดยผู้ใช้ในแอป ไม่ต้องติ๊กก็ได้

14

 

สิ่งที่มัน generate ให้มีทั้ง Widget Class , Layout ของ widget , Metadata xml , ใส่ receiver ให้ใน AndroidManifest ด้วย
และสามารถใช้งาน Widget ได้ทันที เริ่ดจีจี

15

 

แต่ทั้งนี้ทั้งนั้น เราก็ต้องเข้าใจที่มาที่ไป หลักการทำงานก่อนจะดีกว่านะ

 

 

Source code

https://gist.github.com/anonymous/3fa540cc1b7450b7e5b63c5f39563276

 

จบแล้ว

การทำ Widget แบบแสดงข้อมูลทั่วไป ไม่มีอะไรซับซ้อนมากนัก แต่การทำ Widget แบบ Collection มีความซับซ้อนขึ้นมาหน่อยตรงที่มันใช้งานต่างจากพวก Adapter เพราะมันต้องใช้ RemoteViews นั่นเอง หากใช้งาน ListView ได้ พวก GridView ,StackView ก็ไม่ต่างกัน และหัวใจสำคัญคือ onUpdate() ที่ต้องเช็คพวกขนาดของ widget สร้าง RemoteView แล้วอัพเดทข้อมูลให้ถูกต้อง

หากมีคำถามหรือ คำแนะนำสามารถคอมเม้นไว้ได้เลยครับ

ขอบคุณครับ

 

Android Code : สร้าง App Widget เบื้องต้น ตอนที่ 3

 

Reference

https://developer.android.com/guide/topics/appwidgets/index.html
http://stackoverflow.com/questions/14299187/android-start-activity-when-pressing-widget-listview-item