??在很久之前,業(yè)界推出了一種屏幕適配的方案--動態(tài)修改屏幕的density掌动。這個(gè)方案四啰,當(dāng)時(shí)轟動一時(shí),各種分析文章層出不窮粗恢,大家似乎找到了屏幕適配的仙丹靈藥柑晒。
??時(shí)間過去了這么久,不可否認(rèn)眷射,此方案確實(shí)非常的優(yōu)秀匙赞,它能幫助我們快速且準(zhǔn)確的還原視覺。正如此妖碉,在我們的項(xiàng)目中涌庭,也是使用了此方案來進(jìn)行屏幕適配。
??但是欧宜,最近我們陸續(xù)收到一些bug坐榆,正如標(biāo)題所言,在我們代碼中鱼鸠,正常的獲取dimension猛拴,得到的值卻是意料之外的,進(jìn)而導(dǎo)致我們頁面上蚀狰,很多UI顯示不正確愉昆。因此,我們對此問題進(jìn)行了深入分析麻蹋,找到了其中原因跛溉,給出了解決方案。
1. 背景
??在幾個(gè)月前扮授,我在負(fù)責(zé)中大屏(包括平板芳室,折疊屏)的適配工作。在很多頁面里面刹勃,中大屏相比于普通手機(jī)堪侯,UI的差別可能在于尺寸的不同±笕剩基于此伍宦,我就采用了如下的方案:
同一個(gè)dimension芽死,在不同的values文件定義不同的值。
例如:有一個(gè)button_width次洼,在
values
文件下定義為10dp
关贵,在values-w500dp
文件下就定義為30dp
。 如此卖毁,同一行代碼揖曾,如下:
resources.getdimensionionPixelOffset(R.dimen.button_width)
在寬度小于500dp的手機(jī)上,獲取的就是
10dp
; 反之亥啦,獲取的就是30dp
炭剪。
??如上方案,在一定程度上能夠簡化中大屏適配的工作禁悠。因?yàn)闊o論是什么屏幕尺寸念祭,對應(yīng)的代碼都是一樣的,唯一不同的就是values里面定義的值不同的碍侦。
??適配工作按照上面的方案粱坤,快樂的進(jìn)行中,最后也是準(zhǔn)時(shí)上線瓷产,似乎一切都萬事大吉站玄?
??事實(shí)證明,我還是想的過于天真濒旦。直到有一天株旷,有同事反饋說:dimension獲取的不對,我的設(shè)備屏幕寬度沒有達(dá)到xxdp尔邓,但是還是獲取了values-wxxdp文件下的值晾剖。
??我當(dāng)時(shí)第一反應(yīng)是:
??我看到這個(gè)問題,難以置信梯嗽,因?yàn)檫@個(gè)方案的可行性是不容置疑的齿尽,網(wǎng)上有很多使用的例子,包括Google也要求這樣使用的灯节。但是固定的復(fù)現(xiàn)路徑直接貼我臉上循头,讓我不得不懷疑自身。
??于是炎疆,我自己先搞了一個(gè)Demo卡骂,試了下,沒有問題靶稳搿全跨?后來,同事告訴我亿遂,咱們的項(xiàng)目會修改屏幕的density,于是我把修改density的代碼CV到Demo工程中浓若,一運(yùn)行盒使,哦吼!完美復(fù)現(xiàn)七嫌。
??總結(jié)下,我們這個(gè)問題的復(fù)現(xiàn)場景:
- Activity是橫屏的苞慢,
screenOrientation
聲明為sensorLandscape
, 保證能夠隨傳感器變換方向诵原。- 在Activity的onCreate方法里面,會動態(tài)修改屏幕的density挽放。
- 此時(shí)再去通過resource獲取dimension绍赛,屏幕經(jīng)過修改density之后,寬度沒有達(dá)到xxdp,但是還是取了values-wxxdp文件下的dimension值辑畦。
??復(fù)現(xiàn)代碼可參考:DensityDemo
2. 解決方案
??為了不浪費(fèi)各位的寶貴時(shí)間吗蚌,我先貼出解決方案,然后分析此問題的原因纯出。
??解決方案非常簡單蚯妇,如下是我們修改density的代碼:
fun Resources.setDensity() {
val metrics = displayMetrics ?: return
val width = metrics.widthPixels
val height = metrics.heightPixels + getNavigationBarHeight()
//獲取以設(shè)計(jì)圖總寬度360dp下的density值
val targetDensity =
(min(width, height) / 360f).toInt()
//獲取以設(shè)計(jì)圖總寬度360dp下的dpi值
val targetDensityDpi = (160f * targetDensity).toInt()
// 更新displayMetrics的信息
metrics.density = targetDensity.toFloat()
metrics.scaledDensity = targetDensity.toFloat()
metrics.densityDpi = targetDensityDpi
// 更新configuration的信息
val configuration = configuration ?: return
configuration.screenWidthDp = (width / targetDensity)
configuration.screenHeightDp = (height / targetDensity)
configuration.densityDpi = targetDensityDpi
// 修復(fù)dimen獲取不對的問題,7.0及其以下調(diào)用此方法暂筝。
// updateConfiguration(configuration, metrics)
}
??代碼中箩言,最為關(guān)鍵的兩步分別是:
- 更新
configuration
的信息,將configuration
的screenWidthDp
焕襟、screenHeightDp
和densityDpi
更新為預(yù)期值- 將更新完的
configuration
和metrics
陨收,傳遞給Resource
的updateConfiguration
方法,去刷新內(nèi)部的信息鸵赖。這一步非常重要务漩,也是解決此問題的關(guān)鍵所在。
??這里還需要關(guān)注的一點(diǎn)就是它褪,updateConfiguration
方法在Android 7.0 以上饵骨,被Google標(biāo)記為過時(shí)。所以我們還需要去找代替的方法列赎。代替的代碼大致如下:
override fun getResources(): Resources {
val resources = super.getResources()
resources.setDensity()
return createConfigurationContext(resources.configuration).resources
}
??在通過上面的setDensity
方法宏悦,更新完對應(yīng)的信息之后,我們需要重寫Activity的getResource
方法包吝,通過createConfigurationContext
方法去更新Resource
里面的configuration
饼煞。
??總結(jié)如下:
- 在Android 7.0 及其以下,我們可以通過
updateConfiguration
方法去更新Resource內(nèi)部的信息诗越,進(jìn)而解決dimension不對的問題砖瞧。- 在Android 7.0以上版本,需要通過
createConfigurationContext
去更新Resource內(nèi)部的信息嚷狞,同時(shí)setDensity
方法也是需要調(diào)用的块促,進(jìn)而解決dimension不對的問題荣堰。
??額外說一句,其實(shí)在高版本上調(diào)用updateConfiguration
也是可行的竭翠,就是需要看下兼容性問題振坚;完全沒必要重寫getResources
方法,畢竟createConfigurationContext
方法成本也挺大的斋扰。
3. 問題分析
??在分析源碼渡八,我們先對這個(gè)問題進(jìn)行一些分析。項(xiàng)目中聲明多個(gè)values-wxxdp文件传货,在獲取值的時(shí)候屎鳍,系統(tǒng)會根據(jù)當(dāng)前的屏幕寬度,自動匹配符合要求的values文件下问裕,然后再從對應(yīng)的values文件下獲取定義好的值逮壁。
??現(xiàn)在出現(xiàn)的問題,就是匹配錯了values文件夾粮宛。為啥會匹配錯呢窥淆?是不是屏幕寬度不對呢?所以巍杈,當(dāng)時(shí)排查方向就是祖乳,反復(fù)校驗(yàn)屏幕的寬度是否正確。
??屏幕的絕對像素值是不會改變的秉氧,但是屏幕寬度的dp值會跟隨修改的density而變換眷昆。所以,我們當(dāng)時(shí)第一排查方向是汁咏,是不是哪里沒有改到亚斋,導(dǎo)致系統(tǒng)自己在獲取屏幕寬度有問題?
??獲取dimension基本都是通過Resource的getdimensionionXXX
方法攘滩。于是帅刊,我們從這個(gè)方法開始入手,查看方法內(nèi)部的代碼實(shí)現(xiàn)漂问,但是從Java層并沒有發(fā)現(xiàn)可疑的點(diǎn)赖瞒。
??getdimensionionXXX
方法,最終會調(diào)用到AssetManager
的getResourceValue
方法蚤假。
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
boolean resolveRefs) {
Objects.requireNonNull(outValue, "outValue");
synchronized (this) {
ensureValidLocked();
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (cookie <= 0) {
return false;
}
// Convert the changing configurations flags populated by native code.
outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
return false;
}
}
return true;
}
}
??getResourceValue
方法有一個(gè)TypedValue
參數(shù)栏饮,最終獲取的dimension值這個(gè)參數(shù)的data
字段。data
字段雖然是一個(gè)int類型磷仰,但是它自身是一個(gè)復(fù)合數(shù)據(jù)袍嬉,內(nèi)部只有部分bit位才表示最終的dimension值。
??經(jīng)過上面的分析,我們并沒有找到匹配屏幕寬度的地方伺通,且TypedValue
內(nèi)部字段的賦值操作箍土,也是通過nativeGetResourceValue
這個(gè)native方法進(jìn)行的。到這里罐监,看來必須深入到C++層吴藻,去查看對應(yīng)的實(shí)現(xiàn)。
??友情提示弓柱,下面將進(jìn)入枯燥繁雜的C++代碼分析環(huán)節(jié)调缨。如下C++代碼均參考于Android 14.0
版本的aosp。
??AssetManager.java
對應(yīng)的C++文件是android_util_AssetManager.cpp
吆你。這個(gè)文件內(nèi)部的nativeGetResourceValue
方法實(shí)現(xiàn)如下:
static jint NativeGetResourceValue(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint resid,
jshort density, jobject typed_value,
jboolean resolve_references) {
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
ResourceTimer _timer(ResourceTimer::Counter::GetResourceValue);
// 1. 獲取value。
auto value = assetmanager->GetResource(static_cast<uint32_t>(resid), false /*may_be_bag*/,
static_cast<uint16_t>(density));
if (!value.has_value()) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
if (resolve_references) {
auto result = assetmanager->ResolveReference(value.value());
if (!result.has_value()) {
return ApkAssetsCookieToJavaCookie(kInvalidCookie);
}
}
// 2. 將value里面的值賦值給typed_value
return CopyValue(env, *value, typed_value);
}
??這個(gè)方法里面俊犯,我們重點(diǎn)關(guān)注兩個(gè)點(diǎn)妇多。
- 通過
assetmanager
的GetResource
方法,獲取了一個(gè)value值燕侠。這個(gè)value字段里面就包含我們想要的dimension者祖。- 將value字段里面的值賦值給typed_value。這個(gè)
typed_value
里面绢彤,就是在Java層看到TypedValue七问。
??所以,我們重點(diǎn)就要放到GetResource
方法里面去茫舶。這個(gè)assetmanager
是一個(gè)AssetManager2
的對象械巡,對應(yīng)代碼文件是AssetManager2.cpp
。我們來看下GetResource
方法的實(shí)現(xiàn):
base::expected<AssetManager2::SelectedValue, NullOrIOError> AssetManager2::GetResource(
uint32_t resid, bool may_be_bag, uint16_t density_override) const {
// 1. 找到對應(yīng)的結(jié)果饶氏。
auto result = FindEntry(resid, density_override, false /* stop_at_first_match */,
false /* ignore_configuration */);
if (!result.has_value()) {
return base::unexpected(result.error());
}
auto result_map_entry = std::get_if<incfs::verified_map_ptr<ResTable_map_entry>>(&result->entry);
if (result_map_entry != nullptr) {
if (!may_be_bag) {
LOG(ERROR) << base::StringPrintf("Resource %08x is a complex map type.", resid);
return base::unexpected(std::nullopt);
}
// Create a reference since we can't represent this complex type as a Res_value.
return SelectedValue(Res_value::TYPE_REFERENCE, resid, result->cookie, result->type_flags,
resid, result->config);
}
// Convert the package ID to the runtime assigned package ID.
// 2. 從result里面獲取entry字段
Res_value value = std::get<Res_value>(result->entry);
result->dynamic_ref_table->lookupResourceValue(&value);
// 3. 取entry中的data字段讥耗,為dimension的結(jié)果
return SelectedValue(value.dataType, value.data, result->cookie, result->type_flags,
resid, result->config);
}
??通過上述代碼,我們知道了疹启,dimension的值最終取自于result->entry.data
字段古程。看來喊崖,關(guān)鍵在于result
字段挣磨,也就是要去看FindEntry方法
。FindEntry
方法的代碼較長荤懂,我僅截取重要部分進(jìn)行分析茁裙,代碼如下:
base::expected<FindEntryResult, NullOrIOError> AssetManager2::FindEntry(
uint32_t resid, uint16_t density_override, bool stop_at_first_match,
bool ignore_configuration) const {
// ······省略部分代碼········
// Might use this if density_override != 0.
ResTable_config density_override_config;
// Select our configuration or generate a density override configuration.
// 1. 記錄AssetManager2自帶的賦值給desired_config,用于下面的匹配工作节仿。
const ResTable_config* desired_config = &configuration_;
if (density_override != 0 && density_override != configuration_.density) {
density_override_config = configuration_;
density_override_config.density = density_override;
desired_config = &density_override_config;
}
// ······省略部分代碼········
if (!stop_at_first_match && !ignore_configuration && !apk_assets_[result->cookie]->IsLoader()) {
for (const auto& id_map : package_group.overlays_) {
auto overlay_entry = id_map.overlay_res_maps_.Lookup(resid);
if (!overlay_entry) {
// No id map entry exists for this target resource.
continue;
}
if (overlay_entry.IsInlineValue()) {
// The target resource is overlaid by an inline value not represented by a resource.
ConfigDescription best_frro_config;
Res_value best_frro_value;
bool frro_found = false;
for( const auto& [config, value] : overlay_entry.GetInlineValue()) {
// 2.尋找最優(yōu)匹配的values呜达。注意,這里match方法傳入的desired_config是屏幕的粟耻,不是資源的查近。
if ((!frro_found || config.isBetterThan(best_frro_config, desired_config))
&& config.match(*desired_config)) {
frro_found = true;
best_frro_config = config;
best_frro_value = value;
}
}
if (!frro_found) {
continue;
}
// 3. 找到最優(yōu)結(jié)果之后眉踱,賦值給result->entry。最后返回霜威。
result->entry = best_frro_value;
result->dynamic_ref_table = id_map.overlay_res_maps_.GetOverlayDynamicRefTable();
result->cookie = id_map.cookie;
// ······省略部分代碼········
}
// ······省略部分代碼········
}
}
// ······省略部分代碼········
return result;
}
??這個(gè)方法重點(diǎn)代碼谈喳,我將其分為3個(gè)部分。
configuration_
賦值給desired_config
,用于下述的匹配工作戈泼。這個(gè)configuration_
非常重要婿禽,它是我們解決問題的關(guān)鍵所在,所以這里單獨(dú)拎出來大猛。- 去尋找最優(yōu)匹配的value扭倾。這里的
isBetterThan
方法,目的是為了尋找最優(yōu)的values挽绩。怎么理解這個(gè)最優(yōu)呢膛壹?比如說,當(dāng)前定義了兩個(gè)values文件夾:values-w100dp,values-w200dp唉堪, 此時(shí)屏幕寬度是600dp模聋。按照規(guī)則,其實(shí)兩個(gè)values都符合要求唠亚,此時(shí)就要選擇最優(yōu)的values链方,即差值最小的,也就是最終會取values-w200dp內(nèi)部的值灶搜; 這里還有一個(gè)match
方法祟蚀,就是去校驗(yàn)values的config跟屏幕的config(即AssetManager2的config)是否匹配。- 最后一步割卖,就是將找到的結(jié)果暂题,賦值給result->entry,然后返回究珊。
??通過上面的分析薪者,我們基本了解,要找的答案剿涮,就在match
方法里面言津。isBetterThan
方法是尋找最優(yōu)的結(jié)果,內(nèi)部是通過差值來計(jì)算最優(yōu)值取试,這里就不過多介紹悬槽。來看下match
方法的實(shí)現(xiàn),代碼實(shí)現(xiàn)在ResourceTypes.cpp
文件中瞬浓,如下:
bool ResTable_config::match(const ResTable_config& settings) const {
//····省略代碼·······
if (screenSizeDp != 0) {
// 這里的screenWidthDp就是values文件后面聲明的后綴初婆,類似:w500dp。
// settings.screenWidthDp表示系統(tǒng)自身的屏幕寬度,這個(gè)是從AssetManager.cpp自帶的磅叛。
if (screenWidthDp != 0 && screenWidthDp > settings.screenWidthDp) {
if (kDebugTableSuperNoisy) {
ALOGI("Filtering out width %d in requested %d", screenWidthDp,
settings.screenWidthDp);
}
return false;
}
if (screenHeightDp != 0 && screenHeightDp > settings.screenHeightDp) {
if (kDebugTableSuperNoisy) {
ALOGI("Filtering out height %d in requested %d", screenHeightDp,
settings.screenHeightDp);
}
return false;
}
}
//····省略代碼·······
return true;
}
??在上面的方法中屑咳,我們終于看到values的聲明的寬度跟屏幕寬度的比較代碼了。那為什么這個(gè)判斷就錯了呢弊琴?
??可以這么來理解兆龙,screenWidthDp != 0 && screenWidthDp > settings.screenWidthDp
本來這個(gè)要判斷為true的,最終match
方法也是要返回為false敲董。但是由于這個(gè)判斷失效了紫皇,導(dǎo)致最終return true,取錯了dimension腋寨。
??那么聪铺,什么情況下,如上的判斷會失效呢萄窜?首先铃剔,screenWidthDp
是values文件靜態(tài)聲明的,所以不會有錯脂倦;唯一可能有問題的就是settings.screenHeightDp
。那這個(gè)值又是從哪里來的呢元莫?
??settings
是從AssetManager2.cpp
里面?zhèn)鬟f過來赖阻,而這個(gè)參數(shù)是它的成員變量。所以踱蠢,只要settings.screenHeightDp
是一個(gè)錯誤的值火欧,那么這個(gè)判斷就可能不生效了。
??看上去茎截,我們基本分析到原因所在苇侵。
屏幕寬度(dp單位)其實(shí)在系統(tǒng)的代碼中,有兩個(gè)地方保存了企锌,一個(gè)是Java層榆浓,一個(gè)C++層。而我們在更新屏幕density時(shí)撕攒,僅更新了Java層陡鹃,C++層并沒有更新。
所以抖坪,我們在獲取dimension時(shí)萍鲸,就會看到屏幕寬度(dp單位,這里指的是Java層的)明明沒有達(dá)到xxdp擦俐,但還是取了values-xxdp下的值脊阴,因?yàn)镃++層的屏幕寬度(dp單位)達(dá)到了甚至超過了xxdp。
??既然,我們知道了原因嘿期,那么就可以對癥下藥品擎。只要我們把修改density之后的屏幕寬度,更新到C++層秽五,再去獲取對應(yīng)的dimension就沒有問題了吧孽查?答案正是如此,那么怎么更新C++層的屏幕寬度呢坦喘?這個(gè)就要回去看AssetManager2.cpp
的代碼了盲再。
4. 解決方案的分析
??上面有介紹到,AssetManager2.cpp
內(nèi)部有一個(gè)configuration_
的成員變量瓣铣,match
方法就是從這個(gè)變量去獲取當(dāng)前屏幕的寬度答朋。所以,只要我們能找到這個(gè)變量在哪里設(shè)置棠笑,就可以知道怎么去更新它了梦碗。通過搜索AssetManager2
的代碼,我發(fā)現(xiàn)了如下方法:
void AssetManager2::SetConfiguration(const ResTable_config& configuration) {
const int diff = configuration_.diff(configuration);
configuration_ = configuration;
if (diff) {
RebuildFilterList();
InvalidateCaches(static_cast<uint32_t>(diff));
}
}
??AssetManager2
有一個(gè)SetConfiguration
方法蓖救,在這個(gè)方法里面洪规,在更新configuration_
。那么循捺,哪里在調(diào)用SetConfiguration
這個(gè)方法呢斩例?我們就要回到android_util_AssetManager.cpp
中,如下:
static void NativeSetConfiguration(JNIEnv* env, jclass /*clazz*/, jlong ptr, jint mcc, jint mnc,
jstring locale, jint orientation, jint touchscreen, jint density,
jint keyboard, jint keyboard_hidden, jint navigation,
jint screen_width, jint screen_height,
jint smallest_screen_width_dp, jint screen_width_dp,
jint screen_height_dp, jint screen_layout, jint ui_mode,
jint color_mode, jint grammatical_gender, jint major_version) {
ATRACE_NAME("AssetManager::SetConfiguration");
ResTable_config configuration;
memset(&configuration, 0, sizeof(configuration));
// 1. 傳遞過來的參數(shù)从橘,更新到configuration念赶。
configuration.mcc = static_cast<uint16_t>(mcc);
configuration.mnc = static_cast<uint16_t>(mnc);
configuration.orientation = static_cast<uint8_t>(orientation);
configuration.touchscreen = static_cast<uint8_t>(touchscreen);
configuration.density = static_cast<uint16_t>(density);
configuration.keyboard = static_cast<uint8_t>(keyboard);
configuration.inputFlags = static_cast<uint8_t>(keyboard_hidden);
configuration.navigation = static_cast<uint8_t>(navigation);
configuration.screenWidth = static_cast<uint16_t>(screen_width);
configuration.screenHeight = static_cast<uint16_t>(screen_height);
configuration.smallestScreenWidthDp = static_cast<uint16_t>(smallest_screen_width_dp);
configuration.screenWidthDp = static_cast<uint16_t>(screen_width_dp);
configuration.screenHeightDp = static_cast<uint16_t>(screen_height_dp);
configuration.screenLayout = static_cast<uint8_t>(screen_layout);
configuration.uiMode = static_cast<uint8_t>(ui_mode);
configuration.colorMode = static_cast<uint8_t>(color_mode);
configuration.grammaticalInflection = static_cast<uint8_t>(grammatical_gender);
configuration.sdkVersion = static_cast<uint16_t>(major_version);
if (locale != nullptr) {
ScopedUtfChars locale_utf8(env, locale);
CHECK(locale_utf8.c_str() != nullptr);
configuration.setBcp47Locale(locale_utf8.c_str());
}
// Constants duplicated from Java class android.content.res.Configuration.
static const jint kScreenLayoutRoundMask = 0x300;
static const jint kScreenLayoutRoundShift = 8;
// In Java, we use a 32bit integer for screenLayout, while we only use an 8bit integer
// in C++. We must extract the round qualifier out of the Java screenLayout and put it
// into screenLayout2.
configuration.screenLayout2 =
static_cast<uint8_t>((screen_layout & kScreenLayoutRoundMask) >> kScreenLayoutRoundShift);
ScopedLock<AssetManager2> assetmanager(AssetManagerFromLong(ptr));
// 2. 刷新AssetManager2的configuration。
assetmanager->SetConfiguration(configuration);
}
??NativeSetConfiguration
方法內(nèi)部主要做了兩件事:
- 將傳遞過來最新值恰力,更新到configuration上去叉谜。
- 再將configuration傳遞給
AssetManager2
,用以刷新它內(nèi)部的值踩萎。
??NativeSetConfiguration
是一個(gè)native 方法停局,它對應(yīng)的Java方法是哪一個(gè)呢?當(dāng)然是AssetManager
的nativeSetConfiguration
方法香府。
??然后翻具,我們再去尋找,哪里在調(diào)用nativeSetConfiguration
方法呢回还?最終就找到了Resource
的updateConfiguration
方法裆泳,這也是為什么上面的解決方案中,我們調(diào)用下這個(gè)方法柠硕,就能解決這個(gè)問題工禾。
??同理运提,調(diào)用createConfigurationContext
方法,因?yàn)橹匦聞?chuàng)建Resource闻葵,所以也會去調(diào)用nativeSetConfiguration
方法民泵,間接的刷新了C++層的屏幕寬度。
5. 一個(gè)彩蛋
??我們排查的問題過程中槽畔,發(fā)現(xiàn)了另外一個(gè)問題栈妆。將Activity的screenOrientation
刷新聲明為sensorLandscape
之后,切換屏幕方向時(shí)(正向橫屏和反向橫屏之間的切換)厢钧,并不會回調(diào)onConfigurationChanged
方法鳞尔。同時(shí),切換方向之后早直,屏幕的density還被重置了寥假。
??所以,我們還需要在這種情況下重新設(shè)置屏幕的density霞扬。那么去處理這種問題呢糕韧?
??目前,我想到了兩種方案喻圃。
(1).切換方向變換萤彩,重新設(shè)置density
??實(shí)現(xiàn)代碼如下:
findViewById<View>(R.id.container).apply {
viewTreeObserver.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
private var mLastRotation = -1
override fun onGlobalLayout() {
if (display.rotation != mLastRotation) {
mLastRotation = display.rotation
// 更新屏幕的density亲桦。
setupDensity()
}
}
})
}
??但是這種方案有一個(gè)弊端裹驰,由于再layout階段再去設(shè)置density,可能會導(dǎo)致measure階段獲取的dimension有些問題史汗。
(2). 重寫getResource方法饮焦,去更新density
??代碼實(shí)現(xiàn)如下:
override fun getResources(): Resources {
val resources = super.getResources()
resources.setDensity()
return createConfigurationContext(resources.configuration).resources
}
??但是由于getResources
方法會頻繁調(diào)用怕吴,所以最好給setDensity
方法加一個(gè)判斷窍侧,我這里僅是為了簡單县踢,所以沒做處理。
??實(shí)現(xiàn)代碼可參考:DensityDemo
6. 總結(jié)
??最后伟件,我們來做個(gè)總結(jié)硼啤。
- 在我們修改屏幕的density之后,僅更新Java層的值斧账,并沒有更新C++層谴返。所以導(dǎo)致在獲取dimension時(shí),C++層用的是舊值去判斷咧织,所以導(dǎo)致dimension獲取的不對嗓袱。
- 在我們更新完density之后,需要調(diào)用
Resource
的updateConfiguration
方法习绢,去更新C++層的屏幕寬度(dp單位)
??額外補(bǔ)充兩句渠抹,可能大家在實(shí)際開發(fā)過程中很少遇到這種問題蝙昙,原因應(yīng)該是,系統(tǒng)默認(rèn)的屏幕寬度和我們修改density之后的屏幕寬度都比指定的values-wxxdp要大梧却,或者要小奇颠,所以難以發(fā)現(xiàn)這個(gè)問題。
??為什么將Activity
聲明為sensorLandscape
就會復(fù)現(xiàn)這個(gè)問題呢放航?其實(shí)跟screenOrientation
沒有關(guān)系烈拒,只要values設(shè)置的dp值在默認(rèn)的屏幕寬度和我們修改density之后的屏幕寬度中間,就會有問題广鳍。只不過荆几,在我們的場景,剛好是橫屏才會遇到這種情況搜锰。