1 前言部分
目前各大主流手机厂商都已经推出了自己的折叠屏产品,经过几轮产品迭代后,也算是比较成熟可用了。在公司里,(大)领导尤其喜欢用折叠屏,一是比较新颖,二是折叠屏展开后的大屏也有利于领导查阅文件。
但领导用就意味着,APP不得不适配折叠屏手机。适配折叠屏手机指的是折叠屏翻开后,UI不能出现严重变形、显示不全、错位、遮挡等问题,且大小屏必须切换自如,即同一个页面,折叠屏翻开后是正常的UI,合上也是正常的UI,不会出现页面销毁、闪退、崩溃等情况。
完美的适配方案,那肯定是给翻开后的折叠屏写一套布局,就像专门给横屏landscape写一套布局一样。此外,通过Google官方的Jetpack Compose、Jetpack WindowManager、Activity Embedding、SlidingPaneLayout等方案也能较好实现折叠屏适配。但增加布局就意味着工作量增加,不仅开发的工作量增加,UIUE的工作量也增加,一般来说是不会主动选择这些方案的,除非老板加人、加钱(😡)。但折叠屏的适配工作又必须完成,有没有一种比较简单的适配方案呢?
2 监听onConfigurationChanged回调
若Activity在AndroidManifest.xml中配置了android:configChanges属性,当android:configChanges中的属性值发生变化时,Activity的onConfigurationChanged方法都会被调用,这点对于做过禁止横竖屏切换时Activity被销毁重建的开发应该不陌生。android:configChanges可填写的属性值可以Bing一下或者Google一下,对于折叠屏适配,主要是涉及screensize、screenlayout以及smallestscreensize这三个属性值。
screensize:屏幕大小改变了
screenlayout:屏幕的显示发生了变化-不同的显示被激活
smallestscreensize:屏幕的物理大小改变了,如:连接到一个外部的屏幕上
<activity
android:name=".ui.demo.DemoActivity"
android:configChanges="screenSize|screenLayout|smallestScreenSize" />
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
//TODO: do something
}
3 在onConfigurationChanged中刷新UI
3.1 不需要适配器的View
对于ImageView、TextView、EditText这些不需要适配器Adapter的View,包括自定义View,若发现折叠屏大小屏切换后UI不正常,可尝试在onConfigurationChanged中调用该组件的requestLayout()方法和invalidate()方法。一般来说,如果View的layout_width和layout_height是wrap_content,就算不调用requestLayout()方法和invalidate()方法,也会具有自适应的效果。
requestLayout() 用于通知 View 进行重新布局,即测量、布局和绘制三个步骤都会重新进行。
invalidate() 用于通知 View 进行重绘,仅仅是在原有的尺寸和位置上重新绘制 View,不会重新进行测量和布局。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
view.requestLayout();
view.invalidate();
}
3.2 RecyclerView等需要适配器的View
对于RecyclerView等需要适配器Adapter的View,调用requestLayout()方法和invalidate()方法并不能生效,在Stack Overflow上有所记载,需要把RecyclerView的Adapter和LayoutManager置空,然后重新设置Adapter和LayoutManager,并刷新Adapter的数据。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
recyclerView.setAdapter(null);
recyclerView.setLayoutManager(null);
recyclerView.setAdapter(adapter);
recyclerView.setLayoutManager(layoutManager);
adapter.notifyDataSetChanged();
}
3.3 用于显示网络图片的ImageView
当ImageView用于显示网络图片时,在加载完成前,我们无法获知图片的具体尺寸,因此ImageView的layout_width和layout_height在xml中无法确定(但layout_width最大也就match_parent),需要通过代码动态设定ImageView的layout_height。
此处以Glide加载网络图片为例,ImageView的layout_width设置为match_parent,layout_height可以设置为任意合法值。Glide可以通过回调获取网络图片的Bitmap,从而获得图片的实际尺寸。
由于ImageView的layout_width是match_parent,宽度是确定的,因此只需要根据图片的实际尺寸计算宽高比例,再根据ImageView的layout_width和比例,计算并设置ImageView的layout_height。
private void setImageViewHeight(int bitmapWidth, int bitmapHeight) {
if (bitmapWidth == 0 || bitmapHeight == 0) {
return;
}
float bitmapRatio = (float) bitmapWidth / bitmapHeight;
int imageViewMeasuredWidth = imageView.getMeasuredWidth();
float imageViewMeasuredHeight = imageViewMeasuredWidth / bitmapRatio;
ViewGroup.LayoutParams imageViewLayoutParams = imageView.getLayoutParams();
imageViewLayoutParams.height = (int) imageViewMeasuredHeight;
imageView.setLayoutParams(imageViewLayoutParams);
}
private void loadNetworkImage() {
Glide.with(context).load(imageUrl).into(new SimpleTarget<Bitmap>() {
@Override
public void onResourceReady(@NonNull Bitmap resource, @Nullable Transition<? super Bitmap> transition) {
int width = resource.getWidth();
int height = resource.getHeight();
setImageViewHeight(width, height);
}
});
}
当折叠屏大小屏切换时,只需要在onConfigurationChanged中重新调用loadNetworkImage方法加载一次图片即可。
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
loadNetworkImage();
}
3.4 终极刷新方案
如果requestLayout()方法,invalidate()方法,重置Adapter和LayoutManager方法都不生效的话,还有一种终极刷新方案就是重走一遍初始化view的流程,比如说在onCreate、onStart、onResume等Activity生命周期中所做的View初始化工作。需要注意的是某些组件在重新初始化之前要销毁或取消注册,避免重复初始化造成内存泄漏,比如说BroadcastReceiver。
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
initView();
}
@Override
protected void onDestroy() {
super.onDestroy();
destroyView();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
destroyView();
initView();
}
参考: