[Andorid]Custom Widget 만들기!

2015. 1. 29. 13:05Programming/Android

반응형

App widget 위젯이란, 아래 그림과 같이 홈스크린 화면에 떠서 USER와 소통 하는 녀석을 말합니다.


[HomeScreen에 떠있는 Widget의 모습]


위젯을 만들기 위해서는 크게 4가지 준비물들이 필요합니다.

 1. AppWidgetProvider Class를 상속받은 Provider Class

 2. Widget의 View Layout을 기술한 xml 파일

 3. Widget의 속성 (Meta data)를 기술한 xml 파일

 4. Widget의 속성과 Receiver를 정의할 AndroidManifest.xml 파일


자 그럼 순서대로 Widget을 만드는 방법에 대해 알아 보겠습니다. 



  1. AppWidgetProvider Class를 상속받은 Provider Class

위젯의 기능을 정의한 Class (실제 위젯의 동작들을 정의한 Class 입니다.) 





   2. Widget의 View Layout을 기술한 xml 파일

 위젯의 모양을 정의하는 xml 입니다. (실제 위젯이 홈스크린에 붙일 경우의 아래의 xml 형태의 모양이 됩니다.)

 




   3. Widget의 속성 (Meta data)를 기술한 xml 파일

위젯의 속성을 지정하는 xml 파일 입니다. (최소 가로, 세로 Size 설정, widget의 Layout 설정)

widget_configuration.xml 



 


   4. Widget의 속성과 Receiver를 정의할 AndroidManifest.xml 파일

위젯의 속성과 기능들을 사용할 수 있게 정의해놓은 xml 파일 입니다.





   자 이렇게 해서 완성된 APP Widget을 구경해 볼까요?




자 그럼 한단계 한단계 코드를 보면서 설명하겠습니다.

  1. AppWidgetProvider Class를 상속받은 Provider Class


AppWidgetProvider를 상속받게 되면 widget 동작 및 생명주기에 관한 아래의 메서드들을 오버라이드 해서 사용해야 합니다. 필수적으로 구현해야 하는 부분 이므로 유심히 보셔야 합니다.


public void onEnabled(Context context)

   메서드는 APP Widget이 "처음" 생성될 때 불립니다. 여기서 "처음" 에 double quotation mark를 붙인 이유는.. 같은 위젯을 여러개 띄우면, 첫번째 위젯을 띄울 때만 호출되기 때문입니다. 즉, 첫번째 위젯을 붙일 때는 호출 되지만, 동일한 위젯을 하나 더 추가해서 붙일 때는 호출되지 않습니다.


public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds)

   메서드는 APP Widget의 속성(Meta data)에서 지정해준 updatePeriodMillis 값에 따라 주기 적으로 호출됩니다. 또한 처음 widget이 화면에 붙을 때, init() 작업을 해주기 위해서도 call 됩니다. (Configuration activity를 따로 두었다면, 처음 붙을 때 불리지는 않습니다. 대신 Conf, Activity가 init을 하죠.)

이 메서드가 가장 중요한 메서드 입니다. 보통 Handler를 넣어주어서 작업을 수행 합니다.


public void onDeleted(Context context, int[] appWidgetIds)

   메서드는 APP Widget이 Widget host로 부터 삭제될 때 불립니다.


public void onDisabled(Context context)

   메서드는 APP Widget이 Widget이 "삭제" 될 때 불립니다. 여기서도 "삭제"에 double quotation mark를 붙인 이유는..... onEnabled와 비슷합니다. 즉 이 메서드도 가장 마지막에 남아있는 APP Widget이 detach 되었을 때, 그 때만 호출되기 때문입니다.


public void onReceive(Context context, Intent intent)

   메서드는 일반적인 브로드캐스팅 receiver 입니다. 이 메서드는 위에 나열한 CallBack 들보다 먼저 불리게 됩니다. 이 메서드는 implement할 "필요가 없습니다." 이미 다 구현이 되어 있기 때문입니다. 왜 이 메서드도 double quotaion mark를 넣었을까요?

기본적으로 이 onReceive funtion은 AppWidgetProvider에 구현이 되어있습니다. 뭐가 구현이 되어있냐구요? Widget이 붙었을 때 이 콜백함수를 불러라, Widget이 삭제되었을 때 이 콜백함수를 불러라. 

뭐 이런 내용이 미리 구현이 되어있습니다. 하지만 "전문적인 개발자" 라면 이러한 단순한 CallBack 만 부르는 onReceive 의 내용이 맘에 들지 않을 수도 있습니다. 그렇다면!! 추가적인 implement를 해주시면 되겠습니다.


자 그럼 구현한 코드를 보겠습니다.


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
package arabiannight.tistory.com.testappwidget2;
 
import android.app.AlarmManager;
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.util.Log;
import android.widget.RemoteViews;
import android.widget.Toast;
 
public class MyCustomWidget extends AppWidgetProvider {
     
    private static final String TAG = "MyCustomWidget";
    private Context context;
     
    @Override
    public void onEnabled(Context context) {
        Log.i(TAG, "======================= onEnabled() =======================");
        super.onEnabled(context);
    }
     
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
            int[] appWidgetIds) {
         
        Log.i(TAG, "======================= onUpdate() =======================");
         
        this.context = context;
         
        super.onUpdate(context, appWidgetManager, appWidgetIds);
         
        for(int i=0; i<appWidgetIds.length; i++){
            int appWidgetId = appWidgetIds[i];
            RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.mycustomwidget);
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
     
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        Log.i(TAG, "======================= onDeleted() =======================");
        super.onDeleted(context, appWidgetIds);
    }
     
    @Override
    public void onDisabled(Context context) {
        Log.i(TAG, "======================= onDisabled() =======================");
        super.onDisabled(context);
    }
     
    /**
     * UI 설정 이벤트 설정
     */
    public void initUI(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        Log.i(TAG, "======================= initUI() =======================");
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.mycustomwidget);
 
        Intent eventIntent              = new Intent(Const.ACTION_EVENT);
        Intent activityIntent           = new Intent(Const.ACTION_CALL_ACTIVITY);
        Intent dialogIntent             = new Intent(Const.ACTION_DIALOG);
 
        PendingIntent eventPIntent          = PendingIntent.getBroadcast(context, 0, eventIntent        , 0);
        PendingIntent activityPIntent       = PendingIntent.getBroadcast(context, 0, activityIntent     , 0);
        PendingIntent dialogPIntent         = PendingIntent.getBroadcast(context, 0, dialogIntent       , 0);
 
        views.setOnClickPendingIntent(R.id.btn_event            , eventPIntent);
        views.setOnClickPendingIntent(R.id.btn_call_activity    , activityPIntent);
        views.setOnClickPendingIntent(R.id.btn_set_alram        , dialogPIntent);
 
        for(int appWidgetId : appWidgetIds) {
            appWidgetManager.updateAppWidget(appWidgetId, views);
        }
    }
 
    /**
     * Receiver 수신
     */
    @Override
    public void onReceive(Context context, Intent intent) {
        super.onReceive(context, intent);
         
        String action = intent.getAction();
        Log.d(TAG, "onReceive() action = " + action);
         
        // Default Recevier
        if(AppWidgetManager.ACTION_APPWIDGET_ENABLED.equals(action)){
             
        }
        else if(AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(action)){
            AppWidgetManager manager = AppWidgetManager.getInstance(context);
            initUI(context, manager, manager.getAppWidgetIds(new ComponentName(context, getClass())));
        }
        else if(AppWidgetManager.ACTION_APPWIDGET_DELETED.equals(action)){
             
        }
        else if(AppWidgetManager.ACTION_APPWIDGET_DISABLED.equals(action)){
             
        }
         
        // Custom Recevier
        else if(Const.ACTION_EVENT.equals(action)){
            Toast.makeText(context, "Receiver 수신 완료!!.", Toast.LENGTH_SHORT).show();
        }
        else if(Const.ACTION_CALL_ACTIVITY.equals(action)){
            callActivity(context);
        }
        else if(Const.ACTION_DIALOG.equals(action)){
            createDialog(context);
        }
    }
     
    /**
     * Activity 호출 (Intent.FLAG_ACTIVITY_NEW_TASK)
     */
    private void callActivity(Context context){ 
        Log.d(TAG, "callActivity()");
        Intent intent = new Intent("arabiannight.tistory.com.widget.CALL_ACTIVITY");
        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        context.startActivity(intent);
    }
     
    /**
     * Dialog Activity 호출 (PendingIntent)
     */
    private void createDialog(Context context){
        Log.d(TAG, "createDialog()");
        AlarmManager alarmManager = (AlarmManager)context.getSystemService(Context.ALARM_SERVICE);
          
        Intent Intent = new Intent("arabiannight.tistory.com.widget.CALL_PROGRESSDIALOG");
        PendingIntent pIntent = PendingIntent.getActivity(context, 0, Intent, 0);
          
        alarmManager.set(AlarmManager.RTC, System.currentTimeMillis(), pIntent);
    }
     
}

onUpdate 메서드에서 주의 사항 입니다.


만약 아래의 시계 Widget 에서 시침을 빼야 한다면, 이미 홈스크린에 붙여진 모든 Widget의 시침을 빼야 하기 때문에 모든 Widget의 개수를 얻어와서 Update 해줘야 합니다.




APP Widget에서 UI를 셋팅해 주는 부분 입니다. 보통 Widget(TextView, EditText등..)과는 달리 APP Widget에서는 RemoteViews를 사용해서 UI를 구성해 주어야 합니다.


1. APP Widget은 RemoteViews를 사용해서 UI를 구성해야 합니다. RemoteViews의 설명과 사용법에 대한 내용은 아래의 내용을 참고 하시기 바랍니다.

RemoteViews의 설명과 사용법 : http://blog.naver.com/PostView.nhn?blogId=huewu&logNo=110089286698

2. 역시 위와 마찬가지로 모든 APP Widget의 개수를 얻어와서 Update 해줘야 합니다.

3. 중요한 부분입니다. onUpdate() 메서드에서도 보시면 appWidgetManger.updateAppWidget() 이란 메서드를 사용하고 있는데요. APP Widget에 관한 모든 부분은 AppWidgetManger가 관리를 해주고 있기 때문에, APP Widget의 Update나 관리, Receiver 사용시에 활용하게 됩니다.



APP Widget의 onReeive() 메서드 부분 입니다.






  2.  Widget의 View Layout을 기술한 xml 파일

특별히 Widget의 Layout 이라고 다른게 아니라, 보통 Layout 작성하듯 만들어 주시면 됩니다. 



대신 이벤트들은 보통 view와는 달리 Remoteviews의 setOnClickPendingIntent() 메서드를 사용해야 합니다.

PendingIntent에 대한 자세한 설명은 아래를 참고 하시기 바랍니다.

[PendingIntent  설명 바로 가기]


initUI() 에서 이벤트 클릭시 Flow 입니다. 

1initUI() 이벤트 클릭하면 Intent 전달. (Intent Action = arabiannight.tistory.com.widget.ACTION_EVENT)

2. AndroidManifest.xml 파일에서 해당 intent Action에 대한 filtering 작업을 진행 합니다.

3. 전달받은 Intent와 동일한 Intent Filter가 선언되어 있는지 확인 합니다.

4. 동일한 Intent Filter가 선언되어 있을 경우, 전달받은 Intent를 MyCustomWidget(AppWidgetProvider) 의 onReceive()메서드로 전송해 줍니다. onReceive() 메서드에는 아래와 같은 코드가 구현되어 있기 때문에 전달 받은 Intent에 맞는 조건문을 실행하게 됩니다.

 else if(Const.ACTION_EVENT.equals(action)){
      Toast.makeText(context, "Receiver 수신 완료!!.", Toast.LENGTH_SHORT).show();
 }

 public class Const {

    public static final String  ACTION_EVENT = "arabiannight.tistory.com.widget.ACTION_EVENT";

 }






  3. Widget의 속성 (Meta data)를 기술한 xml 파일

위젯의 속성을 지정하는 xml 파일 입니다. (최소 가로, 세로 Size 설정, widget의 Layout 설정)

widget_configuration.xml 



  이 메서드는 APP Widget에 관련된 meta data들을 기술해주면 됩니다. 저장 위치는 /res/xml/ 이고, Tag는 이렇게 시작합니다. <appwidget-provider>

여기에 지정해주는 속성(attribute) 들은 다음과 같습니다.

minWidth(최소넓이) / minHeight(최소높이) / updatePeriodMillis(AppWidgetProvider의 onUpdate()메서드 호출 주기 설정) / initalLayout(초기 레이아웃 설정) / configure(Activity 호출)

minWidth와 minHeight는 Widget Host가 dimension을 계산할 때 쓰입니다. HomeScreen에 붙을 때는 HomeScreen이 정의한 Cell Size라는 단위가 있는데, 이 단위에 맞게 round up 됩니다.

(Developer에 따르면 orientaion등의 최악의 상황을 대비하여 72 * 72를 min으로 잡으라고 합니다.) -> one cell

updatePeriodMills는 얼마나 자주 onUpdate() 메서드를 호출할지 결정하는 속성입니다.

(Developer에 따르면 정확한 시간에 작동하지 않을 뿐더러, 배터리를 아끼기 위해서 가능한 적게 설정하길 권장합니다. 추가적으로 User가 Update 주기를 설정할 수 있도록 하는 것이 가장 좋은 방법이라고 제시하고 있습니다.)

configure 속성 에는 앞서 말했던 부가적인, ConfugurationActivity가 있을 경우에 link 해주시면 되겠습니다.


- 부가적인 것 : Widget의 configuraion 변경을 제공할 Activity

 이 Activity는 일반 Activity와 같습니다. 

단지, 속성(Meta Data)를 통해 기술하며, APP Widget이 처음 붙을 때, Strart 된다는 것만 다릅니다.


- Widget Cell Size

위젯은 홈스크린의 일정 공간을 차지합니다. 홈스크린에서 위젯이 차지할 수 있는 공간은 홈스크린을 일정한 비율로 나눈 영역인 셀(Cell) 단위로 관리되며, 위젯의 크기는 셀을 몇 개 사용하느냐에 따라 결정됩니다.

일반적으로 안드로이드 단말에서 위젯이 차지할 수 있는 홈스크린 영역은 다음과 같이 가로 4개, 세로 4개 총 16개로 나누어집니다.

홈스크린에서 위젯이 차지할 수 있는 영역 구성


각 위젯은 최소 1개의 셀부터 시작하여 최대 16개의 셀을 차지할 수 있으며, 사각형 형태로만 셀을 차지할 수 있습니다. (4x1, 2x2 등) 위젯이 차지하는 셀의 크기는 매니페스트의 위젯 노드에서 설정하며, 이에 대한 자세한 사항은 잠시 뒤에 살펴보겠습니다.


- minWidth, minHeight 속성 지정 하기

화면에 표시되는 위젯의 크기는 위젯 프로바이더의 Min width, Min height 속성에 따라 결정됩니다. 위젯은 셀(Cell) 단위로 홈스크린의 공간을 차지합니다. 홈 스크린의 화면 전환을 감안하면 한 셀의 한 변이 가질 수 있는 최소 크기는 74dp 입니다. 여기에서 안드로이드 단말의 다양한 해상도로 인해 위젯이 화면에 표시될 때 오차가 발생하는 것을 감안하여 양 쪽에 1dp씩을 빼주면, 실질적으로 한 셀의 한 변이 가질 수 있는 최소 크기는 72dp가 됩니다. 

셀 크기에 따른 최소 크기를 계산하는 공식은 다음과 같습니다.

(셀 개수 * 74) - 2 (단위 : dp)

위의 공식을 사용하여 계산한 셀 개수에 따른 최소 크기는 다음과 같습니다.


위에서 계산한 수치를 통해 일반적으로 많이 쓰이는 위젯의 크기를 정리해보면 다음과 같습니다.


여기에서는 4x2 크기의 위젯을 작성하므로, Min width에 294dp, Min height에 146dp를 지정하였습니다. 






   4. Widget의 속성과 Receiver를 정의할 AndroidManifest.xml 파일

위젯의 속성과 기능들을 사용할 수 있게 정의해놓은 xml 파일 입니다.



AndroidManifest.xml 등록 과정

  1. AppWidgetProvider 등록

  2. AppWidgetProvider 안에 <meda-data>를 등록해 줍니다. 기존에 만든 widget_configuration.xml 파일을 추가해 줍니다.

  3. AppWidgetProvider 에서 사용할 Receiver를 추가해 줍니다.

  4. <action android:name="android.appwidget.action.APPWIDGET_UPDATE"/> filter는 명시적으로 추가해 주어야 합니다.

  5. 그 외 filter들은 상황에 맞게 추가해 주시면 됩니다.

  6. 호출할 Activity 들을 등록해 줍니다.





파일첨부 : 

 TestAppWidget2.zip




스크린샷 : 



출처: http://arabiannight.tistory.com/238 , http://arabiannight.tistory.com/entry/안드로이드Android-App-widget을-만들어-보자-2


반응형