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已经可以接受各种奇怪屏幕的挑战了

相关推荐
深海呐1 小时前
Android AlertDialog圆角背景不生效的问题
android
ljl_jiaLiang1 小时前
android10 系统定制:增加应用使用数据埋点,应用使用时长统计
android·系统定制
花花鱼1 小时前
android 删除系统原有的debug.keystore,系统运行的时候,重新生成新的debug.keystore,来完成App的运行。
android
落落落sss2 小时前
sharding-jdbc分库分表
android·java·开发语言·数据库·servlet·oracle
消失的旧时光-19434 小时前
kotlin的密封类
android·开发语言·kotlin
服装学院的IT男5 小时前
【Android 13源码分析】WindowContainer窗口层级-4-Layer树
android
CCTV果冻爽7 小时前
Android 源码集成可卸载 APP
android
码农明明7 小时前
Android源码分析:从源头分析View事件的传递
android·操作系统·源码阅读
秋月霜风8 小时前
mariadb主从配置步骤
android·adb·mariadb
Python私教8 小时前
Python ORM 框架 SQLModel 快速入门教程
android·java·python