1 /*
2  * Copyright (C) 2019 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.launcher3.appprediction;
18 
19 import static com.android.launcher3.util.OnboardingPrefs.ALL_APPS_VISITED_COUNT;
20 
21 import android.annotation.TargetApi;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.Typeface;
25 import android.os.Build;
26 import android.text.Layout;
27 import android.text.StaticLayout;
28 import android.text.TextPaint;
29 import android.util.AttributeSet;
30 import android.view.View;
31 import android.view.accessibility.AccessibilityManager;
32 
33 import androidx.annotation.ColorInt;
34 import androidx.core.content.ContextCompat;
35 
36 import com.android.launcher3.R;
37 import com.android.launcher3.Utilities;
38 import com.android.launcher3.allapps.FloatingHeaderRow;
39 import com.android.launcher3.allapps.FloatingHeaderView;
40 
41 /**
42  * A view which shows a horizontal divider
43  */
44 @TargetApi(Build.VERSION_CODES.O)
45 public class AppsDividerView extends View implements FloatingHeaderRow {
46 
47     public enum DividerType {
48         NONE,
49         LINE,
50         ALL_APPS_LABEL
51     }
52 
53     private final TextPaint mPaint = new TextPaint();
54     private DividerType mDividerType = DividerType.NONE;
55 
56     private final @ColorInt int mStrokeColor;
57     private final @ColorInt int mAllAppsLabelTextColor;
58     private final AccessibilityManager mAccessibilityManager;
59 
60     private Layout mAllAppsLabelLayout;
61     private boolean mShowAllAppsLabel;
62 
63     private FloatingHeaderView mParent;
64     private boolean mTabsHidden;
65     private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
66 
67     private boolean mIsScrolledOut = false;
68 
69     private final int[] mDividerSize;
70 
AppsDividerView(Context context)71     public AppsDividerView(Context context) {
72         this(context, null);
73     }
74 
AppsDividerView(Context context, AttributeSet attrs)75     public AppsDividerView(Context context, AttributeSet attrs) {
76         this(context, attrs, 0);
77     }
78 
AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr)79     public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
80         super(context, attrs, defStyleAttr);
81 
82         mDividerSize = new int[]{
83                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_width),
84                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
85         };
86 
87         mStrokeColor = ContextCompat.getColor(context, R.color.material_color_outline_variant);
88 
89         mAllAppsLabelTextColor = ContextCompat.getColor(context,
90                 R.color.material_color_on_surface_variant);
91 
92         mAccessibilityManager = AccessibilityManager.getInstance(context);
93         setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context));
94     }
95 
setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)96     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
97         mParent = parent;
98         mTabsHidden = tabsHidden;
99         mRows = rows;
100         updateDividerType();
101     }
102 
103     /** {@code true} if all apps label should be shown in place of divider. */
setShowAllAppsLabel(boolean showAllAppsLabel)104     public void setShowAllAppsLabel(boolean showAllAppsLabel) {
105         if (mAccessibilityManager.isEnabled() && !Utilities.isRunningInTestHarness()) {
106             showAllAppsLabel = true;
107         }
108         if (showAllAppsLabel != mShowAllAppsLabel) {
109             mShowAllAppsLabel = showAllAppsLabel;
110             updateDividerType();
111         }
112     }
113 
114     @Override
getExpectedHeight()115     public int getExpectedHeight() {
116         return getPaddingTop() + getPaddingBottom();
117     }
118 
119     @Override
shouldDraw()120     public boolean shouldDraw() {
121         return mDividerType != DividerType.NONE;
122     }
123 
124     @Override
hasVisibleContent()125     public boolean hasVisibleContent() {
126         return false;
127     }
128 
updateDividerType()129     private void updateDividerType() {
130         final DividerType dividerType;
131         if (!mTabsHidden) {
132             dividerType = DividerType.NONE;
133         } else {
134             // Check how many sections above me.
135             int sectionCount = 0;
136             for (FloatingHeaderRow row : mRows) {
137                 if (row == this) {
138                     break;
139                 } else if (row.shouldDraw()) {
140                     sectionCount++;
141                 }
142             }
143 
144             if (mShowAllAppsLabel && sectionCount > 0) {
145                 dividerType = DividerType.ALL_APPS_LABEL;
146             } else if (sectionCount == 1) {
147                 dividerType = DividerType.LINE;
148             } else {
149                 dividerType = DividerType.NONE;
150             }
151         }
152 
153         if (mDividerType != dividerType) {
154             mDividerType = dividerType;
155             int topPadding;
156             int bottomPadding;
157             setContentDescription(null);
158             switch (dividerType) {
159                 case LINE:
160                     topPadding = 0;
161                     bottomPadding = getResources()
162                             .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
163                     mPaint.setColor(mStrokeColor);
164                     break;
165                 case ALL_APPS_LABEL:
166                     topPadding = getAllAppsLabelLayout().getHeight() + getResources()
167                             .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
168                     bottomPadding = getResources()
169                             .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
170                     mPaint.setColor(mAllAppsLabelTextColor);
171                     setContentDescription(mAllAppsLabelLayout.getText());
172                     break;
173                 case NONE:
174                 default:
175                     topPadding = bottomPadding = 0;
176                     break;
177             }
178             setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
179             updateViewVisibility();
180             invalidate();
181             requestLayout();
182             if (mParent != null) {
183                 mParent.onHeightUpdated();
184             }
185         }
186     }
187 
updateViewVisibility()188     private void updateViewVisibility() {
189         setVisibility(mDividerType == DividerType.NONE
190                 ? GONE
191                 : (mIsScrolledOut ? INVISIBLE : VISIBLE));
192     }
193 
194     @Override
onDraw(Canvas canvas)195     protected void onDraw(Canvas canvas) {
196         if (mDividerType == DividerType.LINE) {
197             int l = (getWidth() - mDividerSize[0]) / 2;
198             int t = getHeight() - (getPaddingBottom() / 2);
199             int radius = mDividerSize[1];
200             canvas.drawRoundRect(l, t, l + mDividerSize[0], t + mDividerSize[1], radius, radius,
201                     mPaint);
202         } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
203             Layout textLayout = getAllAppsLabelLayout();
204             int x = getWidth() / 2 - textLayout.getWidth() / 2;
205             int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
206             canvas.translate(x, y);
207             textLayout.draw(canvas);
208             canvas.translate(-x, -y);
209         }
210     }
211 
getAllAppsLabelLayout()212     private Layout getAllAppsLabelLayout() {
213         if (mAllAppsLabelLayout == null) {
214             mPaint.setAntiAlias(true);
215             mPaint.setTypeface(Typeface.create("google-sans", Typeface.NORMAL));
216             mPaint.setTextSize(
217                     getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
218 
219             CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
220             mAllAppsLabelLayout = StaticLayout.Builder.obtain(
221                             allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
222                             Math.round(mPaint.measureText(allAppsLabelText.toString())))
223                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
224                     .setMaxLines(1)
225                     .setIncludePad(true)
226                     .build();
227         }
228         return mAllAppsLabelLayout;
229     }
230 
231     @Override
hasOverlappingRendering()232     public boolean hasOverlappingRendering() {
233         return false;
234     }
235 
onMeasure(int widthMeasureSpec, int heightMeasureSpec)236     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
237         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
238                 getPaddingBottom() + getPaddingTop());
239     }
240 
241     @Override
setVerticalScroll(int scroll, boolean isScrolledOut)242     public void setVerticalScroll(int scroll, boolean isScrolledOut) {
243         setTranslationY(scroll);
244         mIsScrolledOut = isScrolledOut;
245         updateViewVisibility();
246     }
247 
248     @Override
getTypeClass()249     public Class<AppsDividerView> getTypeClass() {
250         return AppsDividerView.class;
251     }
252 
253     @Override
getFocusedChild()254     public View getFocusedChild() {
255         return null;
256     }
257 }
258