꿈꾸는 시스템 디자이너

setOnClickPendingIntent를 가지는 AppWidget 예제 본문

Development/Android

setOnClickPendingIntent를 가지는 AppWidget 예제

독행소년 2013. 8. 29. 17:06

본 포스트에서는 setOnClickPendingIntent를 통해 위젯내에 위치한 View 객체를 클릭했을 때 위젯을 재구성하는 예제를 살펴본다.


<AndroidManifest.xml>

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.testsetonclickpendingintent"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="17"
        android:targetSdkVersion="17" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >

        <!-- AppWidget 선언, AppWidget은 일종의 브로드캐스트리시버임 -->
        <!-- android:name : AppWidget에 해당(AppWidgetProvider를 상속)하는 클래스명 명시 -->
        <!-- android:lable : 위젯의 레이블, 메뉴->위젯 화면에 표시할 위젯의 이름 -->
        <receiver
            android:name="com.example.testsetonclickpendingintent.MainWidget"
            android:label="setOnClickEventWidget" >

            <!-- AppWidget에서 수신할 action들 정의 (본 예제에서는 사용 안함) -->
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
                <action android:name="android.appwidget.action.APPWIDGET_ENABLED" />
                <action android:name="android.appwidget.action.APPWIDGET_DISABLED" />
                <action android:name="android.appwidget.action.APPWIDGET_DELETED" />
            </intent-filter>
            
            <!-- 이 브로드캐스트리시버가 AppWidget임을 명시 -->
            <!-- 이 AppWidget에 대한 명세파일의 위치 명시 -->
            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_info" />
        </receiver>
    </application>

</manifest>

메니페스트파일부터 살펴보면,  20번째줄부터 브로드캐스트 리스버를 정의하고 있다. AppWidget은 일종의 브로드캐스트리시버이기 때문에 <receiver>로 선언한다. 

21번째 줄은 AppWidget을 구현하는 클래스파일을 명시한 것이고, 22번째 줄은 위젯 목록에 표시할 위젯의 이름을 의미한다. 실제 아래의 그림처럼 표시된다.



25번째줄부터, 위젯에서 수신할 액션을 <intent-filter>를 통해 정의한다.

35번째 줄은 이 브로드캐스트리시버가 앱위젯이라는 것을 명시하는 것이고, 이 앱위젯에 대한 정보를 기술한 파일의 위치와 파일명을 명시한다(36번째 줄). 실제로 res폴더에 xml이란 폴더를 생성하고 widget_info.xml 파일을 생성한 후 아래와 같이 기술한다.


<widget_info.xml>

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="288dp"
    android:minHeight="144dp"
    android:updatePeriodMillis="0"
    android:initialLayout="@layout/widget_main" >
</appwidget-provider>

위의 파일에서 정의하는 내용은 화면에 표시할 앱위젯의 크기와, 앱위젯을 구성할 xml 파일에 대한 정보이다. 위에서 정의한 것처럼 widget_main.xml 파일을 기술할 차례이다.


<widget_main.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:layout_margin="10dp"
    android:orientation="vertical" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginBottom="10dp" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Action: "
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <TextView
            android:id="@+id/tvAction"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textAppearance="?android:attr/textAppearanceSmall" />

    </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="10dp" >

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Extras: "
            android:textAppearance="?android:attr/textAppearanceSmall" />

        <TextView
            android:id="@+id/tvExtra"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_weight="1"
            android:textAppearance="?android:attr/textAppearanceSmall" />

    </LinearLayout>

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:gravity="center" >

            <Button
                android:id="@+id/button1"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Button1"
                android:textSize="12sp" />

            <Button
                android:id="@+id/button2"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Button2"
                android:textSize="12sp" />

            <Button
                android:id="@+id/button3"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Button3"
                android:textSize="12sp" />

            <Button
                android:id="@+id/button4"
                style="?android:attr/buttonStyleSmall"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="Button4"
                android:textSize="12sp" />

        </LinearLayout>

</LinearLayout>

위의 레이아웃에서는 브로드캐스트리시버로 수신한 액션명과 getExtras로 수신한 세부 데이터를 표시할 View들과, setOnClickPendingIntent()를 이용해서 클릭이벤트를 발생시킬 네개의 버튼을 정의한다. 실제 아래의 모습으로 화면에 출력된다.




마지막으로 앱위젯을 실제로 구현할 차례이다.


<MainWidget.java>

package com.example.testsetonclickpendingintent;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.widget.RemoteViews;

public class MainWidget extends AppWidgetProvider {

	// 커스텀 액션
	public static String PENDING_ACTION = "com.example.testsetonclickpendingintent.Pending_Action";

	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);

		// RemoteViews 인스턴트 생성
		RemoteViews rv = new RemoteViews(context.getPackageName(),	R.layout.widget_main);
		
		// 수신한 인텐트로부터 액션값을 읽음
		String action = intent.getAction();

		// AppWidget의 기본 Action 들
		if (action.equals(PENDING_ACTION)) {
			rv.setTextViewText(R.id.tvAction, action);
			rv.setTextViewText(R.id.tvExtra,
					String.valueOf(intent.getIntExtra("viewId", 0)));
		}

		rv.setOnClickPendingIntent(R.id.button1, getPendingIntent(context, R.id.button1));
		rv.setOnClickPendingIntent(R.id.button2, getPendingIntent(context, R.id.button2));
		rv.setOnClickPendingIntent(R.id.button3, getPendingIntent(context, R.id.button3));
		rv.setOnClickPendingIntent(R.id.button4, getPendingIntent(context, R.id.button4));
		
		// 위젯 화면 갱신
		AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
		ComponentName cpName = new ComponentName(context, MainWidget.class);
		appWidgetManager.updateAppWidget(cpName, rv);
	}

	// 호출한 객체에 PendingIntent를 부여
	private PendingIntent getPendingIntent(Context context, int id) {
		Intent intent = new Intent(context, MainWidget.class);
		intent.setAction(PENDING_ACTION);
		intent.putExtra("viewId", id);
		
		// 중요!!! getBroadcast를 이용할 때 동일한 Action명을 이용할 경우 서로 다른 request ID를 이용해야함
		// 아래와 같이 동일한 request ID를 주면 서로 다른 값을 putExtra()하더라도 제일 처음 값만 반환됨
		// return PendingIntent.getBroadcast(context, 0, intent, 0);
		return PendingIntent.getBroadcast(context, id, intent, 0);
	}
}

11번째 줄처럼, 앱위젯은 AppWidgetProvider를 상속하여 구현한다.

버튼을 클릭할 때마다 브로드캐스트리시버로 전달할 인텐트의 액션으로 사용할 PENDING_ACTION을 선언한다(line 14).

onReceiver()는 브로드캐스트를 수신하는 부분으로 이 메소드를 통해 인텐트를 수신하고, 수신한 인텐트를 파싱하여 적절한 작업을 수행(위젯의 GUI 재구성)하는 방식으로 구현한다.


21번째 줄의 RemoteViews는 위젯의 레이아웃을 구성하는 View들의 집합이라고 생각하면된다. 앞서 구현한 widget_main.xml을 파라미터로 생성한다.

24번째 줄에서 수신한 인텐트의 액션값을 읽고, 만약 커스텀 액션(RENDIG_ACTION)이라면 해당 인텐트의 액션값과 엑스트라값을 TextView에  Text로 대입시킨다(line 26 ~ 31). 

33번째 줄부터는 각각의 버튼에 setOnClickPendingIntent()를 이용하여 클릭이벤트를 부여하는 기능이다. 앞선 포스트들에서 클릭이벤트 부여방식을 설명하였다.

그리고 39번째부터 41번째까지의 코드를 이용하여 앱위젯의 GUI를 갱신시킨다.


위의 onReceive() 코드에서는 앱위젯상의 버튼에 클릭이벤트를 부여하는 것과, 버튼 클릭으로 발생한 인텐트를 수신하는 내용이 모두 기술되어 있어서 혼란스러울 수 있다. 즉, 인텐트를 발생시키는 코드와 발생한 인텐트를 수신하여 처리하는 코드 모두가 한 메소드에 정의되어 있다는 것을 이해해야 한다. 이는 앱위젯이 일종의 브로드캐스트리시버이기 때문에 브로드캐스트를 수신할 때마다 매번 새롭게 앱위젯의 화면(RemoteView)을 재구성해야 한다는 것이다.


마지막으로 버튼에 클릭이벤트를 부여할 때 이용하는 getPendingIntent() 메소드를 살펴보면, 46번 째 줄부터, 새로운 인텐트를 생성하고 액션값과 엑스트라값을 부여하도록 구현했다. 생성한 인텐트를 PendingIntent로 반환할 때 getBroadcast(Context context, int requestID, Intent intent, int flag) 메소드를 이용하는데 여기서 주의할 사항이 있다.

각각의 버튼에 클릭이벤트를 부여할 때, 인텐트를 생성하고 setAction()을 통해 동일한 액션(PENDING_ACTION)을 설정했다. 그리고 putExtra()를 이용해서 각 버튼의 고유 ID값을 설정하였으므로, 브로드캐스트리시버에서 인텐트를 파싱할 때 엑스트라값으로 실제 눌린 버튼을 구부할 수 있으리라 예상했다. 하지만 실제로는(52번째줄 처럼 구현하면) 서로 다른 버튼을 클릭하더라도 맨 처음 클릭한 버튼의 엑스트라 값이 반환된다. 안드로이드의 버그인지는 모르겠지만 이 부분 때문에 오랜기간 고민을 해왔는데, 결국 현재까지 찾은 해결책으로는 서로 다른 View들이 동일한 액션값을 이용할 경우, getBoradcast() 메소드에서 서로 다른 requestID를 가지도록 해야 한다는 것이다.

결국 53번째 줄과 같이 requestID로 각 버튼들의 ID값을 부여하는 방식으로 문제를 해결했다.


최종 동작 화면은 아래와 같다.

각 버튼을 클릭할 때마다 서로 다른 엑스트라 값을 출력하는 것을 확인할 수 있다.






Comments