blob: 559f6eb8388abb27984b4a27247d5bb7588a6a89 [file] [log] [blame]
/*
* 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;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Point;
import android.graphics.Rect;
import android.graphics.drawable.InsetDrawable;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.launcher3.util.Thunk;
import java.util.List;
/**
* The all apps list view container.
*/
public class AppsContainerView extends FrameLayout implements DragSource, Insettable, TextWatcher,
TextView.OnEditorActionListener, LauncherTransitionable, View.OnTouchListener,
View.OnLongClickListener {
private static final boolean ALLOW_SINGLE_APP_LAUNCH = true;
private static final int GRID_LAYOUT = 0;
private static final int LIST_LAYOUT = 1;
private static final int USE_LAYOUT = GRID_LAYOUT;
@Thunk Launcher mLauncher;
@Thunk AlphabeticalAppsList mApps;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private RecyclerView.ItemDecoration mItemDecoration;
@Thunk AppsContainerRecyclerView mAppsListView;
private EditText mSearchBar;
private int mNumAppsPerRow;
private Point mLastTouchDownPos = new Point();
private Rect mInsets = new Rect();
private Rect mFixedBounds = new Rect();
private int mContentMarginStart;
// Normal container insets
private int mContainerInset;
// Fixed bounds container insets
private int mFixedBoundsContainerInset;
public AppsContainerView(Context context) {
this(context, null);
}
public AppsContainerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
Resources res = context.getResources();
mContainerInset = context.getResources().getDimensionPixelSize(
R.dimen.apps_container_inset);
mFixedBoundsContainerInset = context.getResources().getDimensionPixelSize(
R.dimen.apps_container_fixed_bounds_inset);
mLauncher = (Launcher) context;
mApps = new AlphabeticalAppsList(context);
if (USE_LAYOUT == GRID_LAYOUT) {
mNumAppsPerRow = grid.appsViewNumCols;
AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
mLauncher, this);
adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
mLayoutManager = adapter.getLayoutManager(context);
mItemDecoration = adapter.getItemDecoration();
mAdapter = adapter;
mContentMarginStart = adapter.getContentMarginStart();
} else if (USE_LAYOUT == LIST_LAYOUT) {
mNumAppsPerRow = 1;
AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
adapter.setEmptySearchText(res.getString(R.string.loading_apps_message));
mLayoutManager = adapter.getLayoutManager(context);
mAdapter = adapter;
}
mApps.setAdapter(mAdapter);
}
/**
* Sets the current set of apps.
*/
public void setApps(List<AppInfo> apps) {
mApps.setApps(apps);
}
/**
* Adds new apps to the list.
*/
public void addApps(List<AppInfo> apps) {
mApps.addApps(apps);
}
/**
* Updates existing apps in the list
*/
public void updateApps(List<AppInfo> apps) {
mApps.updateApps(apps);
}
/**
* Removes some apps from the list.
*/
public void removeApps(List<AppInfo> apps) {
mApps.removeApps(apps);
}
/**
* Hides the search bar
*/
public void hideSearchBar() {
mSearchBar.setVisibility(View.GONE);
updateBackgrounds();
updatePaddings();
}
/**
* Scrolls this list view to the top.
*/
public void scrollToTop() {
mAppsListView.scrollToPosition(0);
}
/**
* Returns the content view used for the launcher transitions.
*/
public View getContentView() {
return findViewById(R.id.apps_list);
}
/**
* Returns the reveal view used for the launcher transitions.
*/
public View getRevealView() {
return findViewById(R.id.apps_view_transition_overlay);
}
@Override
protected void onFinishInflate() {
boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL);
if (USE_LAYOUT == GRID_LAYOUT) {
((AppsGridAdapter) mAdapter).setRtl(isRtl);
}
mSearchBar = (EditText) findViewById(R.id.app_search_box);
if (mSearchBar != null) {
mSearchBar.addTextChangedListener(this);
mSearchBar.setOnEditorActionListener(this);
}
mAppsListView = (AppsContainerRecyclerView) findViewById(R.id.apps_list_view);
mAppsListView.setApps(mApps);
mAppsListView.setNumAppsPerRow(mNumAppsPerRow);
mAppsListView.setLayoutManager(mLayoutManager);
mAppsListView.setAdapter(mAdapter);
mAppsListView.setHasFixedSize(true);
if (mItemDecoration != null) {
mAppsListView.addItemDecoration(mItemDecoration);
}
updateBackgrounds();
updatePaddings();
}
@Override
public void setInsets(Rect insets) {
mInsets.set(insets);
updatePaddings();
}
/**
* Sets the fixed bounds for this Apps view.
*/
public void setFixedBounds(Context context, Rect fixedBounds) {
if (!fixedBounds.isEmpty() && !fixedBounds.equals(mFixedBounds)) {
// Update the number of items in the grid
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
if (grid.updateAppsViewNumCols(context.getResources(), fixedBounds.width())) {
mNumAppsPerRow = grid.appsViewNumCols;
mAppsListView.setNumAppsPerRow(mNumAppsPerRow);
if (USE_LAYOUT == GRID_LAYOUT) {
((AppsGridAdapter) mAdapter).setNumAppsPerRow(mNumAppsPerRow);
}
}
mFixedBounds.set(fixedBounds);
}
updateBackgrounds();
updatePaddings();
}
@Override
public boolean onTouch(View v, MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
case MotionEvent.ACTION_MOVE:
mLastTouchDownPos.set((int) ev.getX(), (int) ev.getY());
break;
}
return false;
}
@Override
public boolean onLongClick(View v) {
// Return early if this is not initiated from a touch
if (!v.isInTouchMode()) return false;
// 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
if (!mLauncher.isDraggingEnabled()) return false;
// Start the drag
mLauncher.getWorkspace().beginDragShared(v, mLastTouchDownPos, this, false);
// We delay entering spring-loaded mode slightly to make sure the UI
// thready is free of any work.
postDelayed(new Runnable() {
@Override
public void run() {
// We don't enter spring-loaded mode if the drag has been cancelled
if (mLauncher.getDragController().isDragging()) {
// Go into spring loaded mode (must happen before we startDrag())
mLauncher.enterSpringLoadedDragMode();
}
}
}, 150);
return false;
}
@Override
public boolean supportsFlingToDelete() {
return true;
}
@Override
public boolean supportsAppInfoDropTarget() {
return true;
}
@Override
public boolean supportsDeleteDropTarget() {
return true;
}
@Override
public float getIntrinsicIconScaleFactor() {
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
return (float) grid.allAppsIconSizePx / grid.iconSizePx;
}
@Override
public void onFlingToDeleteCompleted() {
// We just dismiss the drag when we fling, so cleanup here
mLauncher.exitSpringLoadedDragModeDelayed(true,
Launcher.EXIT_SPRINGLOADED_MODE_SHORT_TIMEOUT, null);
mLauncher.unlockScreenOrientation(false);
}
@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);
// Display an error message if the drag failed due to there not being enough space on the
// target layout we were dropping on.
if (!success) {
boolean showOutOfSpaceMessage = false;
if (target instanceof Workspace) {
int currentScreen = mLauncher.getCurrentWorkspaceScreen();
Workspace workspace = (Workspace) target;
CellLayout layout = (CellLayout) workspace.getChildAt(currentScreen);
ItemInfo itemInfo = (ItemInfo) d.dragInfo;
if (layout != null) {
layout.calculateSpans(itemInfo);
showOutOfSpaceMessage =
!layout.findCellForSpan(null, itemInfo.spanX, itemInfo.spanY);
}
}
if (showOutOfSpaceMessage) {
mLauncher.showOutOfSpaceMessage(false);
}
d.deferDragViewCleanupPostAnimation = false;
}
}
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
// Do nothing
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
// Do nothing
}
@Override
public void afterTextChanged(final Editable s) {
if (s.toString().isEmpty()) {
mApps.setFilter(null);
} else {
String formatStr = getResources().getString(R.string.apps_view_no_search_results);
if (USE_LAYOUT == GRID_LAYOUT) {
((AppsGridAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
s.toString()));
} else {
((AppsListAdapter) mAdapter).setEmptySearchText(String.format(formatStr,
s.toString()));
}
final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
mApps.setFilter(new AlphabeticalAppsList.Filter() {
@Override
public boolean retainApp(AppInfo info, String sectionName) {
String title = info.title.toString();
return sectionName.toLowerCase().contains(filterText) ||
title.toLowerCase().replaceAll("\\s+", "").contains(filterText);
}
});
}
}
@Override
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
if (ALLOW_SINGLE_APP_LAUNCH && actionId == EditorInfo.IME_ACTION_DONE) {
// Skip the quick-launch if there isn't exactly one item
if (mApps.getSize() != 1) {
return false;
}
List<AlphabeticalAppsList.AdapterItem> items = mApps.getAdapterItems();
for (int i = 0; i < items.size(); i++) {
AlphabeticalAppsList.AdapterItem item = items.get(i);
if (!item.isSectionHeader) {
mAppsListView.getChildAt(i).performClick();
InputMethodManager imm = (InputMethodManager)
getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
imm.hideSoftInputFromWindow(getWindowToken(), 0);
return true;
}
}
}
return false;
}
@Override
public View getContent() {
return null;
}
@Override
public void onLauncherTransitionPrepare(Launcher l, boolean animated, boolean toWorkspace) {
if (!toWorkspace) {
// Disable the focus so that the search bar doesn't get focus
if (mSearchBar != null) {
mSearchBar.setFocusableInTouchMode(false);
}
}
}
@Override
public void onLauncherTransitionStart(Launcher l, boolean animated, boolean toWorkspace) {
// Do nothing
}
@Override
public void onLauncherTransitionStep(Launcher l, float t) {
// Do nothing
}
@Override
public void onLauncherTransitionEnd(Launcher l, boolean animated, boolean toWorkspace) {
if (mSearchBar != null) {
if (toWorkspace) {
// Clear the search bar
mSearchBar.setText("");
} else {
mSearchBar.setFocusableInTouchMode(true);
}
}
}
/**
* Update the padding of the Apps view and children. To ensure that the RecyclerView has the
* full width to handle touches right to the edge of the screen, we only apply the top and
* bottom padding to the AppsContainerView and then the left/right padding on the RecyclerView
* itself. In particular, the left/right padding is applied to the background of the view,
* and then additionally inset by the start margin.
*/
private void updatePaddings() {
boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL);
boolean hasSearchBar = (mSearchBar != null) && (mSearchBar.getVisibility() == View.VISIBLE);
if (mFixedBounds.isEmpty()) {
// If there are no fixed bounds, then use the default padding and insets
setPadding(mInsets.left, mContainerInset + mInsets.top, mInsets.right,
mContainerInset + mInsets.bottom);
} else {
// If there are fixed bounds, then we update the padding to reflect the fixed bounds.
setPadding(mFixedBounds.left, mFixedBounds.top + mFixedBoundsContainerInset,
getMeasuredWidth() - mFixedBounds.right,
mInsets.bottom + mFixedBoundsContainerInset);
}
// Update the apps recycler view
int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
if (isRtl) {
mAppsListView.setPadding(inset, inset, inset + mContentMarginStart, inset);
} else {
mAppsListView.setPadding(inset + mContentMarginStart, inset, inset, inset);
}
// Update the search bar
if (hasSearchBar) {
LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mSearchBar.getLayoutParams();
lp.leftMargin = lp.rightMargin = inset;
}
}
/**
* Update the background of the Apps view and children.
*/
private void updateBackgrounds() {
int inset = mFixedBounds.isEmpty() ? mContainerInset : mFixedBoundsContainerInset;
boolean hasSearchBar = (mSearchBar != null) && (mSearchBar.getVisibility() == View.VISIBLE);
// Update the background of the reveal view and list to be inset with the fixed bound
// insets instead of the default insets
mAppsListView.setBackground(new InsetDrawable(
getContext().getResources().getDrawable(
hasSearchBar ? R.drawable.apps_list_search_bg : R.drawable.apps_list_bg),
inset, 0, inset, 0));
getRevealView().setBackground(new InsetDrawable(
getContext().getResources().getDrawable(R.drawable.apps_reveal_bg),
inset, 0, inset, 0));
}
}