한 걸음 두 걸음

Android 안드로이드 ] Listview Widget 위젯 만들기 본문

FrontEnd/Android

Android 안드로이드 ] Listview Widget 위젯 만들기

언제나 변함없이 2019. 9. 21. 13:59
반응형

Android App Widget Document : https://developer.android.com/reference/android/appwidget/package-summary

위젯

앱 위젯은 핸드폰 홈화면에서 볼 수 있는 작은 어플리케이션입니다. 보통 시계, 날씨, 데이터리스트 등을 보여주는 역할을 합니다.하나의 어플리케이션을 설치해도 그 안에 여러 개의 위젯을 넣어 활용할 수 있습니다.


위젯 작동방식

우리가 만든 CustomApplication은 이를 클릭해서 실행시켜야 볼 수 있습니다. 이러한 앱은 Activity를 통해서 화면을 제어할 수 있지만, 위젯처럼 CustomApplication을 실행시키기 전에도 볼 수 있는 경우에는 홈화면의 화면을 제어하고 있는 런처앱을 통해 위젯을 띄워달라고 요청해야합니다.

그래서 위젯은 Activity의 개념이 없습니다. 런처 어플리케이션을 통해서(RemoteViews 활용) 데이터를 출력하는 형식이므로 기존에 제어하던 방식과 많이 다릅니다. 이 점 참고하고 따라와주세요!

이는 manifest.xml에 내가 위젯 파일 하나 만들어놨음ㅎㅎ 이라고 넣어서 런처앱에 알려주어야합니다.(이제 앱을 설치하면 위젯 목록에서 찾아볼 수 있습니다.) 만약 해당 위젯을 사용자가 바탕화면에 두면 위젯이 어떻게 작동할 것인지 데이터를 런쳐앱에 보애주어야합니다. 개발한 앱의 데이터를 런처앱으로 어떻게 전달하는 걸까요? 액티비티간 데이터 전달할 때도 intent를 통해서 전달했듯이, 앱 위젯의 데이터는 브로드캐스트 인텐트를 활용하여 보내줍니다. 사용자가 앱 위젯을 바탕화면에 두는 순간부터 런처 앱에서 intent를 발생시켜 브로드캐스트를 주기적으로 발생시킵니다.(구글에서는 최소 30분이라고 정해두었습니다. 그게 아니라면 새로고침 클릭 이벤트가 필요합니다.) 브로드캐스트가 발생하면 RemoteViews 객체를 만들어 런처앱에 요청사항 데이터를 담아 보냅니다.

이제 위젯을 만들기 위해 manifest에 등록하고 위젯data를 만들어야겠네요!

앱 위젯을 위해 지원되는 뷰 종류

Layout : LinearLayout, RelativeLayout, FrameLayout, GridLayout
View : Button, ImageView, ImageButton, ProgressBar, ListView, GridView, StackView, AdapterViewFilipper, AnalogClock, Chronometer, ViewFlipper


위젯을 만들기 시작!

이제 위젯이 대한 간략한 설명이 끝났으니 만들어보겠습니다

new - Widget - appWidget로 만들어줍니다. (스샷에서 짤렸네요 : )??)

필요한 것은 이렇게 만들어주면 자동으로 등록됩니다.

이대로 바로 빌드해서 바탕화면 꾹 눌러준 후 내가 만든 위젯 이름 찾아서 바탕화면에 둬보시면

이런 못생긴 예제 위젯을 보실 수 있습니다 ㅋㅋㅋㅋ

(참고로 이미지뷰로 위젯을 등록시키는 것은 https://onepinetwopine.tistory.com/345 링크를 참고해주세요)

1. WidgetInfo.xml

런처 앱에서 앱 위젯을 어떻게 작동시킬 것인지 규칙을 명세하는 xml입니다. 최소 높이 최소 폭 업데이트 주기 등을 설정할 수 있습니다.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialKeyguardLayout="@layout/widget_listview"
    android:initialLayout="@layout/widget_listview"
    android:minWidth="40dp" //한 칸 
    android:minHeight="40dp" //한 칸
    // = 1x1이 최소크기라는 뜻 
    android:previewImage="@drawable/example_appwidget_preview"
    android:resizeMode="horizontal|vertical" //사용자가 위젯 크기를 다시 재정의할 수 있는지 설정 ,none이면 불가
    //none이 아닐 때 최소 크기 지정하는 minResizeHeight/Width 등도 있다. 
    android:updatePeriodMillis="86400000" //참고로 이는 ms단위로 24시간입니다
    android:widgetCategory="home_screen"></appwidget-provider>
    //previewImage : 앱 위젯 선택할 때 보이는 이미지 설정
    //initalKeyguardLayout : 잠금화면용 위젯 설정

이 때 업데이트 주기는 구글 정책상 가장 짧게 잡아도 30분 주기로 업데이트가 가능합니다. 이 업데이트 주기로 브로드캐스트 리시버가 실행되어 Widget.java의 onUpdate()함수가 실행됩니다. 만약 30분보다 짧게 호출되어야한다면 알람매니저를 활용하는 방법을 찾아보시길 권합니다.

2. manifest.xml

     <receiver android:name=".WidgetListview">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_listview_info" />
        </receiver>

이렇게 매니페스트파일에 위젯을 알려주는 코드가 추가된 것을 확인하실 수 있을거에요!
위젯에는 인텐트필터가 APPWIDGET_UPDATE주기로 실행될 것임을 나타내고 있습니다.

3. Widget.xml

말 그대로 이 위젯이 어떻게 보일 것인가를 나타내는 뷰 설정하는 곳입니다. 사용가능한 레이아웃과 뷰는 위에서 알려드렸으므로 참고해주세요~

4. Widget.java

여기서부턴 찐주인공이라 할 수 있는 영역이네요.

public class WidgetListview extends AppWidgetProvider

이는 AppWidgetProvider를 상속받습니다. 이를 통해 위젯의 상태변화에 따라 호출되는 다양한 콜백함수 작동방법을 적을 수 있습니다.

public class WidgetListview extends AppWidgetProvider {

    /**
     * 위젯의 크기 및 옵션이 변결될 때마다 호출되는 함수
     * @param context
     * @param appWidgetManager
     * @param appWidgetId
     */
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_listview);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    /**
     * 위젯이 설치될 때마다 호출되는 함수
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
    }

    /**
     * 앱 위젯이 최초로 설치되는 순간 호출되는 함수 
     * @param context
     */
    @Override
    public void onEnabled(Context context) {
        // Enter relevant functionality for when the first widget is created
    }

    /**
     * 위젯이 제거되는 순간 호출되는 함수 
     * @param context
     */
    @Override
    public void onDisabled(Context context) {
        // Enter relevant functionality for when the last widget is disabled
    }

    /**
     * 위젯이 마지막으로 제거되는 순간 호출되는 함수 
     * @param context
     * @param appWidgetIds
     */
    @Override
    public void onDeleted(Context context, int[] appWidgetIds) {
        super.onDeleted(context, appWidgetIds);
    }
}

이렇게 상태변화에 따라 호출되는 함수가 많이 있으니 필요에따라 오버라이드 해서 사용하시면 되겠습니다ㅎㅎ
여기서 중요하게 봐야할 것은 바로 updateAppWidget의 RemoteViews입니다.

static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

        CharSequence widgetText = context.getString(R.string.appwidget_text);

        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_listview);
        views.setTextViewText(R.id.appwidget_text, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

RemoteViews는 런처앱에 위젯 업데이트를 의뢰할 때 의뢰내용을 저장하는 클래스입니다. 위에서는 widget의 Textview 데이터를 변경해야 할 사항이 있어 RemoteViews를 통해 setText를 해줄 것이라고 했습니다.(데이터 업데이트). 이 뿐만 아니라 EventClickListener를 설정할 때도 RemoteViews에 set해놓고 추후에 appWidgetManager를 통해 할 일 리스트를 런처앱으로 전달시켜줍니다ㅋㅋㅋ 여기서

 appWidgetManager.updateAppWidget(appWidgetId, views);

로 되어있는데, appWidgetId는 내가 만든 위젯에 부여한 번호이고, 이 번호를 가진 위젯에 views로 할일을 보내줄 것이라는 뜻입니다.


여기까지 앱 위젯을 만들어보고 텍스트뷰가 나타나는 위젯을 바탕화면에 만들어보고 작동방식에 대해 자세히 살펴봤습니다.


이 다음부터는 리스트뷰를 활용한 위젯을 만들겠습니다.

(위의 텍스트코드와는 무관하게 작성합니다. 다만 NEW-Widge_App Widget까지는 똑같이 해주세요! 이름도요)

위젯 사진!! V 


아까 지원되는 뷰들 중 리스트뷰도 있다고 말씀 드렸었는데 (androidx엔 recyclerview도 있었으나, 위젯에서는 리사이클러뷰까진 활용하지 않고 간단하게 리스트뷰로 활용할 예정입니다. 리사이클러뷰로 구현하고 싶으시다면 https://developer.android.com/reference/androidx/recyclerview/widget/RecyclerView?hl=en 참고해주세요)

리스트뷰는 데이터를 관리하는 Adapter가 따로 있습니다. 이는 RemoteViewsFactory 클래스를 통해 런처 앱에 알려주어야합니다. 이와 같은 RemoteViewsFactory를 얻기 위해서는 RemoteViewsService를 사용합니다. 간단한 Textview나 imageView를 사용할 때보다 리스트 뷰는 두 개 더 구현해주어야합니다. 리모트뷰서비스는 이벤트 처리를 위한 인텐트도 설정합니다. 모두 RemoteViews에 담겨갑니다.


이해가 가도록 하나 하나 다시 짚어볼께요 저도 만들어놓은 거 이해할 겸.

0번째로 new로 자동으로 만들어놓은 widget java와 xml입니다

0. ListViewWidget.java

여긴 마지막에 다룰게요 

0. widget_listview.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:background="#ffffff"
    android:orientation="vertical"
    android:padding="@dimen/widget_margin">

    <TextView
        android:id="@+id/widget_test_textview"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="TextView" />

    <ListView xmlns:android="http://schemas.android.com/apk/res/android"
        android:id="@+id/widget_listview"
        android:layout_width="fill_parent"
        android:layout_height="match_parent"
        android:layout_marginLeft="3dp"
        android:layout_marginTop="3dp"
        android:background="#ffffff" />

</LinearLayout>

리스트뷰를 만들기 위해서 필요한 것

1. item_collection.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:background="#ffffff"
    android:orientation="vertical"
    android:padding="@dimen/widget_margin">

<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/text1"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    android:gravity="center_vertical"
    android:paddingLeft="6dip"
    android:minHeight="?android:attr/listPreferredItemHeight"
    android:textColor="#FFFFFFFF"
    android:background="@android:color/holo_orange_dark"
    android:textSize="13dp"
    />
</LinearLayout>

저는 이처럼 텍스트뷰 하나 넣었습니다

2. WidgetItem.java

하나 따로 추가해주세요. 뭐 간단한 content만 리스트뷰로 보여줄거라 데이터셋을 만들어줄 필요는 없었으나, 저는 하나 만들어서 진행했습니다.


public class WidgetItem {
    int _id;
    String content;

    public WidgetItem(int _id, String content) {
        this._id = _id;
        this.content = content;
    }

    public int get_id() {
        return _id;
    }

    public void set_id(int _id) {
        this._id = _id;
    }

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
}

여기까지는 활용할 재료를 만들었고 아래부터는 이를 가지고 꾸며나가볼께요!

3. MyRemoteViewesFactory.java 만들어주기

따로 하나 만들어주세요. 필요한 것만 오버라이드 해줘도 되지만 저는 다 오버라이드 해서 설명 달아놨습니다. 불필요하다 생각되시는 분들은 빈 함수들 지우셔도 돼요!

/**
 * 런처 앱에 리스트뷰의 어뎁터 역할을 해주는 클래스
 */
public class MyRemoteViewsFactory implements RemoteViewsService.RemoteViewsFactory {

    //context 설정하기
    public Context context = null;
    public ArrayList<WidgetItem> arrayList;

    public MyRemoteViewsFactory(Context context) {
        this.context = context;
    }

    //DB를 대신하여 arrayList에 데이터를 추가하는 함수ㅋㅋ
    public void setData() {
        arrayList = new ArrayList<>();
        arrayList.add(new WidgetItem(1, "First"));
        arrayList.add(new WidgetItem(2, "Second"));
        arrayList.add(new WidgetItem(3, "Third"));
        arrayList.add(new WidgetItem(4, "Fourth"));
        arrayList.add(new WidgetItem(5, "Fifth"));
    }

    //이 모든게 필수 오버라이드 메소드

    //실행 최초로 호출되는 함수
    @Override
    public void onCreate() {
        setData();
    }

    //항목 추가 및 제거 등 데이터 변경이 발생했을 때 호출되는 함수
    //브로드캐스트 리시버에서 notifyAppWidgetViewDataChanged()가 호출 될 때 자동 호출
    @Override
    public void onDataSetChanged() {
        setData();
    }

    //마지막에 호출되는 함수
    @Override
    public void onDestroy() {

    }

    // 항목 개수를 반환하는 함수
    @Override
    public int getCount() {
        return arrayList.size();
    }

    //각 항목을 구현하기 위해 호출, 매개변수 값을 참조하여 각 항목을 구성하기위한 로직이 담긴다.
    // 항목 선택 이벤트 발생 시 인텐트에 담겨야 할 항목 데이터를 추가해주어야 하는 함수
    @Override
    public RemoteViews getViewAt(int position) {
        RemoteViews listviewWidget = new RemoteViews(context.getPackageName(), R.layout.item_collection);
        listviewWidget.setTextViewText(R.id.text1, arrayList.get(position).content);

        // 항목 선택 이벤트 발생 시 인텐트에 담겨야 할 항목 데이터를 추가해주는 코드
        Intent dataIntent = new Intent();
        dataIntent.putExtra("item_id", arrayList.get(position)._id);
        dataIntent.putExtra("item_data", arrayList.get(position).content);
        listviewWidget.setOnClickFillInIntent(R.id.text1, dataIntent);
        //setOnClickFillInIntent 브로드캐스트 리시버에서 항목 선택 이벤트가 발생할 때 실행을 의뢰한 인텐트에 각 항목의 데이터를 추가해주는 함수
        //브로드캐스트 리시버의 인텐트와 Extra 데이터가 담긴 인텐트를 함치는 역할을 한다.

        return listviewWidget;
    }

    //로딩 뷰를 표현하기 위해 호출, 없으면 null
    @Override
    public RemoteViews getLoadingView() {
        return null;
    }

    //항목의 타입 갯수를 판단하기 위해 호출, 모든 항목이 같은 뷰 타입이라면 1을 반환하면 된다.
    @Override
    public int getViewTypeCount() {
        return 1;
    }

    //각 항목의 식별자 값을 얻기 위해 호출
    @Override
    public long getItemId(int position) {
        return 0;
    }

    // 같은 ID가 항상 같은 개체를 참조하면 true 반환하는 함수
    @Override
    public boolean hasStableIds() {
        return false;
    }
}

ArrayList를 하나 만들어서 데이터를 넣고, getViewAt에서 연결해주었습니다.

4. MyRemoteViewsService.java 만들어주기

/**
 * RemoteViewsService를 상속받은 개발자 서비스 클래스
 * RemoteViesFactory를 얻을 목적으로 인텐트 발생에 의해 실행됩니다.
 */
public class MyRemoteViewsService extends RemoteViewsService {

    //필수 오버라이드 함수 : RemoteViewsFactory를 반환한다.
    @Override
    public RemoteViewsFactory onGetViewFactory(Intent intent) {
        return new MyRemoteViewsFactory(this.getApplicationContext());
    }
}

Manifest 내의 서비스로 이동해서 아래처럼 퍼미션 하나 추가해주세요,


        <service android:name=".MyRemoteViewsService"
            android:enabled="true"
            android:exported="true"
            android:permission="android.permission.BIND_REMOTEVIEWS"/>

RemoteViewsService시스템 만 시스템에 바인딩 할 수 있도록 권한을 부여하는 것입니다.

마지막 ListViewWidget.java

/**
 * Implementation of App Widget functionality.
 */
public class WidgetListview extends AppWidgetProvider {

    /**
     * 위젯의 크기 및 옵션이 변경될 때마다 호출되는 함수
     * @param context
     * @param appWidgetManager
     * @param appWidgetId
     */
    static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
                                int appWidgetId) {

//        여기부분 다 사용할 일 없어져서 주석처리함!
        CharSequence widgetText = context.getString(R.string.appwidget_text);
        // Construct the RemoteViews object
        RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.widget_listview);
        views.setTextViewText(R.id.widget_test_textview, widgetText);

        // Instruct the widget manager to update the widget
        appWidgetManager.updateAppWidget(appWidgetId, views);
    }

    /**
     * 위젯이 바탕화면에 설치될 때마다 호출되는 함수
     * @param context
     * @param appWidgetManager
     * @param appWidgetIds
     */
    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
        // There may be multiple widgets active, so update all of them
        for (int appWidgetId : appWidgetIds) {
            updateAppWidget(context, appWidgetManager, appWidgetId);
        }
        // RemoteViewsService 실행 등록시키는 함수
        Intent serviceIntent = new Intent(context, MyRemoteViewsService.class);
        RemoteViews widget = new RemoteViews(context.getPackageName(), R.layout.widget_listview);
        widget.setRemoteAdapter(R.id.widget_listview, serviceIntent);
//        클릭이벤트 인텐트 유보.
        //보내기
        appWidgetManager.updateAppWidget(appWidgetIds, widget);
        super.onUpdate(context, appWidgetManager, appWidgetIds);
    }
}

이때까지 열심히 만들어놓은 어댑터와 서비스를 잘 담아 보내줍니다. 안녕

~


Reference
안드로이드 공식 문서
깡쌤의 안드로이드 프로그래밍

context란?

Application 환경에 대한 전역 정보(어플리케이션 특화 리소스(센싱값)이나 패키지네임 등 클래스)를 접근하기 위한 인터페이스.
Activity 실행, Intent 브로드캐스트 리시버 활용 Intent 수신 등과 같이
응용 프로그램 수준의 작업을 수행하기 위한 API를 호출할 때 쓰인다.
Application Context는 application life-cycle에 접목되는 개념이고 Activity Context는 activity life-cycle에 접목되는 개념이다. 즉 전자는 하나의 애플리케이션이 실행되어 종료될 때까지 동일한 객체인 반면, 후자는 액티비티가 onDestroy() 된 경우 사라질 수 있는 객체이다. 이를 참고하여 목적에 맞도록 알맞은 종류의 context를 참조해야 한다.
https://shnoble.tistory.com/57


다른 포스팅으로 30분 이내의 업데이트 주기를 가진 시계 위젯을 만들어보겠습니다

반응형