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.LauncherState.ALL_APPS;
20 
21 import android.annotation.TargetApi;
22 import android.content.Context;
23 import android.graphics.Canvas;
24 import android.graphics.Rect;
25 import android.graphics.Typeface;
26 import android.os.Build;
27 import android.text.Layout;
28 import android.text.StaticLayout;
29 import android.text.TextPaint;
30 import android.util.AttributeSet;
31 import android.view.View;
32 
33 import androidx.annotation.ColorInt;
34 import androidx.core.content.ContextCompat;
35 
36 import com.android.launcher3.DeviceProfile;
37 import com.android.launcher3.Launcher;
38 import com.android.launcher3.LauncherState;
39 import com.android.launcher3.R;
40 import com.android.launcher3.allapps.FloatingHeaderRow;
41 import com.android.launcher3.allapps.FloatingHeaderView;
42 import com.android.launcher3.statemanager.StateManager.StateListener;
43 import com.android.launcher3.util.Themes;
44 
45 /**
46  * A view which shows a horizontal divider
47  */
48 @TargetApi(Build.VERSION_CODES.O)
49 public class AppsDividerView extends View implements StateListener<LauncherState>,
50         FloatingHeaderRow {
51 
52     private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count";
53     private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20;
54 
55     public enum DividerType {
56         NONE,
57         LINE,
58         ALL_APPS_LABEL
59     }
60 
61     private final Launcher mLauncher;
62     private final TextPaint mPaint = new TextPaint();
63     private DividerType mDividerType = DividerType.NONE;
64 
65     private final @ColorInt int mStrokeColor;
66     private final @ColorInt int mAllAppsLabelTextColor;
67 
68     private Layout mAllAppsLabelLayout;
69     private boolean mShowAllAppsLabel;
70 
71     private FloatingHeaderView mParent;
72     private boolean mTabsHidden;
73     private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS;
74 
75     private boolean mIsScrolledOut = false;
76 
77     private final int[] mDividerSize;
78 
AppsDividerView(Context context)79     public AppsDividerView(Context context) {
80         this(context, null);
81     }
82 
AppsDividerView(Context context, AttributeSet attrs)83     public AppsDividerView(Context context, AttributeSet attrs) {
84         this(context, attrs, 0);
85     }
86 
AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr)87     public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) {
88         super(context, attrs, defStyleAttr);
89         mLauncher = Launcher.getLauncher(context);
90 
91         boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark);
92         mDividerSize = new int[]{
93                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_width),
94                 getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)
95         };
96 
97         mStrokeColor = ContextCompat.getColor(context, isMainColorDark
98                 ? R.color.all_apps_prediction_row_separator_dark
99                 : R.color.all_apps_prediction_row_separator);
100 
101         mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark
102                 ? R.color.all_apps_label_text_dark
103                 : R.color.all_apps_label_text);
104     }
105 
setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)106     public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) {
107         mParent = parent;
108         mTabsHidden = tabsHidden;
109         mRows = rows;
110         updateDividerType();
111     }
112 
113     @Override
getExpectedHeight()114     public int getExpectedHeight() {
115         return getPaddingTop() + getPaddingBottom();
116     }
117 
118     @Override
shouldDraw()119     public boolean shouldDraw() {
120         return mDividerType != DividerType.NONE;
121     }
122 
123     @Override
hasVisibleContent()124     public boolean hasVisibleContent() {
125         return false;
126     }
127 
updateDividerType()128     private void updateDividerType() {
129         final DividerType dividerType;
130         if (!mTabsHidden) {
131             dividerType = DividerType.NONE;
132         } else {
133             // Check how many sections above me.
134             int sectionCount = 0;
135             for (FloatingHeaderRow row : mRows) {
136                 if (row == this) {
137                     break;
138                 } else if (row.shouldDraw()) {
139                     sectionCount++;
140                 }
141             }
142 
143             if (mShowAllAppsLabel && sectionCount > 0) {
144                 dividerType = DividerType.ALL_APPS_LABEL;
145             } else if (sectionCount == 1) {
146                 dividerType = DividerType.LINE;
147             } else {
148                 dividerType = DividerType.NONE;
149             }
150         }
151 
152         if (mDividerType != dividerType) {
153             mDividerType = dividerType;
154             int topPadding;
155             int bottomPadding;
156             switch (dividerType) {
157                 case LINE:
158                     topPadding = 0;
159                     bottomPadding = getResources()
160                             .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height);
161                     mPaint.setColor(mStrokeColor);
162                     break;
163                 case ALL_APPS_LABEL:
164                     topPadding = getAllAppsLabelLayout().getHeight() + getResources()
165                             .getDimensionPixelSize(R.dimen.all_apps_label_top_padding);
166                     bottomPadding = getResources()
167                             .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding);
168                     mPaint.setColor(mAllAppsLabelTextColor);
169                     break;
170                 case NONE:
171                 default:
172                     topPadding = bottomPadding = 0;
173                     break;
174             }
175             setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding);
176             updateViewVisibility();
177             invalidate();
178             requestLayout();
179             if (mParent != null) {
180                 mParent.onHeightUpdated();
181             }
182         }
183     }
184 
updateViewVisibility()185     private void updateViewVisibility() {
186         setVisibility(mDividerType == DividerType.NONE
187                 ? GONE
188                 : (mIsScrolledOut ? INVISIBLE : VISIBLE));
189     }
190 
191     @Override
onDraw(Canvas canvas)192     protected void onDraw(Canvas canvas) {
193         if (mDividerType == DividerType.LINE) {
194             int l = (getWidth() - getPaddingLeft() - mDividerSize[0]) / 2;
195             int t = getHeight() - (getPaddingBottom() / 2);
196             int radius = mDividerSize[1];
197             canvas.drawRoundRect(l, t, l + mDividerSize[0], t + mDividerSize[1], radius, radius,
198                     mPaint);
199         } else if (mDividerType == DividerType.ALL_APPS_LABEL) {
200             Layout textLayout = getAllAppsLabelLayout();
201             int x = getWidth() / 2 - textLayout.getWidth() / 2;
202             int y = getHeight() - getPaddingBottom() - textLayout.getHeight();
203             canvas.translate(x, y);
204             textLayout.draw(canvas);
205             canvas.translate(-x, -y);
206         }
207     }
208 
getAllAppsLabelLayout()209     private Layout getAllAppsLabelLayout() {
210         if (mAllAppsLabelLayout == null) {
211             mPaint.setAntiAlias(true);
212             mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL));
213             mPaint.setTextSize(
214                     getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size));
215 
216             CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label);
217             mAllAppsLabelLayout = StaticLayout.Builder.obtain(
218                     allAppsLabelText, 0, allAppsLabelText.length(), mPaint,
219                     Math.round(mPaint.measureText(allAppsLabelText.toString())))
220                     .setAlignment(Layout.Alignment.ALIGN_CENTER)
221                     .setMaxLines(1)
222                     .setIncludePad(true)
223                     .build();
224         }
225         return mAllAppsLabelLayout;
226     }
227 
228     @Override
hasOverlappingRendering()229     public boolean hasOverlappingRendering() {
230         return false;
231     }
232 
onMeasure(int widthMeasureSpec, int heightMeasureSpec)233     protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
234         setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
235                 getPaddingBottom() + getPaddingTop());
236     }
237 
238     @Override
onAttachedToWindow()239     protected void onAttachedToWindow() {
240         super.onAttachedToWindow();
241 
242         if (shouldShowAllAppsLabel()) {
243             mShowAllAppsLabel = true;
244             mLauncher.getStateManager().addStateListener(this);
245             updateDividerType();
246         }
247     }
248 
249     @Override
onDetachedFromWindow()250     protected void onDetachedFromWindow() {
251         super.onDetachedFromWindow();
252         mLauncher.getStateManager().removeStateListener(this);
253     }
254 
255     @Override
onStateTransitionComplete(LauncherState finalState)256     public void onStateTransitionComplete(LauncherState finalState) {
257         if (finalState == ALL_APPS) {
258             setAllAppsVisitedCount(getAllAppsVisitedCount() + 1);
259         } else {
260             if (mShowAllAppsLabel != shouldShowAllAppsLabel()) {
261                 mShowAllAppsLabel = !mShowAllAppsLabel;
262                 updateDividerType();
263             }
264 
265             if (!mShowAllAppsLabel) {
266                 mLauncher.getStateManager().removeStateListener(this);
267             }
268         }
269     }
270 
setAllAppsVisitedCount(int count)271     private void setAllAppsVisitedCount(int count) {
272         mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply();
273     }
274 
getAllAppsVisitedCount()275     private int getAllAppsVisitedCount() {
276         return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0);
277     }
278 
shouldShowAllAppsLabel()279     private boolean shouldShowAllAppsLabel() {
280         return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT;
281     }
282 
283     @Override
setInsets(Rect insets, DeviceProfile grid)284     public void setInsets(Rect insets, DeviceProfile grid) {
285         int leftRightPadding = grid.allAppsLeftRightPadding;
286         setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom());
287     }
288 
289     @Override
setVerticalScroll(int scroll, boolean isScrolledOut)290     public void setVerticalScroll(int scroll, boolean isScrolledOut) {
291         setTranslationY(scroll);
292         mIsScrolledOut = isScrolledOut;
293         updateViewVisibility();
294     }
295 
296     @Override
getTypeClass()297     public Class<AppsDividerView> getTypeClass() {
298         return AppsDividerView.class;
299     }
300 
301     @Override
getFocusedChild()302     public View getFocusedChild() {
303         return null;
304     }
305 }
306