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

来龙去脉

小部件兴起

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

天气地图等 时钟提醒等
image.png image.png

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

今天实现的样式 可以上下滑动来查看一周的天气哦
image.png image.png
添加小部件.gif 小部件效果.gif

是不是也很炫酷,哈哈哈!这就是基于之前编写的天气应用写的(文末也有Github地址)。

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

为什么安卓中的小部件很少人使用呢?主要还是样式太丑,还有就是像上面说的那样太流氓就不想用。Google 其实都快把小部件给忘记了,但去年让苹果给提了下醒,想起了安卓中还有小部件这个东西呢,于是痛定思痛,将小部件做了一些大的更新及升级。

安卓小部件之痛

其实不光使用者不喜欢用安卓的小部件,开发者也不想开发小部件,这是为什么呢?由于小部件是依附在桌面上的,所以并不属于原本应用的进程,而如果想要跨进程修改布局的话就需要使用到 RemoteViews ,但 RemoteViews 不能说是难用,那是相当难用,不仅不能使用自定义 View,连咱们常用的 RecyclerView 等控件都不能使用,只能使用官方固定的几种控件,

可以支持以下布局类:

FrameLayoutLinearLayoutRelativeLayoutGridLayout

以及以下控件:AnalogClock(模拟时钟)、ButtonChronometerImageButtonImageViewProgressBarTextViewViewFlipperListViewGridViewStackViewAdapterViewFlipper

注:这块的控件指的是 Android 12之前的,Android 12中新增了一些新的控件,在下面的部分中会有介绍。

扯皮就先扯到这里吧,开始干活吧!

Android 12 中小部件的更新

刚才也说过,Google 这次在 Android 12中对小部件更新很大,这块来说下吧!

用户可重新设置原有小部件

在之前,用户如果想要重新设置小部件的话只能删除了再重新添加,但是在 Android 12 中,用户将无需通过删除和重新添加 widget 来调整这些原有设定。

image.png

设置方法其实很简单,只需要添加一行配置:

<?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 中增加了小部件的尺寸限制,除了现有的 minWidthminHeighminResizeWidth 以及 minResizeHeight 以外,还新增了新的 maxResizeWidthmaxResizeHeighttargetCellWidthtargetCellHeight 属性,下面来具体说下新增的几个属性的含义。

  • maxResizeWidth:定义用户所能够调整的小部件尺寸的最大宽度
  • maxResizeHeight:定义用户所能够调整的小部件尺寸的最大高度
  • targetCellWidth:定义设备主屏幕上的小部件默认宽度所占格数(即使不同型号的手机中也会占定义好的格数,但手机系统版本必须在 Android 12 及以上)
  • targetCellHeight:定义设备主屏幕上的小部件默认高度所占格数

如果之前有 targetCellWidthtargetCellHeight 属性的话,小部件也不至于像现在这么乱而导致用户不想使用。

新的小部件控件

Android 12 使用以下现有控件新增了对有状态行为的支持:

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

小部件UI更新

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

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

image.png

干活了干活了

上面叨叨了这么多,先是介绍了下小部件的前世今生,然后又说了下 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 ,同 ListViewGridView 一样,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")
    }
}
复制代码

这就可以了,再来放下官方的流程图吧。

image.png

桌面图片显示圆角

这块是为了展示天气背景而出的问题,小部件中不支持自定义 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,感激不尽。

其实还有一些细节的东西我没有说到,大家如果有疑问的话可以在评论区提出来。

先写到这里吧,再会!

猜你喜欢

转载自juejin.im/post/7037303315595526157