在做Android app开发的时候,为了验证不同屏幕分辨率和dpi下界面的布局情况。你可以使用android emulator来实现,也可以找不同屏幕配置的手机来验证。当然,你可以找台Android原生系统的手机来验证如Nexus 4/Nexus 5系列的手机来验证。Android系统中有一个wm命令,可以设置显示窗口的尺寸(重新设置屏幕的罗辑分辩率)和屏幕的dpi。
- 设置显示窗口的尺寸
$ adb shell wm size 540x960
显示窗口的尺寸可以比屏幕的物理分辨率大,也可以比它小。重置用如下命令:
$ adb shell wm size reset
设置完之后,你会发现SurfaceFlinger中的Layer的尺寸也发生的变化。
以Nexus4为例,原来Live Wallpaper: PhaseBeam Layer的尺寸为768×1280:
$ adb shell dumpsys SurfaceFlinger
...
+ Layer 0xb7bbf3b0 ()
Region transparentRegion (this=0xb7bbf510, count=1)
[ 0, 0, 0, 0]
Region visibleRegion (this=0xb7bbf3b8, count=1)
[ 0, 50, 768, 1184]
layerStack= 0, z= 21000, pos=(0,0), size=( 768,1280), crop=( 0, 50, 768,1184), isOpaque=1, invalidate=0, alpha=0xff, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0xb7bad728
format= 2, activeBuffer=[ 768x1280: 768, 3], queued-frames=0, mRefreshPending=0
mTexName=10 mCurrentTexture=0
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
-BufferQueue mMaxAcquiredBufferCount=1, mDequeueBufferCannotBlock=0, default-size=[768x1280], default-format=2, transform-hint=00, FIFO(0)={}
>[00:0xb7bbef08] state=ACQUIRED, 0xb7b18260 [ 768x1280: 768, 3]
[01:0xb7baf328] state=FREE , 0xb7badaa8 [ 768x1280: 768, 3]
[02:0xb7b1a5d0] state=FREE , 0xb7bad9a0 [ 768x1280: 768, 3]
...
将显示窗口的尺寸设为1080×1920之后:
$ adb shell dumpsys SurfaceFlinger
...
+ Layer 0xb7d596b8 ()
Region transparentRegion (this=0xb7d59818, count=1)
[ 0, 0, 0, 0]
Region visibleRegion (this=0xb7d596c0, count=1)
[ 0, 50, 1080, 1824]
layerStack= 0, z= 21000, pos=(0,0), size=(1080,1920), crop=( 0, 50,1080,1824), isOpaque=1, invalidate=0, alpha=0xff, flags=0x00000002, tr=[1.00, 0.00][0.00, 1.00]
client=0xb7dbf270
format= 2, activeBuffer=[1080x1920:1152, 3], queued-frames=0, mRefreshPending=0
mTexName=10 mCurrentTexture=2
mCurrentCrop=[0,0,0,0] mCurrentTransform=0
mAbandoned=0
-BufferQueue mMaxAcquiredBufferCount=1, mDequeueBufferCannotBlock=0, default-size=[1080x1920], default-format=2, transform-hint=00, FIFO(0)={}
[00:0xb7dd1fb0] state=FREE , 0xb7dd0048 [1080x1920:1152, 3]
[01:0xb7d5c2c8] state=FREE , 0xb7d39ca8 [1080x1920:1152, 3]
>[02:0xb7d1d578] state=ACQUIRED, 0xb7d5cb18 [1080x1920:1152, 3]
...
可以想象得到,系统把这个layer对应的窗口当成了一个1080p屏幕来布局和绘制了。
- 设置屏幕的dpi
$ adb shell wm density 320
常用的dpi有160(mdpi), 240(hdpi), 320(xhdpi), 480(xxhdpi)。重置可用如下命令:
$ adb shell wm density reset
- 如何实现 ,以设置显示窗口尺寸为例(@android-5.1.1)
首先看一下wm命令如何通知WindowManagerService更新配置:
代码:frameworks/base/cmds/wm/src/com/android/commands/wm/
public class Wm extends BaseCommand { ... public void onRun() throws Exception { mWm = (( Context.WINDOW_SERVICE)); if (mWm == null) { (NO_SYSTEM_ERROR_CODE); throw new AndroidException("Can't connect to window manager; is the system running?"); } String op = nextArgRequired(); if (("size")) { runDisplaySize(); } else if (("density")) { runDisplayDensity(); } else if (("overscan")) { runDisplayOverscan(); } else { showError("Error: unknown command '" + op + "'"); return; } } ... private void runDisplaySize() throws Exception { String size = nextArg(); int w, h; if (size == null) { Point initialSize = new Point(); Point baseSize = new Point(); try { (Display.DEFAULT_DISPLAY, initialSize); (Display.DEFAULT_DISPLAY, baseSize); ("Physical size: " + + "x" + ); if (!(baseSize)) { ("Override size: " + + "x" + ); } } catch (RemoteException e) { } return; } else if ("reset".equals(size)) { w = h = -1; } else { int div = ('x'); if (div <= 0 || div >= (()-1)) { ("Error: bad size " + size); return; } String wstr = (0, div); String hstr = (div+1); try { w = (wstr); h = (hstr); } catch (NumberFormatException e) { ("Error: bad number " + e); return; } } try { if (w >= 0 && h >= 0) { // TODO(multidisplay): For now Configuration only applies to main screen. (Display.DEFAULT_DISPLAY, w, h); } else { (Display.DEFAULT_DISPLAY); } } catch (RemoteException e) { } } ... }
先获取WindowManagerService再调用它的setForcedDisplaySize()方法。
代码:frameworks/base/services/core/java/com/android/server/wm/
public class WindowManagerService extends implements , { ... @Override public void setForcedDisplaySize(int displayId, int width, int height) { if (( .WRITE_SECURE_SETTINGS) != PackageManager.PERMISSION_GRANTED) { throw new SecurityException("Must hold permission " + .WRITE_SECURE_SETTINGS); } if (displayId != Display.DEFAULT_DISPLAY) { throw new IllegalArgumentException("Can only set the default display"); } final long ident = (); try { synchronized(mWindowMap) { // Set some sort of reasonable bounds on the size of the display that we // will try to emulate. final int MIN_WIDTH = 200; final int MIN_HEIGHT = 200; final int MAX_SCALE = 2; final DisplayContent displayContent = getDisplayContentLocked(displayId); if (displayContent != null) { width = ((width, MIN_WIDTH), * MAX_SCALE); height = ((height, MIN_HEIGHT), * MAX_SCALE); setForcedDisplaySizeLocked(displayContent, width, height); ((), .DISPLAY_SIZE_FORCED, width + "," + height); } } } finally { (ident); } } ... // displayContent must not be null private void setForcedDisplaySizeLocked(DisplayContent displayContent, int width, int height) { (TAG, "Using new display size: " + width + "x" + height); synchronized() { = width; = height; } reconfigureDisplayLocked(displayContent); } ... // displayContent must not be null private void reconfigureDisplayLocked(DisplayContent displayContent) { // TODO: Multidisplay: for now only use with default display. configureDisplayPolicyLocked(displayContent); = true; boolean configChanged = updateOrientationFromAppTokensLocked(false); (); = ; if (computeScreenConfigurationLocked(mTempConfiguration)) { if ((mTempConfiguration) != 0) { configChanged = true; } } if (configChanged) { mWaitingForConfig = true; startFreezingDisplayLocked(false, 0, 0); (H.SEND_NEW_CONFIGURATION); } performLayoutAndPlaceSurfacesLocked(); } ... }
setForcedDisplaySize()->setForcedDisplaySizeLocked()->reconfigureDisplayLocked()->SEND_NEW_CONFIGURATION
这里可以看到可以设置的屏幕窗口最小为(200,200),最大小显示屏分辨率的两倍。
而WindowManagerService又是如何通知DisplayManagerService更新配置:
WindowsManagerService$(SEND_NEW_CONFIGURATION)
->()
->() x2
->()
->()
->()
->()
->DisplayManagerService$()
->$4200()
->()
->()
->()
->()
到这里就会去设置layer stack的尺寸和在屏幕上的位置:
final class LogicalDisplay { ... /** * Applies the layer stack and transformation to the given display device * so that it shows the contents of this logical display. * * We know that the given display device is only ever showing the contents of * a single logical display, so this method is expected to blow away all of its * transformation properties to make it happen regardless of what the * display device was previously showing. * * The caller must have an open Surface transaction. * * The display device may not be the primary display device, in the case * where the display is being mirrored. * * @param device The display device to modify. * @param isBlanked True if the device is being blanked. */ public void configureDisplayInTransactionLocked(DisplayDevice device, boolean isBlanked) { final DisplayInfo displayInfo = getDisplayInfoLocked(); final DisplayDeviceInfo displayDeviceInfo = (); // Set the layer stack. (isBlanked ? BLANK_LAYER_STACK : mLayerStack); // Set the refresh rate (mRequestedRefreshRate); // Set the viewport. // This is the area of the logical display that we intend to show on the // display device. For now, it is always the full size of the logical display. (0, 0, , ); // Set the orientation. // The orientation specifies how the physical coordinate system of the display // is rotated when the contents of the logical display are rendered. int orientation = Surface.ROTATION_0; if (( & DisplayDeviceInfo.FLAG_ROTATES_WITH_CONTENT) != 0) { orientation = ; } // Apply the physical rotation of the display device itself. orientation = (orientation + ) % 4; // Set the frame. // The frame specifies the rotated physical coordinates into which the viewport // is mapped. We need to take care to preserve the aspect ratio of the viewport. // Currently we maximize the area to fill the display, but we could try to be // more clever and match resolutions. boolean rotated = (orientation == Surface.ROTATION_90 || orientation == Surface.ROTATION_270); int physWidth = rotated ? : ; int physHeight = rotated ? : ; // Determine whether the width or height is more constrained to be scaled. // physWidth / => letter box // or physHeight / => pillar box // // We avoid a division (and possible floating point imprecision) here by // multiplying the fractions by the product of their denominators before // comparing them. int displayRectWidth, displayRectHeight; if (physWidth * < physHeight * ) { // Letter box. displayRectWidth = physWidth; displayRectHeight = * physWidth / ; } else { // Pillar box. displayRectWidth = * physHeight / ; displayRectHeight = physHeight; } int displayRectTop = (physHeight - displayRectHeight) / 2; int displayRectLeft = (physWidth - displayRectWidth) / 2; (displayRectLeft, displayRectTop, displayRectLeft + displayRectWidth, displayRectTop + displayRectHeight); (orientation, mTempLayerStackRect, mTempDisplayRect); } ... }
->()
通知SurfaceFlinger:
abstract class DisplayDevice {
...
/**
* Sets the display projection while in a transaction.
*
* @param orientation defines the display's orientation
* @param layerStackRect defines which area of the window manager coordinate
* space will be used
* @param displayRect defines where on the display will layerStackRect be
* mapped to. displayRect is specified post-orientation, that is
* it uses the orientation seen by the end-user
*/
public final void setProjectionInTransactionLocked(int orientation,
Rect layerStackRect, Rect displayRect) {
if (mCurrentOrientation != orientation
|| mCurrentLayerStackRect == null
|| !(layerStackRect)
|| mCurrentDisplayRect == null
|| !(displayRect)) {
mCurrentOrientation = orientation;
if (mCurrentLayerStackRect == null) {
mCurrentLayerStackRect = new Rect();
}
(layerStackRect);
if (mCurrentDisplayRect == null) {
mCurrentDisplayRect = new Rect();
}
(displayRect);
SurfaceControl.setDisplayProjection(mDisplayToken,
orientation, layerStackRect, displayRect);
}
}
...
}
最后,SurfaceFlinger就会跟据这些信息对Layer进行合成,显示在屏幕上。
代码:frameworks/native/services/surfaceflinger/
void DisplayDevice::setProjection(int orientation, const Rect& newViewport, const Rect& newFrame) { Rect viewport(newViewport); Rect frame(newFrame); const int w = mDisplayWidth; const int h = mDisplayHeight; Transform R; DisplayDevice::orientationToTransfrom(orientation, w, h, &R); if (!()) { // the destination frame can be invalid if it has never been set, // in that case we assume the whole display frame. frame = Rect(w, h); } if (()) { // viewport can be invalid if it has never been set, in that case // we assume the whole display size. // it's also invalid to have an empty viewport, so we handle that // case in the same way. viewport = Rect(w, h); if (() & Transform::ROT_90) { // viewport is always specified in the logical orientation // of the display (ie: post-rotation). swap(, ); } } (getBounds()); Transform TL, TP, S; float src_width = (); float src_height = (); float dst_width = (); float dst_height = (); if (src_width != dst_width || src_height != dst_height) { float sx = dst_width / src_width; float sy = dst_height / src_height; (sx, 0, 0, sy); } float src_x = ; float src_y = ; float dst_x = ; float dst_y = ; (-src_x, -src_y); (dst_x, dst_y); // The viewport and frame are both in the logical orientation. // Apply the logical translation, scale to physical size, apply the // physical translation and finally rotate to the physical orientation. mGlobalTransform = R * TP * S * TL; const uint8_t type = (); mNeedsFiltering = (!() || (type >= Transform::SCALE)); mScissor = (viewport); if (()) { mScissor = getBounds(); } mOrientation = orientation; mViewport = viewport; mFrame = frame; }
- 在三星A9上将屏幕的density由原来的480更改为320,重启后发现显示的效果不太好:
- 三星Launcher的主页只在屏幕的上半部显示
- camera的界面只显示在了屏幕的右上方
- 输入法界面也显示不正常
- 常用的应用如计算器、收音机、智能管理器。。。好多应用布局都有问题。
看来还是google原生系统处理得比较好,系在统重启之后,还没有找到有问题的布局(bootanimation会显示不正常)。