别羡慕苹果的小部件了,安卓也有!
大家好,我是皇叔,最近开了一个安卓进阶涨薪训练营,可以帮助大家突破技术&职场瓶颈,从而度过难关,进入心仪的公司。
详情见文章:皇叔的最新作来啦!
作者:Zhujiang
https://juejin.cn/user/3913917127985240
来龙去脉
安卓小部件之痛
Android 12小部件
用户可重新设置原有小部件
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity"
android:widgetFeatures="reconfigurable"
... />
小部件的尺寸限制
maxResizeWidth:定义用户所能够调整的小部件尺寸的最大宽度 maxResizeHeight:定义用户所能够调整的小部件尺寸的最大高度 targetCellWidth:定义设备主屏幕上的小部件默认宽度所占格数(即使不同型号的手机中也会占定义好的格数,但手机系统版本必须在 Android 12 及以上) targetCellHeight:定义设备主屏幕上的小部件默认高度所占格数
新的小部件控件
CheckBox Switch RadioButton
小部件UI更新
干活了干活了
编写配置文件
在清单中声明小部件
<receiver
android:name=".common.widget.WeatherWidget"
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/weather_widget_info" />
receiver>
android:name - 指定元数据名称。必须使用 android.appwidget.provider 将数据标识为 AppWidgetProviderInfo 描述符。 android:resource - 指定 AppWidgetProviderInfo 资源位置。
编写小部件的配置文件
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity"
android:initialKeyguardLayout="@layout/weather_widget"
android:initialLayout="@layout/weather_widget"
android:minWidth="170dp"
android:minHeight="90dp"
android:previewImage="@mipmap/weather_widget"
android:resizeMode="horizontal|vertical"
android:targetCellWidth="3"
android:targetCellHeight="2"
android:updatePeriodMillis="86400000"
android:widgetCategory="home_screen"
android:widgetFeatures="reconfigurable" />
minWidth 和 minHeight :指定小部件默认情况下占用的最小空间。 注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。 minResizeWidth和minResizeHeight:指定小部件的绝对最小大小。 updatePeriodMillis:定义小部件框架通过调用 onUpdate() 回调方法来从 AppWidgetProvider 请求更新的频率应该是多大。 initialLayout:指向用于定义小部件布局的布局资源。 configure:定义要在用户添加小部件时启动以便用户配置小部件属性的 Activity。 previewImage:指定预览来描绘小部件经过配置后是什么样子的,用户在选择小部件时会看到该预览。 autoAdvanceViewId :指定应由小部件的托管应用自动跳转的小部件子视图的视图 ID。 resizeMode :指定可以按什么规则来调整微件的大小,可选值为“horizontal|vertical”,一般默认设置横竖都可以进行调整。 minResizeHeight :指定可将微件大小调整到的最小高度。 minResizeWidth:指定可将微件大小调整到的最小宽度。 widgetCategory:声明小部件是否可以显示在主屏幕 (home_screen) 或锁定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持锁定屏幕微件。对于 Android 5.0 及更高版本,只有 home_screen 有效,所以现在将这个值写为home_screen即可。
编写布局
根布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@android:id/background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#00000000"
android:theme="@style/Theme.Design.NoActionBar">
<StackView
android:id="@+id/stack_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:loopViews="true" />
FrameLayout>
子布局
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_ll_item">
<ImageView
android:id="@+id/widget_iv_bg"/>
<LinearLayout>
<TextView
android:id="@+id/widget_tv_city" />
<TextView
android:id="@+id/widget_tv_date"/>
<ImageView
android:id="@+id/widget_iv_icon" />
<ImageView
android:id="@+id/widget_iv_small_icon" />
<TextView
android:id="@+id/widget_tv_temp" />
LinearLayout>
FrameLayout>
包含集合小部件的清单
<service
android:name=".common.widget.WeatherWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
包含集合小部件的 AppWidgetProvider 类
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
appWidgetIds.forEach { appWidgetId->
updateAppWidget(context, appWidgetManager, appWidgetId)
val cityInfo = loadTitlePref(context, appWidgetId)
// 设置布局
val views = RemoteViews(context.packageName, R.layout.weather_widget)
val intent = Intent(context, WeatherWidgetService::class.java).apply {
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
}
views.apply {
// 设置 StackView 适配器
setRemoteAdapter(R.id.stack_view, intent)
setEmptyView(R.id.stack_view, R.id.empty_view)
}
val toastPendingIntent: PendingIntent = Intent(
context,
WeatherWidget::class.java
).run {
action = CLICK_ITEM_ACTION
putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
data = Uri.parse(toUri(Intent.URI_INTENT_SCHEME))
PendingIntent.getBroadcast(
context,
0,
this,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)
}
// 设置点击事件的模版
views.setPendingIntentTemplate(R.id.stack_view, toastPendingIntent)
appWidgetManager.updateAppWidget(appWidgetId, views)
}
}
RemoteViewsService实现
class WeatherWidgetService : RemoteViewsService() {
override fun onGetViewFactory(intent: Intent): RemoteViewsFactory {
return WeatherRemoteViewsFactory(this.applicationContext, intent)
}
}
class WeatherRemoteViewsFactory(private val context: Context, intent: Intent) :
RemoteViewsService.RemoteViewsFactory, CoroutineScope by MainScope() {
private var cityInfo: CityInfo? = null
init {
intent.getStringExtra(CITY_INFO)?.apply {
cityInfo = Gson().fromJson(this, CityInfo::class.java)
}
}
override fun getViewAt(position: Int): RemoteViews {
if (widgetItems.size != WEEK_COUNT) {
return RemoteViews(context.packageName, R.layout.weather_widget_loading)
}
return RemoteViews(context.packageName, R.layout.widget_item).apply {
val weather = widgetItems[position]
setTextViewText(R.id.widget_tv_temp, "${weather.min}-${weather.max}℃")
setTextViewText(
R.id.widget_tv_city,
"${cityInfo?.city ?: ""} ${cityInfo?.name ?: "北京"}"
)
setImageViewBitmap(
R.id.widget_iv_bg,
fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10)
)
layoutAdapter(weather.icon)
setTextViewText(R.id.widget_tv_date, weather.time)
setImageViewResource(
R.id.widget_iv_icon,
IconUtils.getWeatherIcon(weather.icon)
)
// 设置点击事件
val fillInIntent = Intent().apply {
putExtra(EXTRA_ITEM, weather.time)
}
setOnClickFillInIntent(R.id.widget_ll_item, fillInIntent)
}
}
override fun getLoadingView(): RemoteViews {
// 加载数据时的布局
return RemoteViews(context.packageName, R.layout.weather_widget_loading)
}
}
设置配置Activity
@AndroidEntryPoint
class WeatherWidgetConfigureActivity : BaseActivity() {
private val viewModel by viewModels()
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 刷新城市数据
viewModel.refreshCityList()
setContent {
PlayWeatherTheme {
Surface(color = MaterialTheme.colors.background) {
ConfigureWidget(
viewModel,
onCancelListener = {
setResult(RESULT_CANCELED)
finish()
}) { cityInfo ->
onConfirm(cityInfo)
}
}
}
}
}
@OptIn(ExperimentalPagerApi::class)
@Composable
private fun ConfigureWidget(
viewModel: CityListViewModel,
onCancelListener: () -> Unit,
onConfirmListener: (CityInfo) -> Unit
) {
val cityList by viewModel.cityInfoList.observeAsState(arrayListOf())
val buttonHeight = 45.dp
val pagerState = rememberPagerState()
Column(modifier = Modifier.fillMaxSize()) {
Spacer(modifier = Modifier.height(80.dp))
Text(
text = "小部件城市选择",
modifier = Modifier.fillMaxWidth(),
textAlign = TextAlign.Center,
fontSize = 26.sp,
color = Color(red = 53, green = 128, blue = 186)
)
Box(modifier = Modifier.weight(1f)) {
HorizontalPager(
state = pagerState,
count = cityList.size,
modifier = Modifier.fillMaxSize()
) { page ->
Card(
shape = RoundedCornerShape(10.dp),
backgroundColor = MaterialTheme.colors.onSecondary,
modifier = Modifier.size(300.dp)
) {
val cityInfo = cityList[page]
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = cityInfo.name, fontSize = 30.sp)
}
}
}
DrawIndicator(pagerState = pagerState)
}
Spacer(modifier = Modifier.height(50.dp))
Divider(
modifier = Modifier
.fillMaxWidth()
.height(1.dp)
)
Row {
TextButton(
modifier = Modifier
.weight(1f)
.height(buttonHeight),
onClick = {
onCancelListener()
}
) {
Text(
text = stringResource(id = R.string.city_dialog_cancel),
fontSize = 16.sp,
color = Color(red = 53, green = 128, blue = 186)
)
}
Divider(
modifier = Modifier
.width(1.dp)
.height(buttonHeight)
)
TextButton(
modifier = Modifier
.weight(1f)
.height(buttonHeight),
onClick = {
onConfirmListener(cityList[pagerState.currentPage])
}
) {
Text(
text = stringResource(id = R.string.city_dialog_confirm),
fontSize = 16.sp,
color = Color(red = 53, green = 128, blue = 186)
)
}
}
}
}
遇到的坑
布局适配问题
Android 12 之前的解决方案
override fun onAppWidgetOptionsChanged(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetId: Int,
newOptions: Bundle
) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions)
// See the dimensions and
val options = appWidgetManager.getAppWidgetOptions(appWidgetId)
// 获取小部件最小的宽高
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val minHeight = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT)
// 计算小部件的占的格数
val rows: Int = getCellsForSize(minHeight)
val columns: Int = getCellsForSize(minWidth)
XLog.e("rows:$rows columns:$columns")
updateAppWidget(context, appWidgetManager, appWidgetId, rows, columns)
}
/**
* 返回给定大小的小部件所需的单元格数。
*
* @param size 以 dp 为单位的小部件大小。
* @return 单元格数量的大小。
*/
fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
Android 12 之后的解决方案
val viewMapping = mapOf(
SizeF(150f, 110f) to RemoteViews(
context.packageName,
布局
),
SizeF(250f, 110f) to RemoteViews(
context.packageName,
布局
),
)
// 指示小部件管理器更新小部件
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
StackView 数据刷新问题
/**
* 获取之后一周的天气
*
* @param context /
* @param cityInfo 需要获取天气的城市
* @param onSuccessListener 获取成功的回调
*/
fun getWeather7Day(
context: Context,
cityInfo: CityInfo?,
onSuccessListener: (MutableList<WeekWeather>) -> kotlin.Unit
) {
QWeather.getWeather7D(context, getLocation(cityInfo = cityInfo),
getDefaultLocale(context), Unit.METRIC,
object : QWeather.OnResultWeatherDailyListener {
override fun onError(e: Throwable) {
XLog.e("getWeather7Day1 onError: $e")
showToast(context, e.message)
}
override fun onSuccess(weatherDailyBean: WeatherDailyBean?) {
onSuccessListener(weatherDailyBean.daily)
}
})
}
private fun notifyWeatherWidget(
context: Context,
appWidgetId: Int
) {
WeatherWidgetUtils.getWeather7Day(context = context, cityInfo = cityInfo) { items ->
// 赋值
widgetItems = items
val mgr = AppWidgetManager.getInstance(context)
// 刷新
mgr.notifyAppWidgetViewDataChanged(
appWidgetId,
R.id.stack_view
)
XLog.e(TAG, "init: $widgetItems")
}
}
桌面图片显示圆角
android:scaleType="centerCrop"
/**
* 将普通Bitmap按照centerCrop的方式进行截取
*/
fun zoomImg(bm: Bitmap): Bitmap {
val w = bm.width // 得到图片的宽,高
val h = bm.height
val retX: Int
val retY: Int
val wh = w.toDouble() / h.toDouble()
val nwh = w.toDouble() / w.toDouble()
if (wh > nwh) {
retX = h * w / w
retY = h
} else {
retX = w
retY = w * w / w
}
val startX = if (w > retX) (w - retX) / 2 else 0 //基于原图,取正方形左上角x坐标
val startY = if (h > retY) (h - retY) / 2 else 0
val bit = Bitmap.createBitmap(bm, startX, startY, retX, retY, null, false)
bm.recycle()
return bit
}
setImageViewBitmap(
R.id.widget_iv_bg,
fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10)
)
为了失联,欢迎关注我防备的小号
评论