別羨慕蘋果的小部件了,安卓也有!
大家好,我是皇叔,最近開了一個安卓進階漲薪訓練營,可以幫助大家突破技術(shù)&職場瓶頸,從而度過難關(guān),進入心儀的公司。
詳情見文章:皇叔的最新作來啦!
作者:Zhujiang
https://juejin.cn/user/3913917127985240
來龍去脈


安卓小部件之痛
Android 12小部件
用戶可重新設(shè)置原有小部件

<appwidget-provider?xmlns:android="http://schemas.android.com/apk/res/android"
????android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity"
????android:widgetFeatures="reconfigurable"?
????...?/>
小部件的尺寸限制
maxResizeWidth:定義用戶所能夠調(diào)整的小部件尺寸的最大寬度 maxResizeHeight:定義用戶所能夠調(diào)整的小部件尺寸的最大高度 targetCellWidth:定義設(shè)備主屏幕上的小部件默認寬度所占格數(shù)(即使不同型號的手機中也會占定義好的格數(shù),但手機系統(tǒng)版本必須在 Android 12 及以上) targetCellHeight:定義設(shè)備主屏幕上的小部件默認高度所占格數(shù)
新的小部件控件
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 - 指定元數(shù)據(jù)名稱。必須使用 android.appwidget.provider 將數(shù)據(jù)標識為 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 :指定小部件默認情況下占用的最小空間。 注意:為使小部件能夠在設(shè)備間移植,小部件的最小大小不得超過 4 x 4 單元格。 minResizeWidth和minResizeHeight:指定小部件的絕對最小大小。 updatePeriodMillis:定義小部件框架通過調(diào)用 onUpdate() 回調(diào)方法來從 AppWidgetProvider 請求更新的頻率應(yīng)該是多大。 initialLayout:指向用于定義小部件布局的布局資源。 configure:定義要在用戶添加小部件時啟動以便用戶配置小部件屬性的 Activity。 previewImage:指定預覽來描繪小部件經(jīng)過配置后是什么樣子的,用戶在選擇小部件時會看到該預覽。 autoAdvanceViewId :指定應(yīng)由小部件的托管應(yīng)用自動跳轉(zhuǎn)的小部件子視圖的視圖 ID。 resizeMode :指定可以按什么規(guī)則來調(diào)整微件的大小,可選值為“horizontal|vertical”,一般默認設(shè)置橫豎都可以進行調(diào)整。 minResizeHeight :指定可將微件大小調(diào)整到的最小高度。 minResizeWidth:指定可將微件大小調(diào)整到的最小寬度。 widgetCategory:聲明小部件是否可以顯示在主屏幕 (home_screen) 或鎖定屏幕 (keyguard) 上。只有低于 5.0 的 Android 版本才支持鎖定屏幕微件。對于 Android 5.0 及更高版本,只有 home_screen 有效,所以現(xiàn)在將這個值寫為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)
????????//?設(shè)置布局
????????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?{
????????????//?設(shè)置?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
????????????)
????????}
????????//?設(shè)置點擊事件的模版
????????views.setPendingIntentTemplate(R.id.stack_view,?toastPendingIntent)
????????appWidgetManager.updateAppWidget(appWidgetId,?views)
????}
}
RemoteViewsService實現(xiàn)
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)
????????????)
????????????//?設(shè)置點擊事件
????????????val?fillInIntent?=?Intent().apply?{
????????????????putExtra(EXTRA_ITEM,?weather.time)
????????????}
????????????setOnClickFillInIntent(R.id.widget_ll_item,?fillInIntent)
????????}
????}
????override?fun?getLoadingView():?RemoteViews?{
????????//?加載數(shù)據(jù)時的布局
????????return?RemoteViews(context.packageName,?R.layout.weather_widget_loading)
????}
}
設(shè)置配置Activity
@AndroidEntryPoint
class?WeatherWidgetConfigureActivity?:?BaseActivity()?{
????private?val?viewModel?by?viewModels()
????public?override?fun?onCreate(savedInstanceState:?Bundle?)?{
????????super.onCreate(savedInstanceState)
????????//?刷新城市數(shù)據(jù)
????????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)
????//?計算小部件的占的格數(shù)
????val?rows:?Int?=?getCellsForSize(minHeight)
????val?columns:?Int?=?getCellsForSize(minWidth)
????XLog.e("rows:$rows???columns:$columns")
????updateAppWidget(context,?appWidgetManager,?appWidgetId,?rows,?columns)
}
/**
?*?返回給定大小的小部件所需的單元格數(shù)。
?*
?*?@param?size 以 dp 為單位的小部件大小。
?*?@return?單元格數(shù)量的大小。
?*/
fun?getCellsForSize(size:?Int):?Int?{
????var?n?=?2
????while?(70?*?n?-?30?????????++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 數(shù)據(jù)刷新問題
/**
?*?獲取之后一周的天氣
?*
?*?@param?context?/
?*?@param?cityInfo?需要獲取天氣的城市
?*?@param?onSuccessListener?獲取成功的回調(diào)
?*/
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)
)
為了失聯(lián),歡迎關(guān)注我防備的小號
評論
圖片
表情



