/* * Copyright (C) 2014 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.server.wm; import static com.android.server.wm.WindowManagerDebugConfig.DEBUG_SURFACE_TRACE; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WITH_CLASS_NAME; import static com.android.server.wm.WindowManagerDebugConfig.TAG_WM; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.Paint; import android.graphics.PixelFormat; import android.graphics.Point; import android.graphics.PorterDuff; import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.util.Slog; import android.view.Display; import android.view.Surface; import android.view.Surface.OutOfResourcesException; import android.view.SurfaceControl; import android.view.SurfaceSession; class CircularDisplayMask { private static final String TAG = TAG_WITH_CLASS_NAME ? "CircularDisplayMask" : TAG_WM; // size of the chin private int mScreenOffset = 0; // Display dimensions private Point mScreenSize; private final SurfaceControl mSurfaceControl; private final Surface mSurface = new Surface(); private int mLastDW; private int mLastDH; private boolean mDrawNeeded; private Paint mPaint; private int mRotation; private boolean mVisible; private boolean mDimensionsUnequal = false; private int mMaskThickness; public CircularDisplayMask(Display display, SurfaceSession session, int zOrder, int screenOffset, int maskThickness) { mScreenSize = new Point(); display.getSize(mScreenSize); if (mScreenSize.x != mScreenSize.y + screenOffset) { Slog.w(TAG, "Screen dimensions of displayId = " + display.getDisplayId() + "are not equal, circularMask will not be drawn."); mDimensionsUnequal = true; } SurfaceControl ctrl = null; try { if (DEBUG_SURFACE_TRACE) { ctrl = new WindowSurfaceController.SurfaceTrace(session, "CircularDisplayMask", mScreenSize.x, mScreenSize.y, PixelFormat.TRANSLUCENT, SurfaceControl.HIDDEN); } else { ctrl = new SurfaceControl(session, "CircularDisplayMask", mScreenSize.x, mScreenSize.y, PixelFormat.TRANSLUCENT, SurfaceControl.HIDDEN); } ctrl.setLayerStack(display.getLayerStack()); ctrl.setLayer(zOrder); ctrl.setPosition(0, 0); ctrl.show(); mSurface.copyFrom(ctrl); } catch (OutOfResourcesException e) { } mSurfaceControl = ctrl; mDrawNeeded = true; mPaint = new Paint(); mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_IN)); mScreenOffset = screenOffset; mMaskThickness = maskThickness; } static private double distanceFromCenterSquared(double x, double y) { return x*x + y*y; } static private double distanceFromCenter(double x, double y) { return Math.sqrt(distanceFromCenterSquared(x, y)); } static private double verticalLineIntersectsCircle(double x, double radius) { return Math.sqrt(radius*radius - x*x); } static private double horizontalLineIntersectsCircle(double y, double radius) { return Math.sqrt(radius*radius - y*y); } static private double triangleArea(double width, double height) { return width * height / 2.0; } static private double trapezoidArea(double width, double height1, double height2) { return width * (height1 + height2) / 2.0; } static private double areaUnderChord(double radius, double chordLength) { double isocelesHeight = Math.sqrt(radius*radius - chordLength * chordLength / 4.0); double areaUnderIsoceles = isocelesHeight * chordLength / 2.0; double halfAngle = Math.asin(chordLength / (2.0 * radius)); double areaUnderArc = halfAngle * radius * radius; return areaUnderArc - triangleArea(chordLength, isocelesHeight); } // Returns the fraction of the pixel at (px, py) covered by // the circle with center (cx, cy) and radius 'radius' static private double calcPixelShading(double cx, double cy, double px, double py, double radius) { // Translate so the center is at the origin px -= cx; py -= cy; // Reflect across the axis so the point is in the first quadrant px = Math.abs(px); py = Math.abs(py); // One more transformation which simplifies the logic later if (py > px) { double temp; temp = px; px = py; py = temp; } double left = px - 0.5; double right = px + 0.5; double bottom = py - 0.5; double top = py + 0.5; if (distanceFromCenterSquared(left, bottom) > radius*radius) { return 0.0; } if (distanceFromCenterSquared(right, top) < radius*radius) { return 1.0; } // Check if only the bottom-left corner of the pixel is inside the circle if (distanceFromCenterSquared(left, top) > radius*radius) { double triangleWidth = horizontalLineIntersectsCircle(bottom, radius) - left; double triangleHeight = verticalLineIntersectsCircle(left, radius) - bottom; double chordLength = distanceFromCenter(triangleWidth, triangleHeight); return triangleArea(triangleWidth, triangleHeight) + areaUnderChord(radius, chordLength); } // Check if only the top-right corner of the pixel is outside the circle if (distanceFromCenterSquared(right, bottom) < radius*radius) { double triangleWidth = right - horizontalLineIntersectsCircle(top, radius); double triangleHeight = top - verticalLineIntersectsCircle(right, radius); double chordLength = distanceFromCenter(triangleWidth, triangleHeight); return 1 - triangleArea(triangleWidth, triangleHeight) + areaUnderChord(radius, chordLength); } // It must be that the top-left and bottom-left corners are inside the circle double trapezoidWidth1 = horizontalLineIntersectsCircle(top, radius) - left; double trapezoidWidth2 = horizontalLineIntersectsCircle(bottom, radius) - left; double chordLength = distanceFromCenter(1, trapezoidWidth2 - trapezoidWidth1); double shading = trapezoidArea(1.0, trapezoidWidth1, trapezoidWidth2) + areaUnderChord(radius, chordLength); // When top >= 0 and bottom <= 0 it's possible for the circle to intersect the pixel 4 times. // If so, remove the area of the section which crosses the right-hand edge. if (top >= 0 && bottom <= 0 && radius > right) { shading -= areaUnderChord(radius, 2 * verticalLineIntersectsCircle(right, radius)); } return shading; } private void drawIfNeeded() { if (!mDrawNeeded || !mVisible || mDimensionsUnequal) { return; } mDrawNeeded = false; Rect dirty = new Rect(0, 0, mScreenSize.x, mScreenSize.y); Canvas c = null; try { c = mSurface.lockCanvas(dirty); } catch (IllegalArgumentException e) { } catch (Surface.OutOfResourcesException e) { } if (c == null) { return; } switch (mRotation) { case Surface.ROTATION_0: case Surface.ROTATION_90: // chin bottom or right mSurfaceControl.setPosition(0, 0); break; case Surface.ROTATION_180: // chin top mSurfaceControl.setPosition(0, -mScreenOffset); break; case Surface.ROTATION_270: // chin left mSurfaceControl.setPosition(-mScreenOffset, 0); break; } c.drawColor(Color.BLACK); int maskWidth = mScreenSize.x - 2*mMaskThickness; int maskHeight; // Don't render the whole mask if it is partly offscreen. if (maskWidth > mScreenSize.y) { maskHeight = mScreenSize.y; } else { // To ensure the mask can be properly centered on the canvas the // bitmap dimensions must have the same parity as those of the canvas. maskHeight = mScreenSize.y - ((mScreenSize.y - maskWidth) & ~1); } double cx = (maskWidth - 1.0) / 2.0; double cy = (maskHeight - 1.0 + mScreenOffset) / 2.0; double radius = maskWidth / 2.0; int[] pixels = new int[maskWidth * maskHeight]; for (int py=0; py<maskHeight; py++) { for (int px=0; px<maskWidth; px++) { double shading = calcPixelShading(cx, cy, px, py, radius); pixels[maskWidth*py + px] = Color.argb(255 - (int)Math.round(255.0*shading), 0, 0, 0); } } Bitmap transparency = Bitmap.createBitmap(pixels, maskWidth, maskHeight, Bitmap.Config.ARGB_8888); c.drawBitmap(transparency, (float)mMaskThickness, (float)((mScreenSize.y - maskHeight) / 2), mPaint); mSurface.unlockCanvasAndPost(c); } // Note: caller responsible for being inside // Surface.openTransaction() / closeTransaction() public void setVisibility(boolean on) { if (mSurfaceControl == null) { return; } mVisible = on; drawIfNeeded(); if (on) { mSurfaceControl.show(); } else { mSurfaceControl.hide(); } } void positionSurface(int dw, int dh, int rotation) { if (mLastDW == dw && mLastDH == dh && mRotation == rotation) { return; } mLastDW = dw; mLastDH = dh; mDrawNeeded = true; mRotation = rotation; drawIfNeeded(); } }