/* * 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.Bitmap; import android.graphics.Rect; import android.os.Process; import android.support.annotation.NonNull; import android.support.annotation.Nullable; 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.LayoutInflater; import android.view.MotionEvent; import android.view.View; import android.view.View.OnLongClickListener; import android.view.ViewGroup; import android.widget.RelativeLayout; import com.android.launcher3.AppInfo; import com.android.launcher3.BubbleTextView; import com.android.launcher3.BubbleTextView.BubbleTextShadowHandler; import com.android.launcher3.ClickShadowView; import com.android.launcher3.DeviceProfile; import com.android.launcher3.DeviceProfile.OnDeviceProfileChangeListener; import com.android.launcher3.DragSource; import com.android.launcher3.DropTarget; import com.android.launcher3.DropTarget.DragObject; import com.android.launcher3.Insettable; import com.android.launcher3.InsettableFrameLayout; import com.android.launcher3.ItemInfo; import com.android.launcher3.Launcher; import com.android.launcher3.LauncherState; import com.android.launcher3.R; 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.keyboard.FocusedItemDecorator; import com.android.launcher3.userevent.nano.LauncherLogProto.Target; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.views.BottomUserEducationView; /** * The all apps view container. */ public class AllAppsContainerView extends RelativeLayout implements DragSource, OnLongClickListener, Insettable, BubbleTextShadowHandler, OnDeviceProfileChangeListener { private final Launcher mLauncher; private final AdapterHolder[] mAH; private final ClickShadowView mTouchFeedbackView; private final ItemInfoMatcher mPersonalMatcher = ItemInfoMatcher.ofUser(Process.myUserHandle()); private final ItemInfoMatcher mWorkMatcher = ItemInfoMatcher.not(mPersonalMatcher); private final AllAppsStore mAllAppsStore = new AllAppsStore(); private SearchUiManager mSearchUiManager; private View mSearchContainer; private AllAppsPagedView mViewPager; private FloatingHeaderView mHeader; private SpannableStringBuilder mSearchQueryBuilder = null; private boolean mUsingTabs; private boolean mSearchModeWhileUsingTabs = false; 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); mLauncher.addOnDeviceProfileChangeListener(this); mSearchQueryBuilder = new SpannableStringBuilder(); Selection.setSelection(mSearchQueryBuilder, 0); mTouchFeedbackView = new ClickShadowView(context); // Make the feedback view large enough to hold the blur bitmap. int size = mLauncher.getDeviceProfile().allAppsIconSizePx + mTouchFeedbackView.getExtraSize(); addView(mTouchFeedbackView, size, size); mAH = new AdapterHolder[2]; mAH[AdapterHolder.MAIN] = new AdapterHolder(false /* isWork */); mAH[AdapterHolder.WORK] = new AdapterHolder(true /* isWork */); mAllAppsStore.addUpdateListener(this::onAppsUpdated); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); applyTouchDelegate(); } private void applyTouchDelegate() { // TODO: Reimplement once fast scroller is fixed. } public AllAppsStore getAppsStore() { return mAllAppsStore; } @Override protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); applyTouchDelegate(); } @Override public void onDeviceProfileChanged(DeviceProfile dp) { for (AdapterHolder holder : mAH) { if (holder.recyclerView != null) { // Remove all views and clear the pool, while keeping the data same. After this // call, all the viewHolders will be recreated. holder.recyclerView.swapAdapter(holder.recyclerView.getAdapter(), true); holder.recyclerView.getRecycledViewPool().clear(); } } } private void onAppsUpdated() { if (FeatureFlags.ALL_APPS_TABS_ENABLED) { boolean hasWorkApps = false; for (AppInfo app : mAllAppsStore.getApps()) { if (mWorkMatcher.matches(app, null)) { hasWorkApps = true; break; } } rebindAdapters(hasWorkApps); } } @Override public void setPressedIcon(BubbleTextView icon, Bitmap background) { mTouchFeedbackView.setPressedIcon(icon, background); } /** * 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; } AllAppsRecyclerView rv = getActiveRecyclerView(); return rv == null || rv.shouldContainerScroll(ev, mLauncher.getDragLayer()); } public AllAppsRecyclerView getActiveRecyclerView() { if (!mUsingTabs || mViewPager.getNextPage() == 0) { return mAH[AdapterHolder.MAIN].recyclerView; } else { return mAH[AdapterHolder.WORK].recyclerView; } } /** * Resets the state of AllApps. */ public void reset() { for (int i = 0; i < mAH.length; i++) { if (mAH[i].recyclerView != null) { mAH[i].recyclerView.scrollToTop(); } } if (isHeaderVisible()) { mHeader.reset(); } // Reset the search bar and base recycler view after transitioning home 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. setOnFocusChangeListener((v, hasFocus) -> { if (hasFocus && getActiveRecyclerView() != null) { getActiveRecyclerView().requestFocus(); } }); mHeader = findViewById(R.id.all_apps_header); rebindAdapters(mUsingTabs, true /* force */); mSearchContainer = findViewById(R.id.search_container_all_apps); mSearchUiManager = (SearchUiManager) mSearchContainer; mSearchUiManager.initialize(this); } public SearchUiManager getSearchUiManager() { return mSearchUiManager; } @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.isInState(LauncherState.ALL_APPS) || 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); } }); DeviceProfile grid = mLauncher.getDeviceProfile(); DragOptions options = new DragOptions(); options.intrinsicIconScaleFactor = (float) grid.allAppsIconSizePx / grid.iconSizePx; mLauncher.getWorkspace().beginDragShared(v, this, options); return false; } @Override public void onDropCompleted(View target, DragObject d, boolean success) { } @Override public void fillInLogContainerData(View v, ItemInfo info, Target target, Target targetParent) { // This is filled in {@link AllAppsRecyclerView} } @Override public void setInsets(Rect insets) { DeviceProfile grid = mLauncher.getDeviceProfile(); int leftRightPadding = grid.desiredWorkspaceLeftRightMarginPx + grid.cellLayoutPaddingLeftRightPx; for (int i = 0; i < mAH.length; i++) { mAH[i].padding.bottom = insets.bottom; mAH[i].padding.left = mAH[i].padding.right = leftRightPadding; mAH[i].applyPadding(); } ViewGroup.MarginLayoutParams mlp = (MarginLayoutParams) getLayoutParams(); if (grid.isVerticalBarLayout()) { mlp.leftMargin = insets.left; mlp.topMargin = insets.top; mlp.rightMargin = insets.right; setPadding(grid.workspacePadding.left, 0, grid.workspacePadding.right, 0); } else { mlp.leftMargin = mlp.rightMargin = mlp.topMargin = 0; setPadding(0, 0, 0, 0); } setLayoutParams(mlp); View navBarBg = findViewById(R.id.nav_bar_bg); ViewGroup.LayoutParams navBarBgLp = navBarBg.getLayoutParams(); navBarBgLp.height = insets.bottom; navBarBg.setLayoutParams(navBarBgLp); InsettableFrameLayout.dispatchInsets(this, insets); } public SpringAnimationHandler getSpringAnimationHandler() { return mUsingTabs ? null : mAH[AdapterHolder.MAIN].animationHandler; } private void rebindAdapters(boolean showTabs) { rebindAdapters(showTabs, false /* force */); } private void rebindAdapters(boolean showTabs, boolean force) { if (showTabs == mUsingTabs && !force) { return; } replaceRVContainer(showTabs); mUsingTabs = showTabs; mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.MAIN].recyclerView); mAllAppsStore.unregisterIconContainer(mAH[AdapterHolder.WORK].recyclerView); if (mUsingTabs) { mAH[AdapterHolder.MAIN].setup(mViewPager.getChildAt(0), mPersonalMatcher); mAH[AdapterHolder.WORK].setup(mViewPager.getChildAt(1), mWorkMatcher); onTabChanged(mViewPager.getNextPage()); } else { mAH[AdapterHolder.MAIN].setup(findViewById(R.id.apps_list_view), null); mAH[AdapterHolder.WORK].recyclerView = null; } setupHeader(); mAllAppsStore.registerIconContainer(mAH[AdapterHolder.MAIN].recyclerView); mAllAppsStore.registerIconContainer(mAH[AdapterHolder.WORK].recyclerView); applyTouchDelegate(); } private void replaceRVContainer(boolean showTabs) { for (int i = 0; i < mAH.length; i++) { if (mAH[i].recyclerView != null) { mAH[i].recyclerView.setLayoutManager(null); } } View oldView = getRecyclerViewContainer(); int index = indexOfChild(oldView); removeView(oldView); int layout = showTabs ? R.layout.all_apps_tabs : R.layout.all_apps_rv_layout; View newView = LayoutInflater.from(getContext()).inflate(layout, this, false); addView(newView, index); if (showTabs) { mViewPager = (AllAppsPagedView) newView; mViewPager.initParentViews(this); mViewPager.getPageIndicator().setContainerView(this); } else { mViewPager = null; } } public View getRecyclerViewContainer() { return mViewPager != null ? mViewPager : findViewById(R.id.apps_list_view); } public void onTabChanged(int pos) { mHeader.setMainActive(pos == 0); reset(); applyTouchDelegate(); if (mAH[pos].recyclerView != null) { mAH[pos].recyclerView.bindFastScrollbar(); findViewById(R.id.tab_personal) .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.MAIN)); findViewById(R.id.tab_work) .setOnClickListener((View view) -> mViewPager.snapToPage(AdapterHolder.WORK)); } if (pos == AdapterHolder.WORK) { BottomUserEducationView.showIfNeeded(mLauncher); } } public AlphabeticalAppsList getApps() { return mAH[AdapterHolder.MAIN].appsList; } public FloatingHeaderView getFloatingHeaderView() { return mHeader; } public void setupHeader() { mHeader.setVisibility(View.VISIBLE); mHeader.setup(mAH, mAH[AllAppsContainerView.AdapterHolder.WORK].recyclerView == null); int padding = mHeader.getMaxTranslation(); for (int i = 0; i < mAH.length; i++) { mAH[i].padding.top = padding; mAH[i].applyPadding(); } } public void setLastSearchQuery(String query) { for (int i = 0; i < mAH.length; i++) { mAH[i].adapter.setLastSearchQuery(query); } if (mUsingTabs) { mSearchModeWhileUsingTabs = true; rebindAdapters(false); // hide tabs } } public void onClearSearchResult() { if (mSearchModeWhileUsingTabs) { rebindAdapters(true); // show tabs mSearchModeWhileUsingTabs = false; } } public void onSearchResultsChanged() { for (int i = 0; i < mAH.length; i++) { if (mAH[i].recyclerView != null) { mAH[i].recyclerView.onSearchResultsChanged(); } } } public void setRecyclerViewVerticalFadingEdgeEnabled(boolean enabled) { for (int i = 0; i < mAH.length; i++) { mAH[i].applyVerticalFadingEdgeEnabled(enabled); } } public void addElevationController(RecyclerView.OnScrollListener scrollListener) { if (!mUsingTabs) { mAH[AdapterHolder.MAIN].recyclerView.addOnScrollListener(scrollListener); } } public boolean isHeaderVisible() { return mHeader != null && mHeader.getVisibility() == View.VISIBLE; } public void onScrollUpEnd() { if (mUsingTabs) { ((PersonalWorkSlidingTabStrip) findViewById(R.id.tabs)).highlightWorkTabIfNecessary(); } } public class AdapterHolder { public static final int MAIN = 0; public static final int WORK = 1; public final AllAppsGridAdapter adapter; final LinearLayoutManager layoutManager; final SpringAnimationHandler animationHandler; final AlphabeticalAppsList appsList; final Rect padding = new Rect(); AllAppsRecyclerView recyclerView; boolean verticalFadingEdge; AdapterHolder(boolean isWork) { appsList = new AlphabeticalAppsList(mLauncher, mAllAppsStore, isWork); adapter = new AllAppsGridAdapter(mLauncher, appsList, mLauncher, AllAppsContainerView.this, true); appsList.setAdapter(adapter); animationHandler = adapter.getSpringAnimationHandler(); layoutManager = adapter.getLayoutManager(); } void setup(@NonNull View rv, @Nullable ItemInfoMatcher matcher) { appsList.updateItemFilter(matcher); recyclerView = (AllAppsRecyclerView) rv; recyclerView.setApps(appsList, mUsingTabs); recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(adapter); recyclerView.setHasFixedSize(true); // No animations will occur when changes occur to the items in this RecyclerView. recyclerView.setItemAnimator(null); if (FeatureFlags.LAUNCHER3_PHYSICS && animationHandler != null) { recyclerView.setSpringAnimationHandler(animationHandler); } FocusedItemDecorator focusedItemDecorator = new FocusedItemDecorator(recyclerView); recyclerView.addItemDecoration(focusedItemDecorator); adapter.setIconFocusListener(focusedItemDecorator.getFocusListener()); applyVerticalFadingEdgeEnabled(verticalFadingEdge); applyPadding(); } void applyPadding() { if (recyclerView != null) { recyclerView.setPadding(padding.left, padding.top, padding.right, padding.bottom); } } public void applyVerticalFadingEdgeEnabled(boolean enabled) { verticalFadingEdge = enabled; mAH[AdapterHolder.MAIN].recyclerView.setVerticalFadingEdgeEnabled(!mUsingTabs && verticalFadingEdge); } } }