咱们有这么一个需求,在桌面添加一个app的小部件,小部件展示app里面的热门数据,点击小部件的刷新按钮实现刷新小部件上面的数据的功能。
咱们先看实现的效果图:
小部件的基本需求实现如上:
说明,先创建一个AppWidgetProvider
HimalayanWidgets.kt
package com.gwm.widget.widget
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.Context
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.RemoteViews
import com.gwm.widget.MainActivity
import com.gwm.widget.R
import com.gwm.widget.WidgetBroadcast
/**
* Implementation of App Widget functionality.
*/
class HimalayanWidgets : AppWidgetProvider() {
private val TAG = "HimalayanWidgets"
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
Log.e(TAG, "onUpdate")
// There may be multiple widgets active, so update all of them
for (appWidgetId in appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId)
}
}
override fun onEnabled(context: Context) {
Log.e(TAG, "onEnabled")
// Enter relevant functionality for when the first widget is created
}
override fun onDisabled(context: Context) {
Log.e(TAG, "onDisabled")
// Enter relevant functionality for when the last widget is disabled
}
}
val bundle = Bundle()
internal fun updateAppWidget(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int
) {
Log.e("HimalayanWidgets", "updateAppWidget")
val widgetText = context.getString(R.string.hot)
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.himalayan_widgets)
views.setTextViewText(R.id.hot, widgetText)
views.setTextViewText(R.id.recommend, context.getString(R.string.recommend))
bundle.putString("update", context.getString(R.string.add_widget))
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).putExtras(bundle),
PendingIntent.FLAG_UPDATE_CURRENT,
bundle
)
views.setOnClickPendingIntent(R.id.hot, pendingIntent)
val broadcast = PendingIntent.getBroadcast(
context,
10086,
Intent(context, WidgetBroadcast::class.java),
PendingIntent.FLAG_UPDATE_CURRENT
)
views.setOnClickPendingIntent(R.id.iv_refresh, broadcast)
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
配置小部件对应的清单文件
<receiver
android:name=".widget.HimalayanWidgets"
android:exported="true">
<intent-filter>
//下面的action不要动
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/himalayan_widgets_info" />
</receiver>
在配置下小部件的 himalayan_widgets_info对应的resource文件
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:initialKeyguardLayout="@layout/himalayan_widgets"
android:initialLayout="@layout/himalayan_widgets"
android:minWidth="200dp"
android:minHeight="100dp"
android:previewImage="@drawable/xm"
android:resizeMode="horizontal|vertical"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen" />
再看下小部件对应的layout布局文件himalayan_widgets.xml
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/replace_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/bg_shape"
android:theme="@style/ThemeOverlay.HimalayanWidget.AppWidgetContainer">
<LinearLayout
android:id="@+id/ll_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:layoutAnimation="@anim/layout_rotate_refresh">
<ImageView
android:id="@+id/iv_refresh"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/news"
android:src="@drawable/refresh" />
</LinearLayout>
<TextView
android:id="@+id/hot"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="30dp"
android:layout_marginTop="30dp"
android:background="@drawable/hot_shape"
android:contentDescription="@string/hot"
android:padding="10dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:text="@string/hot"
android:textColor="?attr/appWidgetTextColor"
android:textSize="18sp"
android:textStyle="bold" />
<TextView
android:id="@+id/recommend"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="30dp"
android:layout_toEndOf="@+id/hot"
android:background="@drawable/recommend_shape"
android:contentDescription="@string/recommend"
android:padding="10dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:text="@string/recommend"
android:textColor="?attr/appWidgetTextColor"
android:textSize="18sp" />
<TextView
android:id="@+id/rink"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/hot"
android:layout_margin="30dp"
android:background="@drawable/recommend_shape"
android:contentDescription="@string/recommend"
android:padding="10dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:text="@string/rink"
android:textColor="?attr/appWidgetTextColor"
android:textSize="18sp" />
<TextView
android:id="@+id/news"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/recommend"
android:layout_toEndOf="@+id/rink"
android:background="@drawable/hot_shape"
android:contentDescription="@string/recommend"
android:padding="10dp"
android:paddingStart="15dp"
android:paddingEnd="15dp"
android:text="@string/news"
android:textColor="?attr/appWidgetTextColor"
android:textSize="18sp" />
</RelativeLayout>
上面布局文件有个小细节,刷新按钮的动画是使用这个文件做的,layout_rotate_refresh.xml
<?xml version="1.0" encoding="utf-8"?>
<layoutAnimation xmlns:android="http://schemas.android.com/apk/res/android"
android:animation="@anim/rotate_refresh" />
再看下核心动画文件rotate_refresh.xml
<?xml version="1.0" encoding="utf-8"?>
<rotate xmlns:android="http://schemas.android.com/apk/res/android"
android:duration="1000"
android:fillAfter="true"
android:fromDegrees="0"
android:pivotX="50%"
android:pivotY="50%"
android:toDegrees="360" />
然后说下核心逻辑HimalayanWidgets.kt文件中的onEnabled、onDisabled方法是在添加小部件和移除小部件会执行的方法,onUpdate方法会在第一添加小部件和himalayan_widgets_info.xml文件中配置的间隔时间到了后会在执行onUpdate方法。
在第一次添加小部件的时候就会执行onUpdate方法,然后咱们在这个方法里面设置默认数据(或者从网络请求对应的数据),然后设置给小部件对应的TextView或者ImageView上面,设置方法很简单如下
val widgetText = context.getString(R.string.hot)
// 拿到小部件的布局文件
val views = RemoteViews(context.packageName, R.layout.himalayan_widgets)
//设置TextView的文本
views.setTextViewText(R.id.hot, widgetText)
views.setTextViewText(R.id.recommend, context.getString(R.string.recommend))
bundle.putString("update", context.getString(R.string.add_widget))
//点击文本打开一个activity的方法,先得到一个PengIntenet
val pendingIntent = PendingIntent.getActivity(
context,
0,
Intent(context, MainActivity::class.java).putExtras(bundle),
PendingIntent.FLAG_UPDATE_CURRENT,
bundle
)
//设置文本的点击事件,点击后打开对应的activity
views.setOnClickPendingIntent(R.id.hot, pendingIntent)
//拿到一个广播的PengIntent,用于点击刷新按钮的时候发送一个更新数据的广播
val broadcast = PendingIntent.getBroadcast(
context,
10086,
Intent(context, WidgetBroadcast::class.java),
PendingIntent.FLAG_UPDATE_CURRENT
)
//设置刷新按钮的点击事件,点击后发送对应的广播
views.setOnClickPendingIntent(R.id.iv_refresh, broadcast)
// 最后执行更新小部件
appWidgetManager.updateAppWidget(appWidgetId, views)
上面打开的MainActivity.kt文件如下:
package com.gwm.widget
import android.os.Bundle
import android.util.Log
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val data = intent.extras?.getString("update")
data?.let {
findViewById<TextView>(R.id.home).text = data
Log.e("HimalayanWidgets", data)
}
}
}
上面的布局文件activity_main.xml如下:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/home"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Hello World!"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
下面是要打开发送的广播类WidgetBroadcast.kt
package com.gwm.widget
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import com.gwm.widget.bridge.UseCase
import com.gwm.widget.utils.WidgetUtils
/**
@author xiayiye5
@date 2023/4/4 17:44
*/
class WidgetBroadcast : BroadcastReceiver() {
private val useCase: UseCase by lazy { UseCase() }
override fun onReceive(context: Context?, intent: Intent?) {
context?.let {
//发送请求最新数据
useCase.request()
//收到数据后刷新小部件
WidgetUtils.liveData.observeForever { widgetData ->
//刷新最新数据
WidgetUtils.updateWidget(it, widgetData)
}
}
}
}
上面UseCase.kt文件用于模拟请求数据的类
package com.gwm.widget.bridge
import com.gwm.widget.bean.WidgetData
import com.gwm.widget.utils.WidgetUtils
/**
@author xiayiye5
@date 2023/4/6 10:18
*/
class UseCase {
private val dataArray =
arrayOf(
"最火", "最热", "最爆", "最辣", "最长", "最短", "最冷", "精选", "红火", "排名", "第一",
"最久", "天南", "地北", "天长", "地久", "搞笑", "喜剧", "娱乐", "最佳", "折腰", "投放",
"最近", "最久", "最短", "耗时", "少动", "多跑", "最赞", "少赞", "秒赞", "谬赞", "热情"
)
fun request() {
//请求到数据后发送给小部件
WidgetUtils.liveData.value = WidgetData(
dataArray.random(),
dataArray.random(),
dataArray.random(),
dataArray.random()
)
}
}
上面的WidgetData.java文件bean对象如下:
package com.gwm.widget.bean;
/**
* @author xiayiye5
* @date 2023/4/6 9:58
*/
public class WidgetData {
private String title;
private String name;
private String des;
private String body;
public WidgetData(String title, String name, String des, String body) {
this.title = title;
this.name = name;
this.des = des;
this.body = body;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
this.title = title;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getDes() {
return des;
}
public void setDes(String des) {
this.des = des;
}
public String getBody() {
return body;
}
public void setBody(String body) {
this.body = body;
}
}
在广播中我们使用useCase.request()方法模拟请求数据,拿到数据后设置给LiveData,同时在广播页面监听LiveData,数据获取成功后我们使用WidgetUtils.updateWidget(it, widgetData)方法更新小部件上面的数据。
WidgetUtils.kt文件
package com.gwm.widget.utils
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.util.Log
import android.widget.RemoteViews
import androidx.annotation.IdRes
import androidx.lifecycle.MutableLiveData
import com.gwm.widget.R
import com.gwm.widget.WidgetBroadcast
import com.gwm.widget.bean.WidgetData
import com.gwm.widget.widget.HimalayanWidgets
import java.util.*
/**
@author xiayiye5
@date 2023/4/6 9:23
*/
object WidgetUtils {
fun updateWidget(it: Context, widgetData: WidgetData) {
Log.e("WidgetUtils", "收到更新通知了……")
val views = RemoteViews(it.packageName, R.layout.himalayan_widgets)
val instance = AppWidgetManager.getInstance(it)
val appWidgetIds =
instance.getAppWidgetIds(ComponentName(it, HimalayanWidgets::class.java))
views.removeAllViews(R.id.replace_layout)
views.addView(R.id.replace_layout, RemoteViews(it.packageName, R.layout.himalayan_widgets))
views.setTextViewText(R.id.hot, widgetData.title)
setTextColor(views, R.id.hot)
views.setTextViewText(R.id.recommend, widgetData.name)
setTextColor(views, R.id.recommend)
views.setTextViewText(R.id.rink, widgetData.des)
setTextColor(views, R.id.rink)
views.setTextViewText(R.id.news, widgetData.body)
setTextColor(views, R.id.news)
val broadcast = PendingIntent.getBroadcast(
it, 10086,
Intent(it, WidgetBroadcast::class.java),
PendingIntent.FLAG_UPDATE_CURRENT
)
views.setOnClickPendingIntent(R.id.iv_refresh, broadcast)
instance.updateAppWidget(appWidgetIds, views)
//也可以使用下面的方法更新小部件
// instance.updateAppWidget(ComponentName(it, HimalayanWidgets::class.java), views)
}
/**
* 设置文本颜色的通用方法
*/
private fun setTextColor(views: RemoteViews, @IdRes viewId: Int) {
views.setTextColor(
viewId, Color.rgb(
Random().nextInt(255).toFloat(),
Random().nextInt(255).toFloat(),
Random().nextInt(255).toFloat()
)
)
}
val liveData = MutableLiveData<WidgetData>()
}
最后来看下整个项目的结构:
此项目已上传gitee,不过目前权限默认为私有,后面我会上传到GitHub,然后粘贴源码地址,如果紧急需要源码的可以私信我,或者留言邮箱,我会发你一份。
在此谢谢大家。
GitHub源码直达