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