/* * Copyright (C) 2015 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.allapps; import android.content.Context; import android.graphics.Color; import android.graphics.Rect; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.InsetDrawable; import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.RecyclerView; import android.text.Selection; import android.text.SpannableStringBuilder; import android.util.AttributeSet; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.ViewGroup; import com.android.launcher3.AppInfo; import com.android.launcher3.BaseContainerView; import com.android.launcher3.BubbleTextView; import com.android.launcher3.DeleteDropTarget; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.Insettable; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.PromiseAppInfo; import com.android.launcher3.R; import com.android.launcher3.Utilities; import com.android.launcher3.anim.SpringAnimationHandler; import com.android.launcher3.config.FeatureFlags; import com.android.launcher3.dragndrop.DragController; import com.android.launcher3.dragndrop.DragOptions; import com.android.launcher3.folder.Folder; import com.android.launcher3.keyboard.FocusedItemDecorator; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.PackageUserKey; import java.util.List; import java.util.Set; /** * The all apps view container. */ public class AllAppsContainerView extends BaseContainerView implements DragSource, View.OnLongClickListener, Insettable { private final Launcher mLauncher; private final AlphabeticalAppsList mApps; private final AllAppsGridAdapter mAdapter; private final LinearLayoutManager mLayoutManager; private AllAppsRecyclerView mAppsRecyclerView; private SearchUiManager mSearchUiManager; private View mSearchContainer; private SpannableStringBuilder mSearchQueryBuilder = null; private int mNumAppsPerRow; private int mNumPredictedAppsPerRow; private SpringAnimationHandler mSpringAnimationHandler; public AllAppsContainerView(Context context) { this(context, null); } public AllAppsContainerView(Context context, AttributeSet attrs) { this(context, attrs, 0); } public AllAppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) { super(context, attrs, defStyleAttr); mLauncher = Launcher.getLauncher(context); mApps = new AlphabeticalAppsList(context); mAdapter = new AllAppsGridAdapter(mLauncher, mApps, mLauncher, this); mSpringAnimationHandler = mAdapter.getSpringAnimationHandler(); mApps.setAdapter(mAdapter); mLayoutManager = mAdapter.getLayoutManager(); mSearchQueryBuilder = new SpannableStringBuilder(); Selection.setSelection(mSearchQueryBuilder, 0); } @Override protected void updateBackground( int paddingLeft, int paddingTop, int paddingRight, int paddingBottom) { if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { if (mLauncher.getDeviceProfile().isVerticalBarLayout()) { getRevealView().setBackground(new InsetDrawable(mBaseDrawable, paddingLeft, paddingTop, paddingRight, paddingBottom)); getContentView().setBackground( new InsetDrawable(new ColorDrawable(Color.TRANSPARENT), paddingLeft, paddingTop, paddingRight, paddingBottom)); } else { getRevealView().setBackground(mBaseDrawable); } } else { super.updateBackground(paddingLeft, paddingTop, paddingRight, paddingBottom); } } /** * Sets the current set of predicted apps. */ public void setPredictedApps(List apps) { mApps.setPredictedApps(apps); } /** * Sets the current set of apps. */ public void setApps(List apps) { mApps.setApps(apps); } /** * Adds new apps to the list. */ public void addApps(List apps) { mApps.addApps(apps); mSearchUiManager.refreshSearchResult(); } /** * Updates existing apps in the list */ public void updateApps(List apps) { mApps.updateApps(apps); mSearchUiManager.refreshSearchResult(); } public void updatePromiseAppProgress(PromiseAppInfo app) { int childCount = mAppsRecyclerView.getChildCount(); for (int i = 0; i < childCount; i++) { View child = mAppsRecyclerView.getChildAt(i); if (child instanceof BubbleTextView && child.getTag() == app) { BubbleTextView bubbleTextView = (BubbleTextView) child; bubbleTextView.applyProgressLevel(app.level); } } } /** * Removes some apps from the list. */ public void removeApps(List apps) { mApps.removeApps(apps); mSearchUiManager.refreshSearchResult(); } /** * Returns whether the view itself will handle the touch event or not. */ public boolean shouldContainerScroll(MotionEvent ev) { // IF the MotionEvent is inside the search box, and the container keeps on receiving // touch input, container should move down. if (mLauncher.getDragLayer().isEventOverView(mSearchContainer, ev)) { return true; } int[] point = new int[2]; point[0] = (int) ev.getX(); point[1] = (int) ev.getY(); Utilities.mapCoordInSelfToDescendant( mAppsRecyclerView.getScrollBar(), mLauncher.getDragLayer(), point); // IF the MotionEvent is inside the thumb, container should not be pulled down. if (mAppsRecyclerView.getScrollBar().shouldBlockIntercept(point[0], point[1])) { return false; } // IF scroller is at the very top OR there is no scroll bar because there is probably not // enough items to scroll, THEN it's okay for the container to be pulled down. if (mAppsRecyclerView.getCurrentScrollY() == 0) { return true; } return false; } /** * Resets the state of AllApps. */ public void reset() { // Reset the search bar and base recycler view after transitioning home mAppsRecyclerView.scrollToTop(); mSearchUiManager.reset(); } @Override protected void onFinishInflate() { super.onFinishInflate(); // This is a focus listener that proxies focus from a view into the list view. This is to // work around the search box from getting first focus and showing the cursor. getContentView().setOnFocusChangeListener(new View.OnFocusChangeListener() { @Override public void onFocusChange(View v, boolean hasFocus) { if (hasFocus) { mAppsRecyclerView.requestFocus(); } } }); // Load the all apps recycler view mAppsRecyclerView = findViewById(R.id.apps_list_view); mAppsRecyclerView.setApps(mApps); mAppsRecyclerView.setLayoutManager(mLayoutManager); mAppsRecyclerView.setAdapter(mAdapter); mAppsRecyclerView.setHasFixedSize(true); if (FeatureFlags.LAUNCHER3_PHYSICS) { mAppsRecyclerView.setSpringAnimationHandler(mSpringAnimationHandler); mAppsRecyclerView.addOnScrollListener(new SpringMotionOnScrollListener()); } mSearchContainer = findViewById(R.id.search_container_all_apps); mSearchUiManager = (SearchUiManager) mSearchContainer; mSearchUiManager.initialize(mApps, mAppsRecyclerView); FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(mAppsRecyclerView); mAppsRecyclerView.addItemDecoration(focusedItemDecorator); mAppsRecyclerView.preMeasureViews(mAdapter); mAdapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { getRevealView().setVisibility(View.VISIBLE); getContentView().setVisibility(View.VISIBLE); getContentView().setBackground(null); } } public SearchUiManager getSearchUiManager() { return mSearchUiManager; } @Override public View getTouchDelegateTargetView() { return mAppsRecyclerView; } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { DeviceProfile grid = mLauncher.getDeviceProfile(); // Update the number of items in the grid before we measure the view grid.updateAppsViewNumCols(); if (FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP) { if (mNumAppsPerRow != grid.inv.numColumns || mNumPredictedAppsPerRow != grid.inv.numColumns) { mNumAppsPerRow = grid.inv.numColumns; mNumPredictedAppsPerRow = grid.inv.numColumns; mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); mAdapter.setNumAppsPerRow(mNumAppsPerRow); mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow); } super.onMeasure(widthMeasureSpec, heightMeasureSpec); return; } // --- remove START when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. --- if (mNumAppsPerRow != grid.allAppsNumCols || mNumPredictedAppsPerRow != grid.allAppsNumPredictiveCols) { mNumAppsPerRow = grid.allAppsNumCols; mNumPredictedAppsPerRow = grid.allAppsNumPredictiveCols; mAppsRecyclerView.setNumAppsPerRow(grid, mNumAppsPerRow); mAdapter.setNumAppsPerRow(mNumAppsPerRow); mApps.setNumAppsPerRow(mNumAppsPerRow, mNumPredictedAppsPerRow); } // --- remove END when {@code FeatureFlags.LAUNCHER3_ALL_APPS_PULL_UP} is enabled. --- super.onMeasure(widthMeasureSpec, heightMeasureSpec); } @Override public boolean dispatchKeyEvent(KeyEvent event) { mSearchUiManager.preDispatchKeyEvent(event); return super.dispatchKeyEvent(event); } @Override public boolean onLongClick(final View v) { // When we have exited all apps or are in transition, disregard long clicks if (!mLauncher.isAppsViewVisible() || mLauncher.getWorkspace().isSwitchingState()) return false; // Return if global dragging is not enabled or we are already dragging if (!mLauncher.isDraggingEnabled()) return false; if (mLauncher.getDragController().isDragging()) return false; // Start the drag final DragController dragController = mLauncher.getDragController(); dragController.addDragListener(new DragController.DragListener() { @Override public void onDragStart(DropTarget.DragObject dragObject, DragOptions options) { v.setVisibility(INVISIBLE); } @Override public void onDragEnd() { v.setVisibility(VISIBLE); dragController.removeDragListener(this); } }); mLauncher.getWorkspace().beginDragShared(v, this, new DragOptions()); return false; } @Override public boolean supportsAppInfoDropTarget() { return true; } @Override public boolean supportsDeleteDropTarget() { return false; } @Override public float getIntrinsicIconScaleFactor() { DeviceProfile grid = mLauncher.getDeviceProfile(); return (float) grid.allAppsIconSizePx / grid.iconSizePx; } @Override public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, boolean success) { if (isFlingToDelete || !success || (target != mLauncher.getWorkspace() && !(target instanceof DeleteDropTarget) && !(target instanceof Folder))) { // Exit spring loaded mode if we have not successfully dropped or have not handled the // drop in Workspace mLauncher.exitSpringLoadedDragModeDelayed(true, Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null); } mLauncher.unlockScreenOrientation(false); if (!success) { d.deferDragViewCleanupPostAnimation = false; } } @Override public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { targetParent.containerType = mAppsRecyclerView.getContainerType(v); } @Override public void setInsets(Rect insets) { DeviceProfile grid = mLauncher.getDeviceProfile(); mAppsRecyclerView.setPadding( mAppsRecyclerView.getPaddingLeft(), mAppsRecyclerView.getPaddingTop(), mAppsRecyclerView.getPaddingRight(), insets.bottom); if (grid.isVerticalBarLayout()) { ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); mlp.leftMargin = insets.left; mlp.topMargin = insets.top; mlp.rightMargin = insets.right; setLayoutParams(mlp); } else { View navBarBg = findViewById(R.id.nav_bar_bg); ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams(); navBarBgLp.height = insets.bottom; navBarBg.setLayoutParams(navBarBgLp); navBarBg.setVisibility(FeatureFlags.LAUNCHER3_GRADIENT_ALL_APPS ? View.INVISIBLE : View.VISIBLE); } } public void updateIconBadges(Set updatedBadges) { final PackageUserKey packageUserKey = new PackageUserKey(null, null); final int n = mAppsRecyclerView.getChildCount(); for (int i = 0; i < n; i++) { View child = mAppsRecyclerView.getChildAt(i); if (!(child instanceof BubbleTextView) || !(child.getTag() instanceof ItemInfo)) { continue; } ItemInfo info = (ItemInfo) child.getTag(); if (packageUserKey.updateFromItemInfo(info) && updatedBadges.contains(packageUserKey)) { ((BubbleTextView) child).applyBadgeState(info, true /* animate */); } } } public SpringAnimationHandler getSpringAnimationHandler() { return mSpringAnimationHandler; } public class SpringMotionOnScrollListener extends RecyclerView.OnScrollListener { private int mScrollState = RecyclerView.SCROLL_STATE_IDLE; @Override public void onScrolled(RecyclerView recyclerView, int dx, int dy) { if (mScrollState == RecyclerView.SCROLL_STATE_DRAGGING || (dx == 0 && dy == 0)) { if (mSpringAnimationHandler.isRunning()){ mSpringAnimationHandler.skipToEnd(); } return; } int first = mLayoutManager.findFirstVisibleItemPosition(); int last = mLayoutManager.findLastVisibleItemPosition(); // We only show the spring animation when at the top or bottom, so we wait until the // first or last row is visible to ensure that all animations run in sync. if ((first == 0 && dy < 0) || (last == mAdapter.getItemCount() - 1 && dy > 0)) { mSpringAnimationHandler.animateToFinalPosition(0); } } @Override public void onScrollStateChanged(RecyclerView recyclerView, int newState) { super.onScrollStateChanged(recyclerView, newState); mScrollState = newState; } } }