blob: c57dfb676a7e0f383051121787773b45af38822d [file] [log] [blame]
/*
* 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();
}
}