这个问题可以变成,用有限的
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开发中介绍过,桌面小部件在桌面所占的空间受minWidth
和minHeight
与targetCellWidth
和targetCellHeight
共同影响。
因为我们设置的targetCellWidth
和targetCellHeight
都是2,所以会占用桌面2
X2
的空间。
但是2
X2
的空间可能只有100dp
X150dp
。
因为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
,宽高比是1
比1
。宽度没有问题,那我们看源码,有没有
"ratio"
这个词,有的话我们将"ratio"
设置成1
就可以了。
上面的那些View
都没有找到有用的信息,唯独ImageView
java
#ImageView
/**
* 如果您希望ImageView调整其边界以保持drawable宽高比,则将其设置为 true
*/
public void setAdjustViewBounds(boolean adjustViewBounds) {
······
}
从注释可以理解,也就是说,我们将ImageView
的adjustViewBounds
设置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
已经可以接受各种奇怪屏幕的挑战了