web analytics

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

cover-1

สวัสดีครับ บทความนี้ได้มีโอกาสลองเล่นพวก Widget ลองเล่นแล้ว สนุกดี เลยเขียนบทความสรุปตามความเข้าใจไว้สักหน่อย
โดยจะขอแบ่งเป็น 2 ตอน

ตอนที่ 1 จะพามารู้จัก widget แล้วก็ลองสร้าง widget ง่ายๆสักตัว
ตอนที่ 2 จะทำ Multi widget แล้วก็ทำ Widget collection ListView

 

รู้จักกับ App Widget

App Widget คือ การทำส่วนหนึ่งของแอปเราไปแสดงในหน้า Home screen ที่เห็นชัดๆเลยคือพวก นาฬิกา ประโยชน์ของมันคือ User ไม่ต้องเปิดแอป แต่ก็สามารถเห็นข้อมูลหรือทำอะไรบางอย่างได้ เช่น สามารถควบคุมเครื่องเล่นเพลงได้ สามารถดูข้อมูลการเงินได้ทันที ถ้าเป็นแอปสภาพอากาศก็แสดงสภาพอากาศด้วย Widget ได้

รองรับเฉพาะ API level 17 ขึ้นไป

9

 

ประเภทของ Widget

มี 4 ประเภท

Information Widgets คือ พวกการแสดงข้อมูล เช่น แสดงสภาพอากาศ แสดงข้อมูลล
Collection Widgets คือ การแสดงพวก listView gridView ใน widget ให้นึกถึง email
Controls Widgets คือ widgets ที่เอาไว้ควบคุม เช่น ปุ่มเปิดปิด wifi
Hybrid Widgets คือ ลูกผสม อย่างเช่น เครื่องเล่นเพลง ก็จะมีการแสดงข้อมูลเพลง และก็มีปุ่มควบคุมเพลง เป็นต้น

10

 

ข้อจำกัดของ 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>

5-1

 

สร้าง AppWidgetProviderInfo (MetaData)

AppWidgetProviderInfo คือ ตัว MetaData (สิ่งที่ใช้อธิบายบางสิ่ง) ในที่นี้คือการสำหรับกำหนดคุณสมบัติของ Widget โดยเขียนเป็น xml

ก่อนอื่นสร้าง Resource directory สำหรับเก็บไฟล์ metadata ที่เป็น xml
คลิกขวาที่ res > New > Android resource directory

1

 

Metadata เขียนเป็น xml ดังนั้น เลือก Resource type เป็น xml แล้วกด ok

2

 

สร้าง XML resource file
คลิกขวา xml > New > XML resource file

3

 

ตั้งชื่อได้ตามต้องการ
ผมขอใช้เป็น app_widget_info.xml

4

 

ภายใน 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 ซึ่งกำหนดคนละที่กัน

8

 

กำหนด 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 ของเราให้เจอ แล้วก็ลากมันออกมา

a1

 

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>

7

 

ที่ 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 นั่นเอง ดังนั้นควรกำหนดไว้ดีกว่า เพราะไม่รู้แต่ละเครื่องมันจะต่างกันมัย

a2

 

ระวังเรื่องขนาด

ถ้าเรากำหนด minWidth , minHeight ไม่เหมาะสม widget จะแสดงไม่สมบูรณ์ รับรองผู้ใช้มี งง แน่นอน

6

 

การ 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();
        }
    }
}

 

ดูผลลัพธ์

a3

 

ซึ่งในความเป็นจริง การแสดงข้อมูลก็อาจจะแตกต่างกันไปตามขนาด ถ้าขนาดใหญ่ก็แสดงรายละเอียดมากเป็นต้น

 

ตอนนี้เราเข้าใจการทำงานของ Widget แล้ว สามารถสร้าง Widget ง่ายๆ ให้กับแอปได้ บทความตอนหน้าจะไปลองเล่นในส่วนของการทำหลายๆ Widget และทำ Widget แบบมี ListView กันซึ่งจะยากไปอีกนิดครับ อย่าพึ่งเบื่อกันนนะ

 

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

 

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