Adding the 'non client decor view' to free floating windows
This patch adds a new free floating window non client decor view which
has:
- Two buttons (maximize and close) as it meets our current UX design
- A Shadow which changes size with focus
- It's own view as preparation for the thread distributed drawing and
user interaction handling (resize handles, etc)
Missing is at this point:
- The button functionality itself.
- Changing/removing decors based on window type (once the needed
patches land.
bug: 21738666
bug: 21739152
Change-Id: I6e41f2a7470256cee7bae35029d7e8c43fb235dd
diff --git a/core/java/com/android/internal/policy/PhoneWindow.java b/core/java/com/android/internal/policy/PhoneWindow.java
index 2b39528..0bd8d85 100644
--- a/core/java/com/android/internal/policy/PhoneWindow.java
+++ b/core/java/com/android/internal/policy/PhoneWindow.java
@@ -72,6 +72,7 @@
import com.android.internal.widget.BackgroundFallback;
import com.android.internal.widget.DecorContentParent;
import com.android.internal.widget.FloatingToolbar;
+import com.android.internal.widget.NonClientDecorView;
import com.android.internal.widget.SwipeDismissLayout;
import android.app.ActivityManager;
@@ -1270,7 +1271,7 @@
* @param st The panel being initialized.
*/
protected boolean initializePanelDecor(PanelFeatureState st) {
- st.decorView = new DecorView(getContext(), st.featureId);
+ st.decorView = generateDecor(st.featureId);
st.gravity = Gravity.CENTER | Gravity.BOTTOM;
st.setStyle(getContext());
TypedArray a = getContext().obtainStyledAttributes(null,
@@ -2204,6 +2205,9 @@
private final Rect mFrameOffsets = new Rect();
+ // True if a non client area decor exists.
+ private boolean mHasNonClientDecor = false;
+
private boolean mChanging;
private Drawable mMenuBackground;
@@ -3148,53 +3152,59 @@
return;
}
- setPadding(mFramePadding.left + mBackgroundPadding.left, mFramePadding.top
- + mBackgroundPadding.top, mFramePadding.right + mBackgroundPadding.right,
+ setPadding(mFramePadding.left + mBackgroundPadding.left,
+ mFramePadding.top + mBackgroundPadding.top,
+ mFramePadding.right + mBackgroundPadding.right,
mFramePadding.bottom + mBackgroundPadding.bottom);
requestLayout();
invalidate();
int opacity = PixelFormat.OPAQUE;
- // Note: if there is no background, we will assume opaque. The
- // common case seems to be that an application sets there to be
- // no background so it can draw everything itself. For that,
- // we would like to assume OPAQUE and let the app force it to
- // the slower TRANSLUCENT mode if that is really what it wants.
- Drawable bg = getBackground();
- Drawable fg = getForeground();
- if (bg != null) {
- if (fg == null) {
- opacity = bg.getOpacity();
- } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0
- && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) {
- // If the frame padding is zero, then we can be opaque
- // if either the frame -or- the background is opaque.
- int fop = fg.getOpacity();
- int bop = bg.getOpacity();
- if (false)
- Log.v(TAG, "Background opacity: " + bop + ", Frame opacity: " + fop);
- if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) {
- opacity = PixelFormat.OPAQUE;
- } else if (fop == PixelFormat.UNKNOWN) {
- opacity = bop;
- } else if (bop == PixelFormat.UNKNOWN) {
- opacity = fop;
+ if (windowHasShadow()) {
+ // If the window has a shadow, it must be translucent.
+ opacity = PixelFormat.TRANSLUCENT;
+ } else{
+ // Note: If there is no background, we will assume opaque. The
+ // common case seems to be that an application sets there to be
+ // no background so it can draw everything itself. For that,
+ // we would like to assume OPAQUE and let the app force it to
+ // the slower TRANSLUCENT mode if that is really what it wants.
+ Drawable bg = getBackground();
+ Drawable fg = getForeground();
+ if (bg != null) {
+ if (fg == null) {
+ opacity = bg.getOpacity();
+ } else if (mFramePadding.left <= 0 && mFramePadding.top <= 0
+ && mFramePadding.right <= 0 && mFramePadding.bottom <= 0) {
+ // If the frame padding is zero, then we can be opaque
+ // if either the frame -or- the background is opaque.
+ int fop = fg.getOpacity();
+ int bop = bg.getOpacity();
+ if (false)
+ Log.v(TAG, "Background opacity: " + bop + ", Frame opacity: " + fop);
+ if (fop == PixelFormat.OPAQUE || bop == PixelFormat.OPAQUE) {
+ opacity = PixelFormat.OPAQUE;
+ } else if (fop == PixelFormat.UNKNOWN) {
+ opacity = bop;
+ } else if (bop == PixelFormat.UNKNOWN) {
+ opacity = fop;
+ } else {
+ opacity = Drawable.resolveOpacity(fop, bop);
+ }
} else {
- opacity = Drawable.resolveOpacity(fop, bop);
+ // For now we have to assume translucent if there is a
+ // frame with padding... there is no way to tell if the
+ // frame and background together will draw all pixels.
+ if (false)
+ Log.v(TAG, "Padding: " + mFramePadding);
+ opacity = PixelFormat.TRANSLUCENT;
}
- } else {
- // For now we have to assume translucent if there is a
- // frame with padding... there is no way to tell if the
- // frame and background together will draw all pixels.
- if (false)
- Log.v(TAG, "Padding: " + mFramePadding);
- opacity = PixelFormat.TRANSLUCENT;
}
+ if (false)
+ Log.v(TAG, "Background: " + bg + ", Frame: " + fg);
}
if (false)
- Log.v(TAG, "Background: " + bg + ", Frame: " + fg);
- if (false)
Log.v(TAG, "Selected default opacity: " + opacity);
mDefaultOpacity = opacity;
@@ -3401,8 +3411,7 @@
}
};
} else {
- ViewStub stub = (ViewStub) findViewById(
- R.id.action_mode_bar_stub);
+ ViewStub stub = (ViewStub) findViewById(R.id.action_mode_bar_stub);
if (stub != null) {
mPrimaryActionModeView = (ActionBarContextView) stub.inflate();
}
@@ -3490,6 +3499,28 @@
.addOnPreDrawListener(mFloatingToolbarPreDrawListener);
}
+ // Set when the window is free floating and a non client decor frame was added.
+ void enableNonClientDecor(boolean enable) {
+ if (mHasNonClientDecor != enable) {
+ mHasNonClientDecor = enable;
+ if (getForeground() != null) {
+ drawableChanged();
+ }
+ }
+ }
+
+ // Returns true if the window has a non client decor.
+ private boolean windowHasNonClientDecor() {
+ return mHasNonClientDecor;
+ }
+
+ // Returns true if the Window is free floating and has a shadow. Note that non overlapping
+ // windows do not have a shadow since it could not be seen anyways (a small screen / tablet
+ // "tiles" the windows side by side but does not overlap them).
+ private boolean windowHasShadow() {
+ return windowHasNonClientDecor() && getElevation() > 0;
+ }
+
/**
* Clears out internal references when the action mode is destroyed.
*/
@@ -3599,8 +3630,8 @@
}
}
- protected DecorView generateDecor() {
- return new DecorView(getContext(), -1);
+ protected DecorView generateDecor(int featureId) {
+ return new DecorView(getContext(), featureId);
}
protected void setFeatureFromAttrs(int featureId, TypedArray attrs,
@@ -3879,8 +3910,15 @@
mDecor.startChanging();
+ NonClientDecorView nonClientDecorView = createNonClientDecorView();
View in = mLayoutInflater.inflate(layoutResource, null);
- decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ if (nonClientDecorView != null) {
+ decor.addView(nonClientDecorView,
+ new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ nonClientDecorView.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ } else {
+ decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
+ }
mContentRoot = (ViewGroup) in;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
@@ -3936,6 +3974,54 @@
return contentParent;
}
+ // Free floating overlapping windows require a non client decor with a caption and shadow..
+ private NonClientDecorView createNonClientDecorView() {
+ boolean needsDecor = true;
+ NonClientDecorView nonClientDecorView = null;
+
+ final WindowManager.LayoutParams attrs = getAttributes();
+ // TODO(skuhne): Use the associated stack to figure out if the window is on the free style
+ // desktop, the side by side desktop or the full screen desktop. With that informations the
+ // choice is fairly easy to decide.
+ // => This is only a kludge for now to suppress fullscreen windows, recents, launcher, etc..
+ boolean isFullscreen =
+ 0 != ((mDecor.getWindowSystemUiVisibility() | mDecor.getSystemUiVisibility()) &
+ (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION));
+ boolean isApplication = attrs.type != TYPE_BASE_APPLICATION &&
+ attrs.type != TYPE_APPLICATION;
+
+ // We do not show the non client decor if...
+ // - this is a floating dialog (which is not a real window, e.g. it cannot be maximized).
+ // - it is not an application (special windows have special functions, e.g text selector).
+ // - the application is full screen, drawing everything (since the decor would be out of the
+ // screen in that case and could not be seen).
+ if (isFloating() || isFullscreen || isApplication) {
+ needsDecor = false;
+ }
+
+ if (needsDecor) {
+ // TODO(skuhne): If running in side by side mode on a device - turn off the shadow.
+ boolean windowHasShadow = true;
+ // Dependent on the brightness of the used title we either use the
+ // dark or the light button frame.
+ TypedValue value = new TypedValue();
+ getContext().getTheme().resolveAttribute(R.attr.colorPrimary, value, true);
+ if (Color.brightness(value.data) < 0.5) {
+ nonClientDecorView = (NonClientDecorView) mLayoutInflater.inflate(
+ R.layout.non_client_decor_dark, null);
+ } else {
+ nonClientDecorView = (NonClientDecorView) mLayoutInflater.inflate(
+ R.layout.non_client_decor_light, null);
+ }
+ nonClientDecorView.setPhoneWindow(this, windowHasShadow);
+ }
+
+ // Tell the Decor if it has a non client decor.
+ mDecor.enableNonClientDecor(needsDecor);
+
+ return nonClientDecorView;
+ }
+
/** @hide */
public void alwaysReadCloseOnTouchAttr() {
mAlwaysReadCloseOnTouchAttr = true;
@@ -3943,7 +4029,7 @@
private void installDecor() {
if (mDecor == null) {
- mDecor = generateDecor();
+ mDecor = generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
diff --git a/core/java/com/android/internal/widget/NonClientDecorView.java b/core/java/com/android/internal/widget/NonClientDecorView.java
new file mode 100644
index 0000000..57039b7
--- /dev/null
+++ b/core/java/com/android/internal/widget/NonClientDecorView.java
@@ -0,0 +1,219 @@
+/*
+ * Copyright (C) 2015 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.internal.widget;
+
+import android.app.ActivityManager;
+import android.app.ActivityManager.RunningTaskInfo;
+import android.app.ActivityManagerNative;
+import android.content.Context;
+import android.os.RemoteException;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.util.Log;
+import android.util.TypedValue;
+
+import android.view.ViewOutlineProvider;
+import android.view.WindowInsets;
+import com.android.internal.R;
+import com.android.internal.policy.PhoneWindow;
+
+import java.util.List;
+
+/**
+ * This class represents the special screen elements to control a window on free form
+ * environment. All thse screen elements are added in the "non client area" which is the area of
+ * the window which is handled by the OS and not the application.
+ * As such this class handles the following things:
+ * <ul>
+ * <li>The caption, containing the system buttons like maximize, close and such as well as
+ * allowing the user to drag the window around.</li>
+ * <li>The shadow - which is changing dependent on the window focus.</li>
+ * <li>The border around the client area (if there is one).</li>
+ * <li>The resize handles which allow to resize the window.</li>
+ * </ul>
+ * After creating the view, the function
+ * {@link #setPhoneWindow(PhoneWindow owner, boolean windowHasShadow)} needs to be called to make
+ * the connection to it's owning PhoneWindow.
+ * Note: At this time the application can change various attributes of the DecorView which
+ * will break things (in settle/unexpected ways):
+ * <ul>
+ * <li>setElevation</li>
+ * <li>setOutlineProvider</li>
+ * <li>setSurfaceFormat</li>
+ * <li>..</li>
+ * </ul>
+ * This will be mitigated once b/22527834 will be addressed.
+ */
+public class NonClientDecorView extends ViewGroup implements View.OnClickListener {
+ private final static String TAG = "NonClientDecorView";
+ // The height of a window which has focus in DIP.
+ private final int DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP = 20;
+ // The height of a window which has not in DIP.
+ private final int DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP = 5;
+
+ private PhoneWindow mOwner = null;
+ boolean mWindowHasShadow = false;
+
+ // The current focus state of the window for updating the window elevation.
+ boolean mWindowHasFocus = true;
+
+ // Cludge to address b/22668382: Set the shadow size to the maximum so that the layer
+ // size calculation takes the shadow size into account. We set the elevation currently
+ // to max until the first layout command has been executed.
+ boolean mAllowUpdateElevation = false;
+
+ public NonClientDecorView(Context context) {
+ super(context);
+ }
+
+ public NonClientDecorView(Context context, AttributeSet attrs) {
+ super(context, attrs);
+ }
+
+ public NonClientDecorView(Context context, AttributeSet attrs, int defStyle) {
+ super(context, attrs, defStyle);
+ }
+
+ public void setPhoneWindow(PhoneWindow owner, boolean windowHasShadow) {
+ mOwner = owner;
+ mWindowHasShadow = windowHasShadow;
+ if (mWindowHasShadow) {
+ // TODO(skuhne): Call setMaxElevation here as soon as b/22668382 got fixed.
+ updateElevation();
+ }
+ // By changing the outline provider to BOUNDS, the window can remove its
+ // background without removing the shadow.
+ mOwner.getDecorView().setOutlineProvider(ViewOutlineProvider.BOUNDS);
+ findViewById(R.id.maximize_window).setOnClickListener(this);
+ findViewById(R.id.close_window).setOnClickListener(this);
+ }
+
+ @Override
+ public void onClick(View view) {
+ if (view.getId() == R.id.maximize_window) {
+ // TODO(skuhne): Add code to maximize window.
+ } else if (view.getId() == R.id.close_window) {
+ // TODO(skuhne): This is not the right way to kill an app and we should add a high level
+ // function for it.
+ final ActivityManager m =
+ (ActivityManager) getContext().getSystemService(Context.ACTIVITY_SERVICE);
+ List<RunningTaskInfo> runningTaskInfoList = m.getRunningTasks(1);
+ if (!runningTaskInfoList.isEmpty()) {
+ try {
+ ActivityManagerNative.getDefault().removeTask(runningTaskInfoList.get(0).id);
+ } catch (RemoteException ex) {
+ Log.e(TAG, "Couldn't close task with the close button.");
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onWindowFocusChanged(boolean hasWindowFocus) {
+ mWindowHasFocus = hasWindowFocus;
+ updateElevation();
+ super.onWindowFocusChanged(hasWindowFocus);
+ }
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ measureChildren(widthMeasureSpec, heightMeasureSpec);
+ final int width = MeasureSpec.getSize(widthMeasureSpec);
+ final int height = MeasureSpec.getSize(heightMeasureSpec);
+ setMeasuredDimension(width, height);
+ }
+
+ @Override
+ protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+ // The system inset needs only to be applied to the caption. The client area of
+ // the window will automatically be adjusted by the the DecorView.
+ WindowInsets insets = getRootWindowInsets();
+ int systemMargin = insets.getSystemWindowInsetTop();
+
+ final int leftPos = getPaddingLeft();
+ final int rightPos = right - left - getPaddingRight();
+ final int topPos = getPaddingTop();
+ final int bottomPos = bottom - top - getPaddingBottom();
+
+ // On top we have the caption which has to fill left to right with a fixed height.
+ final int width = rightPos - leftPos;
+ final View caption = getChildAt(0);
+
+ // If the application changed its SystemUI metrics, we might also have to adapt
+ // our shadow elevation.
+ updateElevation();
+ mAllowUpdateElevation = true;
+
+ // Remove the decor temporarily if the window entered a full screen/immersive mode.
+ final int captionHeight = isFillingScreen() ? 0 : caption.getMeasuredHeight();
+ caption.layout(leftPos, topPos + systemMargin, leftPos + width,
+ topPos + systemMargin + captionHeight);
+
+ // Note: We should never have more then 1 additional item in here.
+ if (getChildCount() > 1) {
+ getChildAt(1).layout(leftPos, topPos + captionHeight, leftPos + width, bottomPos);
+ }
+ }
+
+ // Make sure that we never get more then one client area in our view.
+ @Override
+ public void addView(View child, int index, LayoutParams params) {
+ if (index >= 2 || getChildCount() >= 2) {
+ throw new IllegalStateException("NonClientDecorView can only handle 1 client view");
+ }
+ super.addView(child, index, params);
+ }
+
+ // Returns true when the window is filling the entire screen and the non client area
+ // should not be shown.
+ private boolean isFillingScreen() {
+ return (0 != ((getWindowSystemUiVisibility() | getSystemUiVisibility()) &
+ (View.SYSTEM_UI_FLAG_FULLSCREEN | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
+ View.SYSTEM_UI_FLAG_IMMERSIVE | View.SYSTEM_UI_FLAG_LOW_PROFILE)));
+ }
+
+ // The shadow height gets controlled by the focus to visualize highlighted windows.
+ // Note: This will overwrite application elevation properties.
+ // Note: Windows which have (temporarily) changed their attributes to cover the SystemUI
+ // will get no shadow as they are expected to be "full screen".
+ private void updateElevation() {
+ float elevation = 0;
+ if (mWindowHasShadow) {
+ boolean fill = isFillingScreen();
+ elevation = fill ? 0 :
+ (mWindowHasFocus ? DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP :
+ DECOR_SHADOW_UNFOCUSED_HEIGHT_IN_DIP);
+ if (!mAllowUpdateElevation && !fill) {
+ // TODO(skuhne): Change this to setMaxElevation as soon as b/22668382 got fixed
+ // and remove this cludge.
+ elevation = DECOR_SHADOW_FOCUSED_HEIGHT_IN_DIP;
+ }
+ // Convert the DP elevation into physical pixels.
+ elevation = dipToPx(elevation);
+ }
+ // Don't change the elevation if it didn't change since it can require some time.
+ if (mOwner.getDecorView().getElevation() != elevation) {
+ mOwner.setElevation(elevation);
+ }
+ }
+
+ private float dipToPx(float dip) {
+ return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, dip,
+ getResources().getDisplayMetrics());
+ }
+}