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.annotation.VisibleForTesting; 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 = context.getColor(R.color.materialColorOutlineVariant); 88 89 mAllAppsLabelTextColor = context.getColor(R.color.materialColorOnSurfaceVariant); 90 91 mAccessibilityManager = AccessibilityManager.getInstance(context); 92 setShowAllAppsLabel(!ALL_APPS_VISITED_COUNT.hasReachedMax(context)); 93 } 94 setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden)95 public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { 96 mParent = parent; 97 mTabsHidden = tabsHidden; 98 mRows = rows; 99 updateDividerType(); 100 } 101 102 /** {@code true} if all apps label should be shown in place of divider. */ setShowAllAppsLabel(boolean showAllAppsLabel)103 public void setShowAllAppsLabel(boolean showAllAppsLabel) { 104 if (mAccessibilityManager.isEnabled() && !Utilities.isRunningInTestHarness()) { 105 showAllAppsLabel = true; 106 } 107 if (showAllAppsLabel != mShowAllAppsLabel) { 108 mShowAllAppsLabel = showAllAppsLabel; 109 updateDividerType(); 110 } 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 setContentDescription(null); 157 switch (dividerType) { 158 case LINE: 159 topPadding = 0; 160 bottomPadding = getResources() 161 .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height); 162 mPaint.setColor(mStrokeColor); 163 break; 164 case ALL_APPS_LABEL: 165 topPadding = getAllAppsLabelLayout().getHeight() + getResources() 166 .getDimensionPixelSize(R.dimen.all_apps_label_top_padding); 167 bottomPadding = getResources() 168 .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding); 169 mPaint.setColor(mAllAppsLabelTextColor); 170 setContentDescription(mAllAppsLabelLayout.getText()); 171 break; 172 case NONE: 173 default: 174 topPadding = bottomPadding = 0; 175 break; 176 } 177 setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding); 178 updateViewVisibility(); 179 invalidate(); 180 requestLayout(); 181 if (mParent != null) { 182 mParent.onHeightUpdated(); 183 } 184 } 185 } 186 updateViewVisibility()187 private void updateViewVisibility() { 188 setVisibility(mDividerType == DividerType.NONE 189 ? GONE 190 : (mIsScrolledOut ? INVISIBLE : VISIBLE)); 191 } 192 193 @Override onDraw(Canvas canvas)194 protected void onDraw(Canvas canvas) { 195 if (mDividerType == DividerType.LINE) { 196 int l = (getWidth() - mDividerSize[0]) / 2; 197 int t = getHeight() - (getPaddingBottom() / 2); 198 int radius = mDividerSize[1]; 199 canvas.drawRoundRect(l, t, l + mDividerSize[0], t + mDividerSize[1], radius, radius, 200 mPaint); 201 } else if (mDividerType == DividerType.ALL_APPS_LABEL) { 202 Layout textLayout = getAllAppsLabelLayout(); 203 int x = getWidth() / 2 - textLayout.getWidth() / 2; 204 int y = getHeight() - getPaddingBottom() - textLayout.getHeight(); 205 canvas.translate(x, y); 206 textLayout.draw(canvas); 207 canvas.translate(-x, -y); 208 } 209 } 210 getAllAppsLabelLayout()211 private Layout getAllAppsLabelLayout() { 212 if (mAllAppsLabelLayout == null) { 213 mPaint.setAntiAlias(true); 214 mPaint.setTypeface(Typeface.create("google-sans", Typeface.NORMAL)); 215 mPaint.setTextSize( 216 getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size)); 217 218 CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label); 219 mAllAppsLabelLayout = StaticLayout.Builder.obtain( 220 allAppsLabelText, 0, allAppsLabelText.length(), mPaint, 221 Math.round(mPaint.measureText(allAppsLabelText.toString()))) 222 .setAlignment(Layout.Alignment.ALIGN_CENTER) 223 .setMaxLines(1) 224 .setIncludePad(true) 225 .build(); 226 } 227 return mAllAppsLabelLayout; 228 } 229 230 @Override hasOverlappingRendering()231 public boolean hasOverlappingRendering() { 232 return false; 233 } 234 onMeasure(int widthMeasureSpec, int heightMeasureSpec)235 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 236 setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), 237 getPaddingBottom() + getPaddingTop()); 238 } 239 240 @Override setVerticalScroll(int scroll, boolean isScrolledOut)241 public void setVerticalScroll(int scroll, boolean isScrolledOut) { 242 setTranslationY(scroll); 243 mIsScrolledOut = isScrolledOut; 244 updateViewVisibility(); 245 } 246 247 @Override getTypeClass()248 public Class<AppsDividerView> getTypeClass() { 249 return AppsDividerView.class; 250 } 251 252 @Override getFocusedChild()253 public View getFocusedChild() { 254 return null; 255 } 256 257 @VisibleForTesting getDividerType()258 public DividerType getDividerType() { 259 return mDividerType; 260 } 261 } 262