来龙去脉
小部件兴起
2020年九月苹果的 iOS 14 正式版本发布,其中的一项重大更新就是苹果也支持小部件了!不容易啊,安卓好多年前拥有的功能现如今苹果终于用上了,先来看看苹果中的小部件样式吧!
天气地图等 | 时钟提醒等 |
---|---|
苹果的小部件的确不错,还挺好看,但是安卓的其实也不差,前段时间写了一个完全用 Compose
写的天气应用:
从零到一写一个完整的 Compose 版本的天气,想着苹果的天气小部件挺好用,给安卓也整一个吧!就有了今天的文章,来看看今天实现的最终效果吧:
今天实现的样式 | 可以上下滑动来查看一周的天气哦 |
---|---|
是不是也很炫酷,哈哈哈!这就是基于之前编写的天气应用写的(文末也有Github地址)。
虽然安卓在很多年前就有了小部件,但小部件在安卓手机里的使用并不多,甚至可能说很少,最多也就是手机出厂的时候自带的时间小部件。。。其实很多咱们常用应用都有很多小部件,由于使用的确实不多,所以存在感很低(顺带吐槽下,常用的软件都太流氓了,每个应用都有一堆功能一样的小部件,比如:抖音有好几个、头条也有好几个、爱奇艺、优酷等就不说了。。。)
为什么安卓中的小部件很少人使用呢?主要还是样式太丑,还有就是像上面说的那样太流氓就不想用。Google 其实都快把小部件给忘记了,但去年让苹果给提了下醒,想起了安卓中还有小部件这个东西呢,于是痛定思痛,将小部件做了一些大的更新及升级。
安卓小部件之痛
其实不光使用者不喜欢用安卓的小部件,开发者也不想开发小部件,这是为什么呢?由于小部件是依附在桌面上的,所以并不属于原本应用的进程,而如果想要跨进程修改布局的话就需要使用到 RemoteViews
,但 RemoteViews
不能说是难用,那是相当难用,不仅不能使用自定义 View,连咱们常用的 RecyclerView
等控件都不能使用,只能使用官方固定的几种控件,
可以支持以下布局类:
FrameLayout
、 LinearLayout
、 RelativeLayout
、GridLayout
以及以下控件:AnalogClock
(模拟时钟)、Button
、Chronometer
、ImageButton
、ImageView
、ProgressBar
、TextView
、ViewFlipper
、ListView
、GridView
、StackView
、AdapterViewFlipper
注:这块的控件指的是 Android 12之前的,Android 12中新增了一些新的控件,在下面的部分中会有介绍。
扯皮就先扯到这里吧,开始干活吧!
Android 12 中小部件的更新
刚才也说过,Google 这次在 Android 12中对小部件更新很大,这块来说下吧!
用户可重新设置原有小部件
在之前,用户如果想要重新设置小部件的话只能删除了再重新添加,但是在 Android 12 中,用户将无需通过删除和重新添加 widget 来调整这些原有设定。
设置方法其实很简单,只需要添加一行配置:
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
android:configure="com.zj.weather.common.widget.WeatherWidgetConfigureActivity"
android:widgetFeatures="reconfigurable"
... />
上面配置有两个,widgetFeatures 就是 Android 12中新增的可重新设置小部件的配置项,另外一个是配置小部件的 Activity,想要使 widgetFeatures 起作用的话必须要配置 Activity,这很好理解,如果都不知道去哪配置小部件何谈重新设置呢!
小部件的尺寸限制
在 Android 12之前,Android 中的小部件大小其实特别混乱,每个应用在小部件中标柱的大小基本都是错的,比如应用写的大小是 4 * 1 ,当你将页面布局调整之后应用大小就有可能发生变化,就不再是 4 * 1 的大小了。
Google 有可能也知道这种情况,所以在 Android 12 中增加了小部件的尺寸限制,除了现有的 minWidth
、minHeigh
、minResizeWidth
以及 minResizeHeight
以外,还新增了新的 maxResizeWidth
、 maxResizeHeight
、 targetCellWidth
和 targetCellHeight
属性,下面来具体说下新增的几个属性的含义。
- maxResizeWidth:定义用户所能够调整的小部件尺寸的最大宽度
- maxResizeHeight:定义用户所能够调整的小部件尺寸的最大高度
- targetCellWidth:定义设备主屏幕上的小部件默认宽度所占格数(即使不同型号的手机中也会占定义好的格数,但手机系统版本必须在 Android 12 及以上)
- targetCellHeight:定义设备主屏幕上的小部件默认高度所占格数
如果之前有 targetCellWidth
和 targetCellHeight
属性的话,小部件也不至于像现在这么乱而导致用户不想使用。
新的小部件控件
Android 12 使用以下现有控件新增了对有状态行为的支持:
上面这几个控件大家应该非常熟悉了,但在 Android 12 之前在小部件中想要使用的话也是不可能的。
小部件UI更新
这块其实大家应该都看过了,就一带而过吧,就是为小部件默认添加了一个圆角,可以通过 system_app_widget_background_radius
和 system_app_widget_inner_radius
系统参数来设置微件圆角的半径。
这里来放一张官方文档中的图吧。
干活了干活了
上面叨叨了这么多,先是介绍了下小部件的前世今生,然后又说了下 Android 12中的更新内容,终于要准备干活了。
编写配置文件
在清单中声明小部件
如果想要在 Android 中添加一个小部件的话首先应该在 AndroidManifest.xml 中进行声明,因为小部件实际上也是一个 BroadcastReceiver,大家都知道四大组件想要使用的话都需要在 AndroidManifest.xml 中进行声明,所以咱们先来在清单中声明小部件。
<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>
<receiver>
元素需要 android:name
属性,该属性指定小部件使用的 AppWidgetProvider
(AppWidgetProvider的父类就是BroadcastReceiver)。
<intent-filter>
中的 <action>
元素指定小部件接受 ACTION_APPWIDGET_UPDATE
广播。这是必须明确声明的唯一一项广播,用以接收小部件的增删改等信息。
<meta-data>
元素指定小部件的资源,并且需要以下属性:
android:name
- 指定元数据名称。必须使用android.appwidget.provider
将数据标识为AppWidgetProviderInfo
描述符。android:resource
- 指定AppWidgetProviderInfo
资源位置。
编写小部件的配置文件
上面在清单文件中声明了小部件,下面来编写下小部件的配置文件,根据上面的代码可以看到这个配置文件放在了 xml 文件下,具体路径为:res -> xml,如果本地没有这个文件夹的话创建一个就好。
<?xml version="1.0" encoding="utf-8"?>
<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" />
可以看到这里已经使用到了上面讲的 Android 12中的新的配置,并且设置了最小的宽高,还有预览图片等等,下面来详细看下每一项配置都是干啥的吧。
-
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
即可。
编写布局
根布局
配置文件写好了来编写下布局吧,来考虑下布局应该怎么写,通过文章开头的图可以知道这是一个 StackView
,那就先来写下根布局吧。
<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>
子布局
可以看到布局很简单,只放了一个 StackView
,它继承自 AdapterViewAnimator
,同 ListView
和 GridView
一样,StackView
也需要子布局,那就来吧。
<?xml version="1.0" encoding="utf-8"?>
<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>
由于篇幅原因将布局给简化了下,详细布局可以看文末提供的项目源码。
包含集合小部件的清单
由于咱们的布局中有 StackView
,包含集合的小部件除了上面中列出的要求之外,要使包含集合的小部件能够绑定到 RemoteViewsService
,还必须在清单文件中使用 BIND_REMOTEVIEWS
权限来声明该服务。这样可防止其他应用自由访问小部件的数据。
<service
android:name=".common.widget.WeatherWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
包含集合小部件的 AppWidgetProvider 类
与常规小部件一样,AppWidgetProvider
子类中的大部分代码通常都在 onUpdate()
中。在创建包含集合的小部件时,必须调用 setRemoteAdapter()
来设置适配器,这样将告知集合视图要从何处获取其数据。然后,RemoteViewsService
可以返回 RemoteViewsFactory
实现,并且微件可以提供适当的数据。当调用此方法时,必须传递指向 RemoteViewsService
实现的 Intent,以及指定要更新的小部件的小部件 ID,来看看具体实现吧。
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)
}
}
可以看到 WeatherWidgetService
继承自 RemoteViewsService
,并自己实现了 WeatherRemoteViewsFactory
。
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)
}
}
上面编写了 RemoteViewsFactory
的实现,省略了一些不重要的方法,大家可以去源码中进行查看。
设置配置Activity
配置 Activity
在上面咱们已经说过如何添加到小部件的配置文件中,剩下的就和普通的 Activity
一样了。
由于小部件不支持 Compose
,所以上面咱们都是编写的 Layout
,但是在 Activity
中就可以使用 Compose
了!
@AndroidEntryPoint
class WeatherWidgetConfigureActivity : BaseActivity() {
private val viewModel by viewModels<CityListViewModel>()
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)
}
}
}
}
}
这样 Layout
布局咱们就不需要编写了,下面来看下 ConfigureWidget
的实现吧。
@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)
)
}
}
}
}
看着代码多,其实布局很简单,一个线性布局包裹着标题、城市ViewPager、确定和取消按钮,然后通过高阶函数的方式将确定按钮的点击事件回调出去。
遇到的坑
OK,到这里本篇文章基本就算结束了,上面的这些一般在别的博客中都能搜到,但是重点来了,有很多东西网上是搜不到的,包括在官方文档中写的也是很笼统,并没有实际的应用案例,下面就来详细说一说吧。
布局适配问题
在苹果中小部件的布局在添加的时候就固定好了,后面是不可以进行修改的,想要修改的话只能是删除掉然后重新进行添加,但是在安卓中小部件的大小是可以进行拉伸的,长按即可进行宽高的调整,所以就难免出现布局适配的问题。
Android 12 之前的解决方案
在 Android 12 之前如果想适配不同宽高下显示不同布局的话需要重写下 onAppWidgetOptionsChanged()
方法,然后从中获取到当前小部件的最小宽高,根据宽高的不同就可以进行布局适配了。
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)
}
上面代码中提到了一个 getCellsForSize()
方法,这个方法是根据官方文档中写的计算小部件格数的方法进行定义的,来看下吧:
/**
* 返回给定大小的小部件所需的单元格数。
*
* @param size 以 dp 为单位的小部件大小。
* @return 单元格数量的大小。
*/
fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
注意!!! 这里所计算出的单元格数量不一定是正确的,在有的手机上可能没问题,但一些手机上就有可能出问题,大家一定要注意,这也是没办法的事,手机厂商太多了,每个桌面的实现方式也略有不同,这事是正常的。
Android 12 之后的解决方案
在 Android 12 之后,可以通过响应式布局来进行适配,首先需要创建一组不同尺寸的布局,然后调用 updateAppWidget()
函数,并传入一组布局,当小部件尺寸发生变化时,系统会自动更改布局。
val viewMapping = mapOf(
SizeF(150f, 110f) to RemoteViews(
context.packageName,
布局
),
SizeF(250f, 110f) to RemoteViews(
context.packageName,
布局
),
)
// 指示小部件管理器更新小部件
appWidgetManager.updateAppWidget(appWidgetId, RemoteViews(viewMapping))
这样确实会简单一些,相当于是 RemoteViews
内部为我们做了处理,无需再重写 onAppWidgetOptionsChanged()
方法了,但这样的话只能在 Android 12 及之后的版本中进行使用,大家根据需求来使用吧。
StackView 数据刷新问题
这个问题是真的挺恶心,也有可能是我水平有限,官方给出的刷新是 notifyAppWidgetViewDataChanged()
方法,这块搞的时候差点给我搞疯。。。
也是我自己的问题,人家都告诉刷新的流程了还写的有问题。
我之前是将天气的数据请求放在 onCreate
方法中,然后通过 runBlocking()
方法将异步转为同步,获取到数据再执行下一步,但这样的话就会 anr。。
然后我又写了一个高阶函数:
/**
* 获取之后一周的天气
*
* @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")
}
}
这就可以了,再来放下官方的流程图吧。
桌面图片显示圆角
这块是为了展示天气背景而出的问题,小部件中不支持自定义 View,所以就只能通过图片本身了,需要将图片加上圆角,这很简单,网上一搜一大堆,但我设置完了之后并不是我想要的效果,我想要的是宽高一样,这也简单,加一行配置就行:
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
}
这样设置完再切圆角就没问题了,最后再将图片设置到 ImageView 中。
setImageViewBitmap(
R.id.widget_iv_bg,
fillet(context = context, bitmap = zoomImg(context, weather.icon), roundDp = 10)
)
打完收工
大家可以购买我的新书《Jetpack Compose:Android全新UI编程》进行阅读,里面有完整的 Compose 框架供大家学习。
呸呸呸,太不要脸了,又在推荐自己的新书。。。
说了这么多还没放 Github 地址呢:github.com/zhujiang521…
如果你在学习或者想要学习关于 Compose 的话,亦或是想学习安卓的小部件,这个项目应该或多或少会对你有点帮助,如果对你有帮助的话,别忘记点个 Star,感激不尽。
其实还有一些细节的东西我没有说到,大家如果有疑问的话可以在评论区提出来。
先写到这里吧,再会!