Android怎么实现正方形桌面小部件(APP Widget)?

这个问题可以变成,用有限的View,仅靠XML,你能够绘制正方形吗?
因为受限于RemoteViews的缘故,想要实现正方形的小部件可没那么容易,以至于一些主流应用的APP Widget也不是一个完美的正方形。

我们都知道,RemoteViews是不支持ConstraintLayout的,如果支持的话那就好说了,直接

xml 复制代码
<View
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintDimensionRatio="1" />

就能实现一个正方形的View
有同学会说,实现正方形的View不是很简单吗?

xml 复制代码
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="200dp"
    android:layout_height="200dp"
    android:background="@color/white"/>

让高度等于宽度不就好了~

如果宽度等于高度的话,我们看看实际上展示的效果。

实际上是一个长方形。

为什么是长方形呢?

Android小部件APP Widget开发中介绍过,桌面小部件在桌面所占的空间受minWidthminHeighttargetCellWidthtargetCellHeight共同影响。

因为我们设置的targetCellWidthtargetCellHeight都是2,所以会占用桌面2X2的空间。

但是2X2的空间可能只有100dpX150dp

因为View的大小大于桌面空间的大小,所以最终会被缩小,缩小到能展示得下的宽高。

有同学会说,那如果桌面2X2的空间可能只有100dpX150dp。

那我将代码改成这样就好了。

xml 复制代码
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:background="@color/white">

    <TextView />

</FrameLayout>

如果将宽高都改成100dp会怎么样呢?

从结果可以看到,虽然是正方形,但是宽度却没有充满整个空间,也就是说,我们浪费了一些空间。

那我们慢慢调,调到让宽度充满整个空间为止。

因为Android设备品牌型号特别多,我们将宽度和高度硬编码是绝对不行的,那么有别的什么办法吗?

我们只能先从RemoteViews支持哪些View入手,看哪些是能够让其展示成正方形的。

RemoteViews 支持的布局:

  • AdapterViewFlipper
  • FrameLayout
  • GridLayout
  • GridView
  • LinearLayout
  • ListView
  • RelativeLayout
  • StackView
  • ViewFlipper

支持的控件:

  • AnalogClock
  • Button
  • Chronometer
  • ImageButton
  • ImageView
  • ProgressBar
  • TextClock
  • TextView

Android 12及以上支持的控件:

  • CheckBox
  • RadioButton
  • RadioGroup
  • Switch

一眼看过去,没有一个能用的,正如我上面说所,如果能用ConstraintLayout的话,就没啥事了。

所以只能进入源码,搜索一些关键词看能不能找到线索。

要实现一个宽度填充空间的正方形,也就是说宽度是match_parent,宽高比是11

宽度没有问题,那我们看源码,有没有"ratio"这个词,有的话我们将"ratio"设置成1就可以了。

上面的那些View都没有找到有用的信息,唯独ImageView

java 复制代码
#ImageView

/**
 * 如果您希望ImageView调整其边界以保持drawable宽高比,则将其设置为 true
 */
public void setAdjustViewBounds(boolean adjustViewBounds) {
  ······
}

从注释可以理解,也就是说,我们将ImageViewadjustViewBounds设置true,他就会调整自己的宽高,让他的宽高比跟drawable的宽高保持一致。

那简单,改代码

写一个drawable,宽高比是1比1。

xml 复制代码
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF" />
    <size android:width="10dp" android:height="10dp" />
</shape>

修改我们最开始的代码,改成ImageView

xml 复制代码
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"
    android:adjustViewBounds="true"
    android:background="@color/teal_200"
    android:src="@drawable/square_white" />

运行程序。

很遗憾,并没有变成正方形。。。。

貌似被骗了,回去查看adjustViewBounds的一些逻辑。

看到这些代码的时候我貌似懂了。

java 复制代码
// We are supposed to adjust view bounds to match the aspect
// ratio of our drawable. See if that is possible.
if (mAdjustViewBounds) {
    resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
    resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;

    desiredAspect = (float) w / (float) h;
}

从代码中能看出,只有SpecMode不是EXACTLY的时候,才会调整宽高。

我们现在想调整高度,我们都知道,wrap_content就不是EXACTLY了,所以我们将高度改成wrap_content

改后的代码如下

xml 复制代码
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:adjustViewBounds="true"
    android:background="@color/teal_200"
    android:src="@drawable/square_white" />

运行程序。

很完美,这一个正方形的ImageView就有了,有正方形的ImageView,只要依附于这个ImageView做布局,就可以实现正方形的Widget了。

特殊情况

还没完,比如这种情况。

把屏幕旋转过来,因为宽度大于高度了,所以展示的还是一个长方形。。。

也就说我们不单止要调整高度,宽度也要调整。

那么尝试将宽度改成wrap_content看行不行?

xml 复制代码
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:adjustViewBounds="true"
    android:background="@color/teal_200"
    android:src="@drawable/square_white" />

运行程序

是的你没看错,只剩下中间的一个小白点了

为什么呢?只能回去看看adjustViewBounds的实现逻辑了。

为了方便大家阅读,这里写了一个伪代码。

阅读源码需要耐心,不想看的同学可以跳过,直接看后面的结论。

java 复制代码
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {

    //需要调整宽度
    boolean resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
    //需要调整高度
    boolean resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
    //根据Drawable算出想要的宽高比,我们这个场景应该是1
    float desiredAspect = mDrawableWidth / mDrawableHeight;
    //如果需要调整宽度或者高度
    if (resizeWidth || resizeHeight) {
        //在我们这种场景,会取min(parentSize,drawableSize),
        //1.也就是说drawable的宽度和横向的可用空间,取一个最小值
        int widthSize = resolveAdjustedSize(.....);
        //在我们这种场景,会取min(parentSize,drawableSize),
        //也就是说drawable的高度和纵向的可用空间,取一个最小值
        int heightSize = resolveAdjustedSize(.......);
        //我们这里不会为0
        if (desiredAspect != 0.0f) {
            //算出一个最终实际可能展示的宽高比。
            float actualAspect =  widthSize /heightSize;
          
            //这里可以理解为想要的宽高比跟实际会展示的宽高比不一样
            if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
		//用来标识是否结束
                boolean done = false;
		//如果需要调整宽度
                if (resizeWidth) {
                     //根据宽高比*实际会展示的高度,算出调整后的宽度。
                    int newWidth = desiredAspect * heightSize;
                    
                    //调整后的宽度小于等于上面算的实际会展示的宽度
                    //之所以这么判断是因为不能让调整后的大小比实际会展示的大小还要大,
                    //可以理解为widthSize已经是这个view的最大的宽度了
                    if (newWidth <= widthSize) {
                        //直接用这个调整后的宽度
                        widthSize = newWidth;
                        //这里直接完成是因为调整宽度后已经符合宽高比了,无需调整高度了。
                        done = true;
                    }
                }

                if (!done && resizeHeight) {
                     // 宽度/想要的宽高比
                    int newHeight = widthSize / desiredAspect;
                    //如果调整后高度的小于等于实际会展示的高度
                    if (newHeight <= heightSize) {
                        //使用这个高度
                        heightSize = newHeight;
                    }
                }
            }
        }
    } 
    //设置宽高
    setMeasuredDimension(widthSize, heightSize);
}

从后面的代码可以看出,无论最后怎么调整,调整后的宽高是不可能会比resolveAdjustedSize计算出来的大小大的。 从代码resolveAdjustedSize的计算可以看出,我们的drawableSize不能比parentSize小,这里的parentSize也就是小部件在屏幕占用的空间,如果小于parentSize的话就会取drawableSize,那么就有可能填充不了整个空间了。

结论就是,在我们刚才layout_width和layout_height都改成wrap_content的基础上,再加上一个足够大的1比1drawable即可。

修改drawable

xml 复制代码
<shape xmlns:android="http://schemas.android.com/apk/res/android">
    <solid android:color="#FFFFFF" />
    <size android:width="300dp" android:height="300dp" />
</shape>

再运行程序

好了,这样我们的这个Widget已经可以接受各种奇怪屏幕的挑战了

相关推荐
千里马学框架2 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台2 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone2 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc2 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo2 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077002 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼2 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui