본문 바로가기

OS/Android

Android 위젯 (Widget)

반응형

라디오 앱 유저들이 홈 화면에서 라디오를 컨트롤 할 수 있는 위젯을 만들어 달라는 요청이 있어서 이번 기회에 위젯에 관련된 내용들을 정리하고자 한다.

 

1. 위젯 유형

위젯을 만들때 어떤 종류의 위젯을 만들지 생각해봐야 한다.

크게 위젯은 네가지로 분류할 수 있다.

 

- 정보 위젯

일반적으로 정보 위젯은 사용자에게 시간에 따른 정보를 알려줄 때 사용한다.

보통 해당 위젯을 터치했을 때 연결된 앱으로 이동된다.

대표적인 정보 위젯은 시계 위젯, 날씨 위젯이다.

 

- 컬렉션 위젯

컬렉션 위젯은 동일한 유형의 여러 요소를 표시한다. 보통 컬렉션의 요소들을 사용하기 위해 세부 정보 뷰로 열기 기능을 포함하고 세로로 스크롤할 수 있다. 

- 관리 위젯

관리 위젯의 기본 목적은 사용자가 앱을 먼저 열 필요 없이 홈 화면에서 바로 트리거할 수 있는 함수를 표시하는 것이다.

관리 위젯의 일반적인 예는 사용자가 실제 음악 앱 외부에서 음악 트랙을 재생, 일시중지 또는 건너뛸 수 있는 음악 앱 위젯이다.

 

- 하이브리드 위젯 

다양한 유형의 요소를 결합한 위젯이다. 음악 플레이어 위젯은 기본적으로 관리 위젯이라고 볼 수 있지만 현재 재생중인 노래 제목과 같이 정보를 알려주어야 하는 경우 관리 위젯 + 정보 위젯의 성격을 가지고 있다.

따라서 라디오 플레이어 위젯은 하이브리드 위젯이라고 볼 수 있다.

 

2. 위젯 제한사항

위젯은 홈 화면에서 사용하기 때문에 디자인과 기능에 대한 제약이 있다.

 

- 기능 제한

위젯에 사용할 수 있는 유일한 기능은 터치 및 수직 스와이프이다. 가로 스와이프 같은 경우 홈 화면에서 이미 사용하고 있기 때문에 막아놓은 것으로 보인다.

 

- 레이아웃 제한

안드로이드 위젯은 RemoteViews 객체를 기반으로 하기 때문에 사용할 수 있는 뷰 객체들이 제한되어 있다.

사용할 수 있는 뷰 객체들은 다음과 같다.

  • FrameLayout
  • LinearLayout
  • RelativeLayout
  • GridLayout
  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextView
  • ViewFlipper
  • ListView
  • GridView
  • StackView
  • AdapterViewFlipper

 

3. 위젯 만들기

 

- 매니페스트에 AppWidgetProvider 등록

AppWidgetProvider는 위젯과 앱이 상호 작용 할 수 있게 도와주는 클래스이다. BroadCastReceiver를 상속 받았기 때문에 onReceive 함수에서 Action에 따른 기능 분할 처리가 가능하다.

그 외에도 onUpdate, onAppWidgetOptionsChanged, onDeleted, onDisabled, onRestored 함수를 오버라이딩할 수 있는데 자세한 내용은 https://developer.android.com/guide/topics/appwidgets?hl=en#ProviderBroadcasts 에서 확인하면 된다.

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

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

 

example_appwidget_info 파일은 앱 위젯의 기본 설정을 위한 파일이다.

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="300dp"
    android:minHeight="60dp"
    android:updatePeriodMillis="86400000"
    android:previewImage="@drawable/ic_launcher_foreground"
    android:initialLayout="@layout/layout_widget"
    android:resizeMode="horizontal|vertical"
    android:widgetCategory="home_screen">
</appwidget-provider>

 

- Widget 레이아웃 만들기 (R.layout.layout_widget)

제한된 뷰 객체를 사용하지 않게 주의하자.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout android:id="@+id/llRoot"
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="8dp"
    android:orientation="horizontal"
    android:background="@color/black"
    >

    <ImageView android:id="@+id/ivIcon"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:src="@mipmap/ic_launcher"
        android:layout_gravity="center_vertical"
        />

    <LinearLayout
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:layout_weight="1"
        android:layout_marginStart="8dp"
        android:layout_marginEnd="8dp"
        >

        <TextView android:id="@+id/tvTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="16dp"
            android:textColor="#ffffff"
            tools:text="제목입니다"
            />

        <TextView android:id="@+id/tvSubTitle"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14dp"
            android:textColor="#ffffff"
            tools:text="현재 재생중인 곡입니다."
            />

        <FrameLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="center_vertical"
            >

            <ImageView android:id="@+id/ivPrev"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginTop="5dp"
                android:layout_marginBottom="5dp"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="8dp"
                android:background="@drawable/ic_prev"
                android:layout_gravity="start|center_vertical"
                />

            <ImageView android:id="@+id/ivPlay"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginTop="5dp"
                android:layout_marginBottom="5dp"
                android:layout_marginStart="8dp"
                android:background="@drawable/ic_notification_play"
                android:layout_gravity="center"
                />
            <ImageView android:id="@+id/ivNext"
                android:layout_width="32dp"
                android:layout_height="32dp"
                android:layout_marginTop="5dp"
                android:layout_marginBottom="5dp"
                android:layout_marginStart="16dp"
                android:layout_marginEnd="8dp"
                android:background="@drawable/ic_next"
                android:layout_gravity="end|center_vertical"
                />

        </FrameLayout>
    </LinearLayout>
</LinearLayout>

 

- AppWidgetProvider 만들기

RemoteViews를 사용해봤다면 어렵지 않은 코드이다.

커스텀 액션을 상수로 만들어 이전 곡 재생, 현재 곡 재생, 다음 곡 재생 버튼을 등록해주었다.

class ExampleAppWidgetProvider : AppWidgetProvider()
{
    private val ACTION_BTN_PREV: String = ExampleAppWidgetProvider::class.java.getPackage().name + ".BTN_PREV"
    private val ACTION_BTN_PLAY: String = ExampleAppWidgetProvider::class.java.getPackage().name + ".BTN_PLAY"
    private val ACTION_BTN_NEXT: String = ExampleAppWidgetProvider::class.java.getPackage().name + ".BTN_NEXT"

    override fun onReceive(context: Context, intent: Intent)
    {
        super.onReceive(context, intent)
        Toast.makeText(context, intent.action, Toast.LENGTH_SHORT).show()
    }

    override fun onUpdate(
        context: Context,
        appWidgetManager: AppWidgetManager,
        appWidgetIds: IntArray
    ) {
        val widget = ComponentName(context, ExampleAppWidgetProvider::class.java)
        appWidgetIds.forEach {
            val views : RemoteViews = RemoteViews(context.packageName, R.layout.layout_widget)

            val pendingIntent = Intent(context, MainActivity::class.java).let {
                PendingIntent.getActivity(context, 0, it, 0)
            }
            views.setOnClickPendingIntent(R.id.llRoot, pendingIntent)

            views.setOnClickPendingIntent(R.id.ivPrev, getCustomActPedingIntent(context, ACTION_BTN_PREV))
            views.setOnClickPendingIntent(R.id.ivPlay, getCustomActPedingIntent(context, ACTION_BTN_PLAY))
            views.setOnClickPendingIntent(R.id.ivNext, getCustomActPedingIntent(context, ACTION_BTN_NEXT))

            views.setTextViewText(R.id.tvTitle, "임시 타이틀")
            views.setTextViewText(R.id.tvSubTitle, "임시 서브 타이틀")
            views.setImageViewResource(R.id.ivIcon, R.mipmap.ic_launcher)

            appWidgetManager.updateAppWidget(widget, views)
        }
    }

    override fun onAppWidgetOptionsChanged(
        context: Context?,
        appWidgetManager: AppWidgetManager?,
        appWidgetId: Int,
        newOptions: Bundle?
    ) {
        super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
    }

    override fun onDeleted(context: Context?, appWidgetIds: IntArray?) 
    {
        super.onDeleted(context, appWidgetIds)
    }
    override fun onDisabled(context: Context?) 
    {
        super.onDisabled(context)
    }

    override fun onRestored(context: Context?, oldWidgetIds: IntArray?, newWidgetIds: IntArray?) 
    {
        super.onRestored(context, oldWidgetIds, newWidgetIds)
    }

    private fun getCustomActPedingIntent(context: Context, customAction: String): PendingIntent 
    {
        val intent = Intent(context, javaClass).apply {
            action = customAction
        }
        return PendingIntent.getBroadcast(context, 0, intent, 0)
    }
}

 

4. 결과 확인

재생 버튼을 눌렀을 때 커스텀 액션이 노출되는 것을 확인할 수 있다.

반응형

'OS > Android' 카테고리의 다른 글