要在Android开发中创建一个widget,你可以使用Glance,这是由Google推出的一个全新的Jetpack库,专门用于构建简洁、美观且响应式的Android Widget。Glance提供了一个声明式的API来创建小部件,使得开发更加简单和直观。
添加依赖
Groovy
glance-appwidget = { group = "androidx.glance", name = "glance-appwidget", version.ref = "glance" }
清单说明
XML
<receiver
android:name=".receiver.ProgressWidgetReceiver"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/progress_widget_info" />
</receiver>
其中progress_widget_info.xml
XML
?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:minWidth="110dp"
android:minHeight="110dp"
android:updatePeriodMillis="0"
android:initialLayout="@layout/widget_placeholder"
android:resizeMode="horizontal|vertical"
android:widgetCategory="home_screen"/>
widget_placeholder.xml 初始布局
XML
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp">
<!-- Glance 会显示图片,不需要复杂内容 -->
<ImageView
android:id="@+id/widget_image"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop" />
</FrameLayout>
定义receiver
class ProgressWidgetReceiver : GlanceAppWidgetReceiver(){
override val glanceAppWidget = WorkoutPlanProgress2X2()
}
WorkoutPlanProgress2X2
Kotlin
class WorkoutPlanProgress2X2() : GlanceAppWidget() {
//通过prefs获取plan具体信息然后更新界面
override suspend fun provideGlance(
context: Context,
id: GlanceId
) {
provideContent {
val prefs = currentState<Preferences>()
val planJson = prefs[WorkoutPlanPrefs.PLAN_JSON] ?: ""
val plan = if (planJson.isNotEmpty()) {
Gson().fromJson(planJson, Plan::class.java)
} else null
WorkoutPlanProgressCard2X2(plan)
}
}
}
以compose方式些widget,与普通的Android view少许不同
Kotlin
import android.graphics.BitmapFactory
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.glance.GlanceModifier
import androidx.glance.Image
import androidx.glance.ImageProvider
import androidx.glance.LocalContext
import androidx.glance.layout.Alignment
import androidx.glance.layout.Box
import androidx.glance.layout.Column
import androidx.glance.layout.Row
import androidx.glance.layout.Spacer
import androidx.glance.layout.fillMaxWidth
import androidx.glance.layout.height
import androidx.glance.layout.size
import androidx.glance.layout.width
import androidx.glance.text.FontWeight
import androidx.glance.text.Text
import androidx.glance.text.TextStyle
import androidx.glance.unit.ColorProvider
import com.demo.common.network.bean.Plan
import java.io.File
@Composable
fun WorkoutPlanProgressCard2X2(plan: Plan?) {
val context = LocalContext.current
if (plan != null) {
// create your AppWidget here
val cacheFile = File(context.cacheDir, "progress_widget.png")
val imageProvider = if (cacheFile.exists()) {
// 读取 Bitmap,并传给 ImageProvider
val bitmap = BitmapFactory.decodeFile(cacheFile.absolutePath)
ImageProvider(bitmap)
} else {
// 如果没有 bitmap,显示一个占位(Text)
null
}
WorkoutPlanProgressDetailCard2X2(plan, imageProvider)
} else {
WorkoutPlanProgressEmptyCard2X2()
}
}
@Composable
fun WorkoutPlanProgressDetailCard2X2(plan: Plan, imageProvider: ImageProvider?) {
GradientCard {
// 顶部图标 + 标题
Row(verticalAlignment = Alignment.CenterVertically) {
Image(
provider = ImageProvider(com.healthfitness.widget.R.drawable.icon),
contentDescription = null,
modifier = GlanceModifier.size(16.dp)
)
Spacer(GlanceModifier.width(6.dp))
Text(
text = "Workout Plan"
)
}
Spacer(GlanceModifier.height(12.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(contentAlignment = Alignment.Center) {
if (imageProvider != null) {
Image(
provider = imageProvider,
contentDescription = "progress",
modifier = GlanceModifier.size(80.dp)
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = "${(plan.progress ?: 0).toInt()}%",
style = TextStyle(fontSize = 20.sp, fontWeight = FontWeight.Bold)
)
Text(
text = "Progress",
style = TextStyle(
fontSize = 12.sp,
color = ColorProvider(Color(0x4D000000))
)
)
}
}
}
Spacer(GlanceModifier.height(12.dp))
// 下方 remain + missed
Row(
modifier = GlanceModifier.fillMaxWidth(),
) {
Text(
text = "Remain: ${(plan.total - plan.finished).toInt()}",
style = TextStyle(fontSize = 12.sp),
)
Spacer(modifier = GlanceModifier.defaultWeight())
Text(
text = "total: ${plan.total.toInt()}",
style = TextStyle(fontSize = 12.sp)
)
}
}
}
@Composable
fun WorkoutPlanProgressEmptyCard2X2() {
Image(
provider = ImageProvider(com.healthfitness.widget.R.drawable.icon_add),
contentDescription = null,
modifier = GlanceModifier.size(70.dp)
)
}
如何更新widget信息呢
Kotlin
suspend fun updateAllWidgets(context: Context, plan: Plan?) {
val manager = GlanceAppWidgetManager(context)
val glanceIds = manager.getGlanceIds(WorkoutPlanProgress2X2::class.java)
val widget = WorkoutPlanProgress2X2()
val json = plan?.let { Gson().toJson(it) } ?: ""
glanceIds.forEach { id ->
updateAppWidgetState(context, PreferencesGlanceStateDefinition, id) { prefs ->
prefs.toMutablePreferences().apply {
this[WorkoutPlanPrefs.PLAN_JSON] = json
}
}
widget.update(context, id)
}
}
通过updateAppWidgetState将bean通过JSON string传递