Home | History | Annotate | Download | only in statusbar
      1 /*
      2  * Copyright (C) 2014 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License
     15  */
     16 
     17 package com.android.systemui.statusbar;
     18 
     19 import android.content.Context;
     20 import android.content.res.Resources;
     21 import android.graphics.Canvas;
     22 import android.graphics.Outline;
     23 import android.graphics.Path;
     24 import android.graphics.Rect;
     25 import android.graphics.RectF;
     26 import android.util.AttributeSet;
     27 import android.view.View;
     28 import android.view.ViewOutlineProvider;
     29 
     30 import com.android.settingslib.Utils;
     31 import com.android.systemui.R;
     32 import com.android.systemui.statusbar.notification.AnimatableProperty;
     33 import com.android.systemui.statusbar.notification.PropertyAnimator;
     34 import com.android.systemui.statusbar.stack.AnimationProperties;
     35 import com.android.systemui.statusbar.stack.StackStateAnimator;
     36 
     37 /**
     38  * Like {@link ExpandableView}, but setting an outline for the height and clipping.
     39  */
     40 public abstract class ExpandableOutlineView extends ExpandableView {
     41 
     42     private static final AnimatableProperty TOP_ROUNDNESS = AnimatableProperty.from(
     43             "topRoundness",
     44             ExpandableOutlineView::setTopRoundnessInternal,
     45             ExpandableOutlineView::getCurrentTopRoundness,
     46             R.id.top_roundess_animator_tag,
     47             R.id.top_roundess_animator_end_tag,
     48             R.id.top_roundess_animator_start_tag);
     49     private static final AnimatableProperty BOTTOM_ROUNDNESS = AnimatableProperty.from(
     50             "bottomRoundness",
     51             ExpandableOutlineView::setBottomRoundnessInternal,
     52             ExpandableOutlineView::getCurrentBottomRoundness,
     53             R.id.bottom_roundess_animator_tag,
     54             R.id.bottom_roundess_animator_end_tag,
     55             R.id.bottom_roundess_animator_start_tag);
     56     private static final AnimationProperties ROUNDNESS_PROPERTIES =
     57             new AnimationProperties().setDuration(StackStateAnimator.ANIMATION_DURATION_STANDARD);
     58     private static final Path EMPTY_PATH = new Path();
     59 
     60     private final Rect mOutlineRect = new Rect();
     61     private final Path mClipPath = new Path();
     62     private boolean mCustomOutline;
     63     private float mOutlineAlpha = -1f;
     64     protected float mOutlineRadius;
     65     private boolean mAlwaysRoundBothCorners;
     66     private Path mTmpPath = new Path();
     67     private Path mTmpPath2 = new Path();
     68     private float mCurrentBottomRoundness;
     69     private float mCurrentTopRoundness;
     70     private float mBottomRoundness;
     71     private float mTopRoundness;
     72     private int mBackgroundTop;
     73 
     74     /**
     75      * {@code true} if the children views of the {@link ExpandableOutlineView} are translated when
     76      * it is moved. Otherwise, the translation is set on the {@code ExpandableOutlineView} itself.
     77      */
     78     protected boolean mShouldTranslateContents;
     79     private boolean mTopAmountRounded;
     80     private float mDistanceToTopRoundness = -1;
     81     private float mExtraWidthForClipping;
     82     private int mMinimumHeightForClipping = 0;
     83 
     84     private final ViewOutlineProvider mProvider = new ViewOutlineProvider() {
     85         @Override
     86         public void getOutline(View view, Outline outline) {
     87             if (!mCustomOutline && mCurrentTopRoundness == 0.0f
     88                     && mCurrentBottomRoundness == 0.0f && !mAlwaysRoundBothCorners
     89                     && !mTopAmountRounded) {
     90                 int translation = mShouldTranslateContents ? (int) getTranslation() : 0;
     91                 int left = Math.max(translation, 0);
     92                 int top = mClipTopAmount + mBackgroundTop;
     93                 int right = getWidth() + Math.min(translation, 0);
     94                 int bottom = Math.max(getActualHeight() - mClipBottomAmount, top);
     95                 outline.setRect(left, top, right, bottom);
     96             } else {
     97                 Path clipPath = getClipPath();
     98                 if (clipPath != null && clipPath.isConvex()) {
     99                     // The path might not be convex in border cases where the view is small and
    100                     // clipped
    101                     outline.setConvexPath(clipPath);
    102                 }
    103             }
    104             outline.setAlpha(mOutlineAlpha);
    105         }
    106     };
    107 
    108     private Path getClipPath() {
    109         return getClipPath(false, /* ignoreTranslation */
    110                 false /* clipRoundedToBottom */);
    111     }
    112 
    113     protected Path getClipPath(boolean ignoreTranslation, boolean clipRoundedToBottom) {
    114         int left;
    115         int top;
    116         int right;
    117         int bottom;
    118         int height;
    119         Path intersectPath = null;
    120         if (!mCustomOutline) {
    121             int translation = mShouldTranslateContents && !ignoreTranslation
    122                     ? (int) getTranslation() : 0;
    123             left = Math.max(translation, 0);
    124             top = mClipTopAmount + mBackgroundTop;
    125             right = getWidth() + Math.min(translation, 0);
    126             bottom = Math.max(getActualHeight(), top);
    127             int intersectBottom = Math.max(getActualHeight() - mClipBottomAmount, top);
    128             if (bottom != intersectBottom) {
    129                 if (clipRoundedToBottom) {
    130                     bottom = intersectBottom;
    131                 } else {
    132                     getRoundedRectPath(left, top, right,
    133                             intersectBottom, 0.0f,
    134                             0.0f, mTmpPath2);
    135                     intersectPath = mTmpPath2;
    136                 }
    137             }
    138         } else {
    139             left = mOutlineRect.left;
    140             top = mOutlineRect.top;
    141             right = mOutlineRect.right;
    142             bottom = mOutlineRect.bottom;
    143         }
    144         height = bottom - top;
    145         if (height == 0) {
    146             return EMPTY_PATH;
    147         }
    148         float topRoundness = mAlwaysRoundBothCorners
    149                 ? mOutlineRadius : getCurrentBackgroundRadiusTop();
    150         float bottomRoundness = mAlwaysRoundBothCorners
    151                 ? mOutlineRadius : getCurrentBackgroundRadiusBottom();
    152         if (topRoundness + bottomRoundness > height) {
    153             float overShoot = topRoundness + bottomRoundness - height;
    154             topRoundness -= overShoot * mCurrentTopRoundness
    155                     / (mCurrentTopRoundness + mCurrentBottomRoundness);
    156             bottomRoundness -= overShoot * mCurrentBottomRoundness
    157                     / (mCurrentTopRoundness + mCurrentBottomRoundness);
    158         }
    159         getRoundedRectPath(left, top, right, bottom, topRoundness,
    160                 bottomRoundness, mTmpPath);
    161         Path roundedRectPath = mTmpPath;
    162         if (intersectPath != null) {
    163             roundedRectPath.op(intersectPath, Path.Op.INTERSECT);
    164         }
    165         return roundedRectPath;
    166     }
    167 
    168     public static void getRoundedRectPath(int left, int top, int right, int bottom,
    169             float topRoundness, float bottomRoundness, Path outPath) {
    170         outPath.reset();
    171         int width = right - left;
    172         float topRoundnessX = topRoundness;
    173         float bottomRoundnessX = bottomRoundness;
    174         topRoundnessX = Math.min(width / 2, topRoundnessX);
    175         bottomRoundnessX = Math.min(width / 2, bottomRoundnessX);
    176         if (topRoundness > 0.0f) {
    177             outPath.moveTo(left, top + topRoundness);
    178             outPath.quadTo(left, top, left + topRoundnessX, top);
    179             outPath.lineTo(right - topRoundnessX, top);
    180             outPath.quadTo(right, top, right, top + topRoundness);
    181         } else {
    182             outPath.moveTo(left, top);
    183             outPath.lineTo(right, top);
    184         }
    185         if (bottomRoundness > 0.0f) {
    186             outPath.lineTo(right, bottom - bottomRoundness);
    187             outPath.quadTo(right, bottom, right - bottomRoundnessX, bottom);
    188             outPath.lineTo(left + bottomRoundnessX, bottom);
    189             outPath.quadTo(left, bottom, left, bottom - bottomRoundness);
    190         } else {
    191             outPath.lineTo(right, bottom);
    192             outPath.lineTo(left, bottom);
    193         }
    194         outPath.close();
    195     }
    196 
    197     public ExpandableOutlineView(Context context, AttributeSet attrs) {
    198         super(context, attrs);
    199         setOutlineProvider(mProvider);
    200         initDimens();
    201     }
    202 
    203     @Override
    204     protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
    205         canvas.save();
    206         Path intersectPath = null;
    207         if (mTopAmountRounded && topAmountNeedsClipping()) {
    208             int left = (int) (- mExtraWidthForClipping / 2.0f);
    209             int top = (int) (mClipTopAmount - mDistanceToTopRoundness);
    210             int right = getWidth() + (int) (mExtraWidthForClipping + left);
    211             int bottom = (int) Math.max(mMinimumHeightForClipping,
    212                     Math.max(getActualHeight() - mClipBottomAmount, top + mOutlineRadius));
    213             ExpandableOutlineView.getRoundedRectPath(left, top, right, bottom, mOutlineRadius,
    214                     0.0f,
    215                     mClipPath);
    216             intersectPath = mClipPath;
    217         }
    218         boolean clipped = false;
    219         if (childNeedsClipping(child)) {
    220             Path clipPath = getCustomClipPath(child);
    221             if (clipPath == null) {
    222                 clipPath = getClipPath();
    223             }
    224             if (clipPath != null) {
    225                 if (intersectPath != null) {
    226                     clipPath.op(intersectPath, Path.Op.INTERSECT);
    227                 }
    228                 canvas.clipPath(clipPath);
    229                 clipped = true;
    230             }
    231         }
    232         if (!clipped && intersectPath != null) {
    233             canvas.clipPath(intersectPath);
    234         }
    235         boolean result = super.drawChild(canvas, child, drawingTime);
    236         canvas.restore();
    237         return result;
    238     }
    239 
    240     public void setExtraWidthForClipping(float extraWidthForClipping) {
    241         mExtraWidthForClipping = extraWidthForClipping;
    242     }
    243 
    244     public void setMinimumHeightForClipping(int minimumHeightForClipping) {
    245         mMinimumHeightForClipping = minimumHeightForClipping;
    246     }
    247 
    248     @Override
    249     public void setDistanceToTopRoundness(float distanceToTopRoundness) {
    250         super.setDistanceToTopRoundness(distanceToTopRoundness);
    251         if (distanceToTopRoundness != mDistanceToTopRoundness) {
    252             mTopAmountRounded = distanceToTopRoundness >= 0;
    253             mDistanceToTopRoundness = distanceToTopRoundness;
    254             applyRoundness();
    255         }
    256     }
    257 
    258     protected boolean childNeedsClipping(View child) {
    259         return false;
    260     }
    261 
    262     public boolean topAmountNeedsClipping() {
    263         return true;
    264     }
    265 
    266     protected boolean isClippingNeeded() {
    267         return mAlwaysRoundBothCorners || mCustomOutline || getTranslation() != 0 ;
    268     }
    269 
    270     private void initDimens() {
    271         Resources res = getResources();
    272         mShouldTranslateContents =
    273                 res.getBoolean(R.bool.config_translateNotificationContentsOnSwipe);
    274         mOutlineRadius = res.getDimension(R.dimen.notification_shadow_radius);
    275         mAlwaysRoundBothCorners = res.getBoolean(R.bool.config_clipNotificationsToOutline);
    276         if (!mAlwaysRoundBothCorners) {
    277             mOutlineRadius = res.getDimensionPixelSize(
    278                     Utils.getThemeAttr(mContext, android.R.attr.dialogCornerRadius));
    279         }
    280         setClipToOutline(mAlwaysRoundBothCorners);
    281     }
    282 
    283     /**
    284      * Set the topRoundness of this view.
    285      * @return Whether the roundness was changed.
    286      */
    287     public boolean setTopRoundness(float topRoundness, boolean animate) {
    288         if (mTopRoundness != topRoundness) {
    289             mTopRoundness = topRoundness;
    290             PropertyAnimator.setProperty(this, TOP_ROUNDNESS, topRoundness,
    291                     ROUNDNESS_PROPERTIES, animate);
    292             return true;
    293         }
    294         return false;
    295     }
    296 
    297     protected void applyRoundness() {
    298         invalidateOutline();
    299         invalidate();
    300     }
    301 
    302     public float getCurrentBackgroundRadiusTop() {
    303         // If this view is top amount notification view, it should always has round corners on top.
    304         // It will be applied with applyRoundness()
    305         if (mTopAmountRounded) {
    306             return mOutlineRadius;
    307         }
    308         return mCurrentTopRoundness * mOutlineRadius;
    309     }
    310 
    311     public float getCurrentTopRoundness() {
    312         return mCurrentTopRoundness;
    313     }
    314 
    315     public float getCurrentBottomRoundness() {
    316         return mCurrentBottomRoundness;
    317     }
    318 
    319     protected float getCurrentBackgroundRadiusBottom() {
    320         return mCurrentBottomRoundness * mOutlineRadius;
    321     }
    322 
    323     /**
    324      * Set the bottom roundness of this view.
    325      * @return Whether the roundness was changed.
    326      */
    327     public boolean setBottomRoundness(float bottomRoundness, boolean animate) {
    328         if (mBottomRoundness != bottomRoundness) {
    329             mBottomRoundness = bottomRoundness;
    330             PropertyAnimator.setProperty(this, BOTTOM_ROUNDNESS, bottomRoundness,
    331                     ROUNDNESS_PROPERTIES, animate);
    332             return true;
    333         }
    334         return false;
    335     }
    336 
    337     protected void setBackgroundTop(int backgroundTop) {
    338         if (mBackgroundTop != backgroundTop) {
    339             mBackgroundTop = backgroundTop;
    340             invalidateOutline();
    341         }
    342     }
    343 
    344     private void setTopRoundnessInternal(float topRoundness) {
    345         mCurrentTopRoundness = topRoundness;
    346         applyRoundness();
    347     }
    348 
    349     private void setBottomRoundnessInternal(float bottomRoundness) {
    350         mCurrentBottomRoundness = bottomRoundness;
    351         applyRoundness();
    352     }
    353 
    354     public void onDensityOrFontScaleChanged() {
    355         initDimens();
    356         applyRoundness();
    357     }
    358 
    359     @Override
    360     public void setActualHeight(int actualHeight, boolean notifyListeners) {
    361         int previousHeight = getActualHeight();
    362         super.setActualHeight(actualHeight, notifyListeners);
    363         if (previousHeight != actualHeight) {
    364             applyRoundness();
    365         }
    366     }
    367 
    368     @Override
    369     public void setClipTopAmount(int clipTopAmount) {
    370         int previousAmount = getClipTopAmount();
    371         super.setClipTopAmount(clipTopAmount);
    372         if (previousAmount != clipTopAmount) {
    373             applyRoundness();
    374         }
    375     }
    376 
    377     @Override
    378     public void setClipBottomAmount(int clipBottomAmount) {
    379         int previousAmount = getClipBottomAmount();
    380         super.setClipBottomAmount(clipBottomAmount);
    381         if (previousAmount != clipBottomAmount) {
    382             applyRoundness();
    383         }
    384     }
    385 
    386     protected void setOutlineAlpha(float alpha) {
    387         if (alpha != mOutlineAlpha) {
    388             mOutlineAlpha = alpha;
    389             applyRoundness();
    390         }
    391     }
    392 
    393     @Override
    394     public float getOutlineAlpha() {
    395         return mOutlineAlpha;
    396     }
    397 
    398     protected void setOutlineRect(RectF rect) {
    399         if (rect != null) {
    400             setOutlineRect(rect.left, rect.top, rect.right, rect.bottom);
    401         } else {
    402             mCustomOutline = false;
    403             applyRoundness();
    404         }
    405     }
    406 
    407     @Override
    408     public int getOutlineTranslation() {
    409         return mCustomOutline ? mOutlineRect.left : (int) getTranslation();
    410     }
    411 
    412     public void updateOutline() {
    413         if (mCustomOutline) {
    414             return;
    415         }
    416         boolean hasOutline = needsOutline();
    417         setOutlineProvider(hasOutline ? mProvider : null);
    418     }
    419 
    420     /**
    421      * @return Whether the view currently needs an outline. This is usually {@code false} in case
    422      * it doesn't have a background.
    423      */
    424     protected boolean needsOutline() {
    425         if (isChildInGroup()) {
    426             return isGroupExpanded() && !isGroupExpansionChanging();
    427         } else if (isSummaryWithChildren()) {
    428             return !isGroupExpanded() || isGroupExpansionChanging();
    429         }
    430         return true;
    431     }
    432 
    433     public boolean isOutlineShowing() {
    434         ViewOutlineProvider op = getOutlineProvider();
    435         return op != null;
    436     }
    437 
    438     protected void setOutlineRect(float left, float top, float right, float bottom) {
    439         mCustomOutline = true;
    440 
    441         mOutlineRect.set((int) left, (int) top, (int) right, (int) bottom);
    442 
    443         // Outlines need to be at least 1 dp
    444         mOutlineRect.bottom = (int) Math.max(top, mOutlineRect.bottom);
    445         mOutlineRect.right = (int) Math.max(left, mOutlineRect.right);
    446         applyRoundness();
    447     }
    448 
    449     public Path getCustomClipPath(View child) {
    450         return null;
    451     }
    452 }
    453