blob: cabacec3c19d02333ff72dec8f9bd413a27055b6 [file] [log] [blame]
package com.android.launcher3;
import android.content.ComponentName;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ListView;
import android.widget.SectionIndexer;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Represents a row in the apps list view.
*/
class AppsRow {
int sectionId;
String sectionDescription;
List<AppInfo> apps;
public AppsRow(int sId, String sc, List<AppInfo> ai) {
sectionId = sId;
sectionDescription = sc;
apps = ai;
}
public AppsRow(int sId, List<AppInfo> ai) {
sectionId = sId;
apps = ai;
}
}
/**
* An interface to an algorithm that generates app rows.
*/
interface AppRowAlgorithm {
public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow);
public int getIconViewLayoutId();
public int getRowViewLayoutId();
public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info);
}
/**
* Computes the rows in the apps list view.
*/
class SectionedAppsAlgorithm implements AppRowAlgorithm {
@Override
public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) {
List<AppsRow> rows = new ArrayList<>();
LinkedHashMap<String, List<AppInfo>> sections = computeSectionedApps(sortedApps);
int sectionId = 0;
for (Map.Entry<String, List<AppInfo>> sectionEntry : sections.entrySet()) {
String section = sectionEntry.getKey();
List<AppInfo> apps = sectionEntry.getValue();
int numRows = (int) Math.ceil((float) apps.size() / appsPerRow);
for (int i = 0; i < numRows; i++) {
List<AppInfo> appsInRow = new ArrayList<>();
int offset = i * appsPerRow;
for (int j = 0; j < appsPerRow; j++) {
if (offset + j < apps.size()) {
appsInRow.add(apps.get(offset + j));
}
}
if (i == 0) {
rows.add(new AppsRow(sectionId, section, appsInRow));
} else {
rows.add(new AppsRow(sectionId, appsInRow));
}
}
sectionId++;
}
return rows;
}
@Override
public int getIconViewLayoutId() {
return R.layout.apps_grid_row_icon_view;
}
@Override
public int getRowViewLayoutId() {
return R.layout.apps_grid_row_view;
}
private LinkedHashMap<String, List<AppInfo>> computeSectionedApps(List<AppInfo> sortedApps) {
LinkedHashMap<String, List<AppInfo>> sections = new LinkedHashMap<>();
for (AppInfo info : sortedApps) {
String section = getSection(info);
List<AppInfo> sectionApps = sections.get(section);
if (sectionApps == null) {
sectionApps = new ArrayList<>();
sections.put(section, sectionApps);
}
sectionApps.add(info);
}
return sections;
}
@Override
public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) {
icon.applyFromApplicationInfo(info);
}
private String getSection(AppInfo app) {
return app.title.toString().substring(0, 1).toLowerCase();
}
}
/**
* Computes the rows in the apps grid view.
*/
class ListedAppsAlgorithm implements AppRowAlgorithm {
@Override
public List<AppsRow> computeAppRows(List<AppInfo> sortedApps, int appsPerRow) {
List<AppsRow> rows = new ArrayList<>();
int sectionId = -1;
String prevSection = "";
for (AppInfo info : sortedApps) {
List<AppInfo> appsInRow = new ArrayList<>();
appsInRow.add(info);
String section = getSection(info);
if (!prevSection.equals(section)) {
prevSection = section;
sectionId++;
rows.add(new AppsRow(sectionId, section, appsInRow));
} else {
rows.add(new AppsRow(sectionId, appsInRow));
}
}
return rows;
}
@Override
public int getIconViewLayoutId() {
return R.layout.apps_list_row_icon_view;
}
@Override
public int getRowViewLayoutId() {
return R.layout.apps_list_row_view;
}
@Override
public void bindRowViewIconToInfo(BubbleTextView icon, AppInfo info) {
icon.applyFromApplicationInfo(info);
}
private String getSection(AppInfo app) {
return app.title.toString().substring(0, 1).toLowerCase();
}
}
/**
* The adapter of all the apps
*/
class AppsListAdapter extends BaseAdapter implements SectionIndexer {
private LayoutInflater mLayoutInflater;
private List<AppsRow> mAppRows = new ArrayList<>();
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
private AppRowAlgorithm mRowAlgorithm;
private int mAppsPerRow;
public AppsListAdapter(Context context, View.OnTouchListener touchListener,
View.OnClickListener iconClickListener, View.OnLongClickListener iconLongClickListener) {
mLayoutInflater = LayoutInflater.from(context);
mTouchListener = touchListener;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
}
void setApps(List<AppsRow> apps, int appsPerRow, AppRowAlgorithm algo) {
mAppsPerRow = appsPerRow;
mRowAlgorithm = algo;
mAppRows.clear();
mAppRows.addAll(apps);
notifyDataSetChanged();
}
@Override
public int getCount() {
return mAppRows.size();
}
@Override
public Object getItem(int position) {
return mAppRows.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
AppsRow info = mAppRows.get(position);
ViewGroup row = (ViewGroup) convertView;
if (row == null) {
// Inflate the row and all the icon children necessary
row = (ViewGroup) mLayoutInflater.inflate(mRowAlgorithm.getRowViewLayoutId(),
parent, false);
for (int i = 0; i < mAppsPerRow; i++) {
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
mRowAlgorithm.getIconViewLayoutId(), row, false);
LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(0,
ViewGroup.LayoutParams.WRAP_CONTENT, 1);
lp.gravity = Gravity.CENTER_VERTICAL;
icon.setLayoutParams(lp);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setFocusable(true);
row.addView(icon);
}
}
// Bind the section header
TextView tv = (TextView) row.findViewById(R.id.section);
if (info.sectionDescription != null) {
tv.setText(info.sectionDescription);
tv.setVisibility(View.VISIBLE);
} else {
tv.setVisibility(View.INVISIBLE);
}
// Bind the icons
for (int i = 0; i < mAppsPerRow; i++) {
BubbleTextView icon = (BubbleTextView) row.getChildAt(i + 1);
if (i < info.apps.size()) {
mRowAlgorithm.bindRowViewIconToInfo(icon, info.apps.get(i));
icon.setVisibility(View.VISIBLE);
} else {
icon.setVisibility(View.INVISIBLE);
}
}
return row;
}
@Override
public Object[] getSections() {
ArrayList<Object> sections = new ArrayList<>();
int prevSectionId = -1;
for (AppsRow row : mAppRows) {
if (row.sectionId != prevSectionId) {
sections.add(row.sectionDescription.toUpperCase());
prevSectionId = row.sectionId;
}
}
return sections.toArray();
}
@Override
public int getPositionForSection(int sectionIndex) {
for (int i = 0; i < mAppRows.size(); i++) {
AppsRow row = mAppRows.get(i);
if (row.sectionId == sectionIndex) {
return i;
}
}
return 0;
}
@Override
public int getSectionForPosition(int position) {
return mAppRows.get(position).sectionId;
}
}
/**
* The alphabetically sorted list of applications.
*/
class AlphabeticalAppList {
/**
* Callbacks for when this list is modified.
*/
public interface Callbacks {
public void onAppsUpdated();
}
private List<AppInfo> mApps;
private Callbacks mCb;
public AlphabeticalAppList(Callbacks cb) {
mCb = cb;
}
/**
* Returns the list of applications.
*/
public List<AppInfo> getApps() {
return mApps;
}
/**
* Sets the current set of apps.
*/
public void setApps(List<AppInfo> apps) {
Collections.sort(apps, LauncherModel.getAppNameComparator());
mApps = apps;
mCb.onAppsUpdated();
}
/**
* Adds new apps to the list.
*/
public void addApps(List<AppInfo> apps) {
// We add it in place, in alphabetical order
Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
for (AppInfo info : apps) {
// This call will return the exact index of where the item is if >= 0, or the index
// where it should be inserted if < 0.
int index = Collections.binarySearch(mApps, info, appNameComparator);
if (index < 0) {
mApps.add(-(index + 1), info);
}
}
mCb.onAppsUpdated();
}
/**
* Updates existing apps in the list
*/
public void updateApps(List<AppInfo> apps) {
Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
for (AppInfo info : apps) {
int index = mApps.indexOf(info);
if (index != -1) {
mApps.set(index, info);
} else {
index = Collections.binarySearch(mApps, info, appNameComparator);
if (index < 0) {
mApps.add(-(index + 1), info);
}
}
}
mCb.onAppsUpdated();
}
/**
* Removes some apps from the list.
*/
public void removeApps(List<AppInfo> apps) {
for (AppInfo info : apps) {
int removeIndex = findAppByComponent(mApps, info);
if (removeIndex != -1) {
mApps.remove(removeIndex);
}
}
mCb.onAppsUpdated();
}
/**
* Finds the index of an app given a target AppInfo.
*/
private int findAppByComponent(List<AppInfo> apps, AppInfo targetInfo) {
ComponentName targetComponent = targetInfo.intent.getComponent();
int length = apps.size();
for (int i = 0; i < length; ++i) {
AppInfo info = apps.get(i);
if (info.user.equals(info.user)
&& info.intent.getComponent().equals(targetComponent)) {
return i;
}
}
return -1;
}
}
/**
* The all apps list view container.
*/
public class AppsContainerView extends FrameLayout implements DragSource, View.OnTouchListener,
View.OnLongClickListener, Insettable, AlphabeticalAppList.Callbacks {
static final int GRID_LAYOUT = 0;
static final int LIST_LAYOUT = 1;
static final int USE_LAYOUT = LIST_LAYOUT;
private Launcher mLauncher;
private AppRowAlgorithm mAppRowsAlgorithm;
private AppsListAdapter mAdapter;
private AlphabeticalAppList mApps;
private ListView mList;
private int mAppsRowSize;
private Point mLastTouchDownPos = new Point();
private Rect mPadding = new Rect();
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) {
this(context, attrs, defStyleAttr, 0);
}
public AppsContainerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
LauncherAppState app = LauncherAppState.getInstance();
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
mLauncher = (Launcher) context;
if (USE_LAYOUT == GRID_LAYOUT) {
mAppRowsAlgorithm = new SectionedAppsAlgorithm();
mAppsRowSize = grid.allAppsRowsSize;
} else if (USE_LAYOUT == LIST_LAYOUT) {
mAppRowsAlgorithm = new ListedAppsAlgorithm();
mAppsRowSize = 1;
}
mAdapter = new AppsListAdapter(context, this, mLauncher, this);
mApps = new AlphabeticalAppList(this);
}
/**
* 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);
}
/**
* Scrolls this list view to the top.
*/
public void scrollToTop() {
mList.scrollTo(0, 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.all_apps_transition_overlay);
}
@Override
public void onAppsUpdated() {
List<AppsRow> rows = mAppRowsAlgorithm.computeAppRows(mApps.getApps(), mAppsRowSize);
mAdapter.setApps(rows, mAppsRowSize, mAppRowsAlgorithm);
}
@Override
protected void onFinishInflate() {
mList = (ListView) findViewById(R.id.apps_list);
mList.setFastScrollEnabled(true);
mList.setFastScrollAlwaysVisible(true);
mList.setItemsCanFocus(true);
mList.setAdapter(mAdapter);
mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(), getPaddingBottom());
}
@Override
public void setInsets(Rect insets) {
setPadding(mPadding.left + insets.left, mPadding.top + insets.top,
mPadding.right + insets.right, mPadding.bottom + insets.bottom);
}
@Override
public boolean onTouch(View v, MotionEvent event) {
if (event.getAction() == MotionEvent.ACTION_DOWN ||
event.getAction() == MotionEvent.ACTION_MOVE) {
mLastTouchDownPos.set((int) event.getX(), (int) event.getY());
}
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;
}
}
}