/   今日科技快讯   /

12月9日上午美团外卖由骑手App技术原因导致骑手无法接单,于10时46分全面恢复正常。对于在故障中受影响的用户,美团外卖建议可选择自主退款,并将为该部分用户提供10元无门槛红包。在故障中,商家遭受的正常餐损也将由美团外卖承担。此外,美团外卖还将对骑手因故障导致的超时送达、差评等考核问题免责,同时通过红包雨等活动方式对骑手当天的收入进行补偿。

/   作者简介   /

明天就是周六啦,大家好好休息,我们下周一再见!

本篇文章来自Zhujiang的投稿,文章主要讲解了Android小部件实现与适配,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。

Zhujiang的博客地址:

https://juejin.cn/user/3913917127985240

/   来龙去脉   /

2020年九月苹果的 iOS 14 正式版本发布,其中的一项重大更新就是苹果也支持小部件了!不容易啊,安卓好多年前拥有的功能现如今苹果终于用上了,先来看看苹果中的小部件样式吧!

苹果的小部件的确不错,还挺好看,但是安卓的其实也不差,前段时间写了一个完全用 Compose 写的天气应用: 从零到一写一个完整的 Compose 版本的天气(https://juejin.cn/post/7030986229512404999),想着苹果的天气小部件挺好用,给安卓也整一个吧!就有了今天的文章,来看看今天实现的最终效果吧:

虽然安卓在很多年前就有了小部件,但小部件在安卓手机里的使用并不多,甚至可能说很少,最多也就是手机出厂的时候自带的时间小部件。。。其实很多咱们常用应用都有很多小部件,由于使用的确实不多,所以存在感很低(顺带吐槽下,常用的软件都太流氓了,每个应用都有一堆功能一样的小部件,比如:抖音有好几个、头条也有好几个、爱奇艺、优酷等就不说了。。。)

为什么安卓中的小部件很少人使用呢?主要还是样式太丑,还有就是像上面说的那样太流氓就不想用。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 使用以下现有控件新增了对有状态行为的支持:

  • CheckBox

  • Switch

  • RadioButton

上面这几个控件大家应该非常熟悉了,但在 Android 12 之前在小部件中想要使用的话也是不可能的。

小部件UI更新

这块其实大家应该都看过了,就一带而过吧,就是为小部件默认添加了一个圆角,可以通过 system_app_widget_background_radius 和 system_app_widget_inner_radius 系统参数来设置微件圆角的半径。

这里来放一张官方文档中的图吧。

/   干活了干活了   /

上面叨叨了这么多,先是介绍了下小部件的前世今生,然后又说了下 Android 12中的更新内容,终于要准备干活了。

编写配置文件

  • 在清单中声明小部件

如果想要在 Android 中添加一个小部件的话首先应该在 AndroidManifest.xml 中进行声明,因为小部件实际上也是一个 BroadcastReceiver,大家都知道四大组件想要使用的话都需要在 AndroidManifest.xml 中进行声明,所以咱们先来在清单中声明小部件。

<receiverandroid:name=".common.widget.WeatherWidget"android:exported="true" ><intent-filter><action android:name="android.appwidget.action.APPWIDGET_UPDATE" /></intent-filter><meta-dataandroid: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中的新的配置,并且设置了最小的宽高,还有预览图片等等,下面来详细看下每一项配置都是干啥的吧。

  • minWidthminHeight :指定小部件默认情况下占用的最小空间。

    注意:为使小部件能够在设备间移植,小部件的最小大小不得超过 4 x 4 单元格。

  • minResizeWidthminResizeHeight:指定小部件的绝对最小大小。

  • 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"><StackViewandroid: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"><ImageViewandroid:id="@+id/widget_iv_bg"/><LinearLayout><TextViewandroid:id="@+id/widget_tv_city" /><TextViewandroid:id="@+id/widget_tv_date"/><ImageViewandroid:id="@+id/widget_iv_icon" /><ImageViewandroid:id="@+id/widget_iv_small_icon" /><TextViewandroid:id="@+id/widget_tv_temp" /></LinearLayout></FrameLayout>

由于篇幅原因将布局给简化了下,详细布局可以看文末提供的项目源码。

包含集合小部件的清单

由于咱们的布局中有 StackView ,包含集合的小部件除了上面中列出的要求之外,要使包含集合的小部件能够绑定到 RemoteViewsService,还必须在清单文件中使用 BIND_REMOTEVIEWS 权限来声明该服务。这样可防止其他应用自由访问小部件的数据。

<serviceandroid: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_ACTIONputExtra(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? = nullinit {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.dpval 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 andval 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 = 2while (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 = itemsval 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.heightval retX: Intval retY: Intval wh = w.toDouble() / h.toDouble()val nwh = w.toDouble() / w.toDouble()if (wh > nwh) {retX = h * w / wretY = h} else {retX = wretY = w * w / w}val startX = if (w > retX) (w - retX) / 2 else 0 //基于原图,取正方形左上角x坐标val startY = if (h > retY) (h - retY) / 2 else 0val 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)
)

github地址:https://github.com/zhujiang521/PlayWeather

推荐阅读:

我的新书,《第一行代码 第3版》已出版!

探究APP换肤机制的设计与实现

你知道Edge这种项目是如何进行版本管理的吗?

欢迎关注我的公众号

学习技术或投稿

长按上图,识别图中二维码即可关注

别羡慕苹果的小部件了,安卓也有!相关推荐

  1. 苹果6s最大屏幕尺寸_羡慕苹果3DTouch好用?安卓这个功能不比它差!

    作为从iPhone 6s时代加入的功能,3D touch在如今的苹果手机里存在感似乎不是特别大.但有些时候,你要是用不到它还真的挺不习惯的--尤其是像一些应用使用快捷功能的时候. 从iPhone 6s ...

  2. 苹果隐藏app_初探iOS 14主屏幕体验:小部件、App Library、隐藏应用等

    在即将到来的 iOS 14 系统中,苹果对主屏幕进行了最近几年最大规模的重新设计.除了终于引入各种小部件之外,还提供了能隐藏应用程序的 App Library .外媒 MacRumors 在最新文章中 ...

  3. Android 12 小部件详解

    来龙去脉 小部件兴起 2020年九月苹果的 iOS 14 正式版本发布,其中的一项重大更新就是苹果也支持小部件了!不容易啊,安卓好多年前拥有的功能现如今苹果终于用上了,先来看看苹果中的小部件样式吧! ...

  4. 手机bootstrap搜索框_这些桌面小部件,Android 手机可不能错过

    在开源.可玩性较高的安卓系统上,小部件呈现了与 iOS 不一样的魅力.它高度的灵活自由,使得安卓手机用户,更能对自己的手机桌面进行个性化的定制. 可在细数当下众多的安卓小部件里,哪些能在保持实用性的同 ...

  5. android 仿苹果 小组件,安卓仿ios14桌面小部件

    安卓仿ios14桌面小部件,是一个可以让安卓手机的界面看起来像苹果界面的软件,功能非常强大,它可以提供多种不同主题的壁纸桌面,随心选择,设置后的效果还是非常不错的,很有高级感,操作简单,上手也很快. ...

  6. android 仿iphone多任务管理效果,别羡慕苹果了,安卓悬浮神器比 iPhone 好用太多!...

    原标题:别羡慕苹果了,安卓悬浮神器比 iPhone 好用太多! 都 2021 年了,你还不知道安卓悬浮球是款神器? 就在不久前,小黑被友人如此嘲讽.作为一名经常接触数码科技的资深搞机党,小黑一直对悬浮 ...

  7. android 仿苹果 小组件,仿ios14桌面小部件

    仿ios14桌面小部件,这是一款面向广大安卓手机用户推出的高仿iOS14桌面插件软件,大家可以使用这款软件快速完成自己想要的桌面显示,多种插件一键点击轻松完成设置过程,让大家体验到同款iOS14桌面强 ...

  8. ios分屏_苹果WWDC前瞻,分屏/息屏显示/桌面小部件,你想要的都有

    ↑ 点击上方"回收老司机"关注我们 苹果一年一度的WWDC全球开发者大会还有1天就到来,WWDC被誉为科技界的春晚,每年都吸引无数开发者参与,苹果也会分享软件最新的发展方向,而每年 ...

  9. 显示农历天气时钟小部件下载_安卓最强桌面小部件:Zooper Widget

    天气.时钟还是其他应用的小部件,对于安卓用户来说,主屏放哪个小部件也是一件让人纠结的事,功能强大的,外观.尺寸不满意,外观好看的功能性又不够强,如何才能拥有一款二者兼顾的小部件呢,来看看可以自定义Wi ...

最新文章

  1. folders默认配置 shell_分布式存储Ceph RBD-Mirror灾备方案(二)镜像模式配置
  2. ubuntu13.04下安装jdk7
  3. python函数赋值函数_python 函数参数赋值过程
  4. Angular TView和LView数据结构
  5. 软件需求规格说明书通用模版_通用需求挑战和机遇
  6. 被限高消费后,王思聪又有新动作:新增对外投资...
  7. 微课|玩转Python轻松过二级(2.4节):常用内置函数用法精要3
  8. 【重点:BFS】LeetCode 407. Trapping Rain Water II
  9. (转)Oracle中通过位图索引提高查询效率
  10. scapy爬虫-Url去重
  11. nvm nodejs npm快速安装
  12. 一 集成电路与IP核技术
  13. eclipse下载哪个版本开发java_官网上有很多版本的eclipse,下载哪个版本比较合适?...
  14. 10-调用门(有参)实验
  15. (linux)idr(integer ID management)机制
  16. 柳传志:想过得富有光鲜就要知道为啥穷
  17. 冬至计算(一千年冬至计算)
  18. Bluehost注册流程与问题
  19. Mysql数据库快速插入亿级数据
  20. 自定义View——幸运转盘

热门文章

  1. 免费css代码下载-Free Css Templates
  2. 芯片测试算法及时间复杂度分析
  3. iOS开发 支付之银联支付集成
  4. 分享20个无版权的高清无码图库站
  5. 声纹识别与声源定位(一)
  6. 12306登录验证码识别(Java版)
  7. OpenGL之建立三维坐标网格
  8. CSDN博客运营团队2022年H2总结
  9. linux查看系统所有变量,Linux系统各指标命令
  10. Android中的Activity