| /* |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.launcher3.appprediction; |
| |
| import static com.android.launcher3.LauncherAnimUtils.VIEW_ALPHA; |
| import static com.android.launcher3.LauncherState.ALL_APPS; |
| |
| import android.annotation.TargetApi; |
| import android.content.Context; |
| import android.graphics.Canvas; |
| import android.graphics.Rect; |
| import android.graphics.Typeface; |
| import android.os.Build; |
| import android.text.Layout; |
| import android.text.StaticLayout; |
| import android.text.TextPaint; |
| import android.util.AttributeSet; |
| import android.view.View; |
| import android.view.animation.Interpolator; |
| |
| import androidx.annotation.ColorInt; |
| import androidx.core.content.ContextCompat; |
| |
| import com.android.launcher3.DeviceProfile; |
| import com.android.launcher3.Launcher; |
| import com.android.launcher3.LauncherState; |
| import com.android.launcher3.R; |
| import com.android.launcher3.allapps.FloatingHeaderRow; |
| import com.android.launcher3.allapps.FloatingHeaderView; |
| import com.android.launcher3.anim.PropertySetter; |
| import com.android.launcher3.config.FeatureFlags; |
| import com.android.launcher3.statemanager.StateManager.StateListener; |
| import com.android.launcher3.util.Themes; |
| |
| /** |
| * A view which shows a horizontal divider |
| */ |
| @TargetApi(Build.VERSION_CODES.O) |
| public class AppsDividerView extends View implements StateListener<LauncherState>, |
| FloatingHeaderRow { |
| |
| private static final String ALL_APPS_VISITED_COUNT = "launcher.all_apps_visited_count"; |
| private static final int SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT = 20; |
| |
| public enum DividerType { |
| NONE, |
| LINE, |
| ALL_APPS_LABEL |
| } |
| |
| private final Launcher mLauncher; |
| private final TextPaint mPaint = new TextPaint(); |
| private DividerType mDividerType = DividerType.NONE; |
| |
| private final @ColorInt int mStrokeColor; |
| private final @ColorInt int mAllAppsLabelTextColor; |
| |
| private Layout mAllAppsLabelLayout; |
| private boolean mShowAllAppsLabel; |
| |
| private FloatingHeaderView mParent; |
| private boolean mTabsHidden; |
| private FloatingHeaderRow[] mRows = FloatingHeaderRow.NO_ROWS; |
| |
| private boolean mIsScrolledOut = false; |
| |
| public AppsDividerView(Context context) { |
| this(context, null); |
| } |
| |
| public AppsDividerView(Context context, AttributeSet attrs) { |
| this(context, attrs, 0); |
| } |
| |
| public AppsDividerView(Context context, AttributeSet attrs, int defStyleAttr) { |
| super(context, attrs, defStyleAttr); |
| mLauncher = Launcher.getLauncher(context); |
| |
| boolean isMainColorDark = Themes.getAttrBoolean(context, R.attr.isMainColorDark); |
| mPaint.setStrokeWidth( |
| getResources().getDimensionPixelSize(R.dimen.all_apps_divider_height)); |
| |
| mStrokeColor = ContextCompat.getColor(context, isMainColorDark |
| ? R.color.all_apps_prediction_row_separator_dark |
| : R.color.all_apps_prediction_row_separator); |
| |
| mAllAppsLabelTextColor = ContextCompat.getColor(context, isMainColorDark |
| ? R.color.all_apps_label_text_dark |
| : R.color.all_apps_label_text); |
| } |
| |
| public void setup(FloatingHeaderView parent, FloatingHeaderRow[] rows, boolean tabsHidden) { |
| mParent = parent; |
| mTabsHidden = tabsHidden; |
| mRows = rows; |
| updateDividerType(); |
| } |
| |
| @Override |
| public int getExpectedHeight() { |
| return getPaddingTop() + getPaddingBottom(); |
| } |
| |
| @Override |
| public boolean shouldDraw() { |
| return mDividerType != DividerType.NONE; |
| } |
| |
| @Override |
| public boolean hasVisibleContent() { |
| return false; |
| } |
| |
| private void updateDividerType() { |
| final DividerType dividerType; |
| if (!mTabsHidden) { |
| dividerType = DividerType.NONE; |
| } else { |
| // Check how many sections above me. |
| int sectionCount = 0; |
| for (FloatingHeaderRow row : mRows) { |
| if (row == this) { |
| break; |
| } else if (row.shouldDraw()) { |
| sectionCount++; |
| } |
| } |
| |
| if (mShowAllAppsLabel && sectionCount > 0) { |
| dividerType = DividerType.ALL_APPS_LABEL; |
| } else if (sectionCount == 1) { |
| dividerType = DividerType.LINE; |
| } else { |
| dividerType = DividerType.NONE; |
| } |
| } |
| |
| if (mDividerType != dividerType) { |
| mDividerType = dividerType; |
| int topPadding; |
| int bottomPadding; |
| switch (dividerType) { |
| case LINE: |
| topPadding = 0; |
| bottomPadding = getResources() |
| .getDimensionPixelSize(R.dimen.all_apps_prediction_row_divider_height); |
| mPaint.setColor(mStrokeColor); |
| break; |
| case ALL_APPS_LABEL: |
| topPadding = getAllAppsLabelLayout().getHeight() + getResources() |
| .getDimensionPixelSize(R.dimen.all_apps_label_top_padding); |
| bottomPadding = getResources() |
| .getDimensionPixelSize(R.dimen.all_apps_label_bottom_padding); |
| mPaint.setColor(mAllAppsLabelTextColor); |
| break; |
| case NONE: |
| default: |
| topPadding = bottomPadding = 0; |
| break; |
| } |
| setPadding(getPaddingLeft(), topPadding, getPaddingRight(), bottomPadding); |
| updateViewVisibility(); |
| invalidate(); |
| requestLayout(); |
| if (mParent != null) { |
| mParent.onHeightUpdated(); |
| } |
| } |
| } |
| |
| private void updateViewVisibility() { |
| // hide divider since we have item decoration for prediction row |
| if (FeatureFlags.ENABLE_DEVICE_SEARCH.get()) { |
| setVisibility(GONE); |
| return; |
| } |
| setVisibility(mDividerType == DividerType.NONE |
| ? GONE |
| : (mIsScrolledOut ? INVISIBLE : VISIBLE)); |
| } |
| |
| @Override |
| protected void onDraw(Canvas canvas) { |
| if (mDividerType == DividerType.LINE) { |
| int side = getResources().getDimensionPixelSize(R.dimen.dynamic_grid_edge_margin); |
| int y = getHeight() - (getPaddingBottom() / 2); |
| int x1 = getPaddingLeft() + side; |
| int x2 = getWidth() - getPaddingRight() - side; |
| canvas.drawLine(x1, y, x2, y, mPaint); |
| } else if (mDividerType == DividerType.ALL_APPS_LABEL) { |
| Layout textLayout = getAllAppsLabelLayout(); |
| int x = getWidth() / 2 - textLayout.getWidth() / 2; |
| int y = getHeight() - getPaddingBottom() - textLayout.getHeight(); |
| canvas.translate(x, y); |
| textLayout.draw(canvas); |
| canvas.translate(-x, -y); |
| } |
| } |
| |
| private Layout getAllAppsLabelLayout() { |
| if (mAllAppsLabelLayout == null) { |
| mPaint.setAntiAlias(true); |
| mPaint.setTypeface(Typeface.create("sans-serif-medium", Typeface.NORMAL)); |
| mPaint.setTextSize( |
| getResources().getDimensionPixelSize(R.dimen.all_apps_label_text_size)); |
| |
| CharSequence allAppsLabelText = getResources().getText(R.string.all_apps_label); |
| mAllAppsLabelLayout = StaticLayout.Builder.obtain( |
| allAppsLabelText, 0, allAppsLabelText.length(), mPaint, |
| Math.round(mPaint.measureText(allAppsLabelText.toString()))) |
| .setAlignment(Layout.Alignment.ALIGN_CENTER) |
| .setMaxLines(1) |
| .setIncludePad(true) |
| .build(); |
| } |
| return mAllAppsLabelLayout; |
| } |
| |
| @Override |
| public boolean hasOverlappingRendering() { |
| return false; |
| } |
| |
| protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { |
| setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec), |
| getPaddingBottom() + getPaddingTop()); |
| } |
| |
| @Override |
| protected void onAttachedToWindow() { |
| super.onAttachedToWindow(); |
| |
| if (shouldShowAllAppsLabel()) { |
| mShowAllAppsLabel = true; |
| mLauncher.getStateManager().addStateListener(this); |
| updateDividerType(); |
| } |
| } |
| |
| @Override |
| protected void onDetachedFromWindow() { |
| super.onDetachedFromWindow(); |
| mLauncher.getStateManager().removeStateListener(this); |
| } |
| |
| @Override |
| public void onStateTransitionComplete(LauncherState finalState) { |
| if (finalState == ALL_APPS) { |
| setAllAppsVisitedCount(getAllAppsVisitedCount() + 1); |
| } else { |
| if (mShowAllAppsLabel != shouldShowAllAppsLabel()) { |
| mShowAllAppsLabel = !mShowAllAppsLabel; |
| updateDividerType(); |
| } |
| |
| if (!mShowAllAppsLabel) { |
| mLauncher.getStateManager().removeStateListener(this); |
| } |
| } |
| } |
| |
| private void setAllAppsVisitedCount(int count) { |
| mLauncher.getSharedPrefs().edit().putInt(ALL_APPS_VISITED_COUNT, count).apply(); |
| } |
| |
| private int getAllAppsVisitedCount() { |
| return mLauncher.getSharedPrefs().getInt(ALL_APPS_VISITED_COUNT, 0); |
| } |
| |
| private boolean shouldShowAllAppsLabel() { |
| return getAllAppsVisitedCount() < SHOW_ALL_APPS_LABEL_ON_ALL_APPS_VISITED_COUNT; |
| } |
| |
| @Override |
| public void setInsets(Rect insets, DeviceProfile grid) { |
| int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx |
| + grid.cellLayoutPaddingLeftRightPx; |
| setPadding(leftRightPadding, getPaddingTop(), leftRightPadding, getPaddingBottom()); |
| } |
| |
| @Override |
| public void setContentVisibility(boolean hasHeaderExtra, boolean hasAllAppsContent, |
| PropertySetter setter, Interpolator headerFade, Interpolator allAppsFade) { |
| // Don't use setViewAlpha as we want to control the visibility ourselves. |
| setter.setFloat(this, VIEW_ALPHA, hasAllAppsContent ? 1 : 0, allAppsFade); |
| } |
| |
| @Override |
| public void setVerticalScroll(int scroll, boolean isScrolledOut) { |
| setTranslationY(scroll); |
| mIsScrolledOut = isScrolledOut; |
| updateViewVisibility(); |
| } |
| |
| @Override |
| public Class<AppsDividerView> getTypeClass() { |
| return AppsDividerView.class; |
| } |
| |
| @Override |
| public View getFocusedChild() { |
| return null; |
| } |
| } |