Merge "Adding app grid layout with fastscroller." into ub-launcher3-burnaby

This commit is contained in:
Winson Chung
2015-03-13 02:01:40 +00:00
committed by Android (Google) Code Review
25 changed files with 1295 additions and 478 deletions
+3
View File
@@ -37,6 +37,9 @@ LOCAL_PROTOC_OPTIMIZE_TYPE := nano
LOCAL_PROTOC_FLAGS := --proto_path=$(LOCAL_PATH)/protos/
LOCAL_SDK_VERSION := 21
LOCAL_STATIC_JAVA_LIBRARIES := \
android-support-v4 \
android-support-v7-recyclerview
LOCAL_PACKAGE_NAME := Launcher3
#LOCAL_CERTIFICATE := shared
+24
View File
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/apps_view_scrollbar_thumb_color" />
<size
android:width="48dp"
android:height="48dp" />
<corners android:radius="4dp" />
</shape>
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/apps_view_scrollbar_thumb_color" />
<size android:width="4dp"/>
</shape>
+23
View File
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#ffffff" />
<corners
android:topLeftRadius="3dp"
android:topRightRadius="3dp" />
</shape>
+4 -4
View File
@@ -23,12 +23,12 @@
android:descendantFocusability="afterDescendants">
<include
layout="@layout/apps_list_reveal_view"
android:layout_width="420dp"
android:layout_height="match_parent"
android:layout_width="@dimen/apps_container_width"
android:layout_height="540dp"
android:layout_gravity="center" />
<include
layout="@layout/apps_list_view"
android:layout_width="420dp"
android:layout_height="match_parent"
android:layout_width="@dimen/apps_container_width"
android:layout_height="540dp"
android:layout_gravity="center" />
</com.android.launcher3.AppsContainerView>
+10 -3
View File
@@ -13,10 +13,17 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<com.android.launcher3.BubbleTextView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:launcher="http://schemas.android.com/apk/res-auto"
style="@style/WorkspaceIcon.AppsCustomize"
android:id="@+id/application_icon"
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="left|center_vertical"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:focusable="true"
android:background="@drawable/focusable_view_bg" />
android:background="@drawable/focusable_view_bg"
launcher:deferShadowGeneration="true" />
-38
View File
@@ -1,38 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- 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.
-->
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="@dimen/apps_view_row_height"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:orientation="horizontal"
android:focusable="true"
android:background="@drawable/focusable_view_bg"
android:descendantFocusability="afterDescendants">
<TextView
android:id="@+id/section"
android:layout_width="48dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingRight="8dp"
android:paddingBottom="12dp"
android:gravity="right"
android:textColor="#1ca195"
android:textSize="16sp"
android:textAllCaps="true"
android:focusable="false" />
</LinearLayout>
+1 -1
View File
@@ -15,7 +15,7 @@
-->
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/all_apps_transition_overlay"
android:id="@+id/apps_view_transition_overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
+3 -2
View File
@@ -20,9 +20,10 @@
style="@style/WorkspaceIcon.AppsCustomize"
android:id="@+id/application_icon"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_height="match_parent"
android:focusable="true"
android:background="@drawable/focusable_view_bg"
launcher:iconPaddingOverride="24dp"
launcher:textSizeOverride="16dp"
launcher:layoutHorizontal="true" />
launcher:layoutHorizontal="true"
launcher:deferShadowGeneration="true" />
+3 -4
View File
@@ -26,9 +26,8 @@
android:layout_width="64dp"
android:layout_height="match_parent"
android:paddingLeft="16dp"
android:gravity="left|center_vertical"
android:textColor="#009688"
android:textSize="24sp"
android:textAllCaps="true"
android:gravity="start|center_vertical"
android:textColor="@color/apps_view_section_text_color"
android:textSize="@dimen/apps_view_section_text_size"
android:focusable="false" />
</LinearLayout>
+32 -9
View File
@@ -13,18 +13,41 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
<ListView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/apps_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:clipToPadding="false"
android:scrollbars="vertical"
android:orientation="vertical"
android:elevation="15dp"
android:background="@drawable/apps_list_bg"
android:visibility="gone"
android:focusable="true"
android:descendantFocusability="afterDescendants" />
android:visibility="gone">
<EditText
android:id="@+id/app_search_box"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:hint="@string/apps_view_search_bar_hint"
android:maxLines="1"
android:singleLine="true"
android:scrollHorizontally="true"
android:gravity="fill_horizontal"
android:textSize="16sp"
android:textColor="#4c4c4c"
android:textColorHint="#9c9c9c"
android:imeOptions="flagNoExtractUi"
android:background="@drawable/apps_list_search_bg"
android:elevation="4dp" />
<com.android.launcher3.AppsContainerRecyclerView
android:id="@+id/apps_list_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="center"
android:paddingTop="12dp"
android:paddingBottom="12dp"
android:clipToPadding="false"
android:scrollbars="vertical"
android:scrollbarThumbVertical="@drawable/apps_list_scrollbar_thumb"
android:focusable="true"
android:descendantFocusability="afterDescendants" />
</LinearLayout>
+1 -1
View File
@@ -19,7 +19,7 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="8dp"
android:background="#22000000"
android:background="@drawable/apps_customize_bg"
android:descendantFocusability="afterDescendants">
<include
layout="@layout/apps_list_reveal_view" />
+1
View File
@@ -18,6 +18,7 @@
<dimen name="app_icon_size">64dp</dimen>
<!-- Apps view -->
<dimen name="apps_container_width">480dp</dimen>
<dimen name="apps_view_row_height">76dp</dimen>
<!-- AppsCustomize -->
+1
View File
@@ -24,6 +24,7 @@
<attr name="iconSizeOverride" format="dimension" />
<attr name="iconPaddingOverride" format="dimension" />
<attr name="textSizeOverride" format="dimension" />
<attr name="deferShadowGeneration" format="boolean" />
</declare-styleable>
<!-- Page Indicator specific attributes. -->
+4
View File
@@ -36,4 +36,8 @@
<color name="outline_color">#FFFFFFFF</color>
<color name="widget_text_panel">#FF374248</color>
<!-- Apps view -->
<color name="apps_view_scrollbar_thumb_color">#009688</color>
<color name="apps_view_section_text_color">#009688</color>
</resources>
+6
View File
@@ -47,7 +47,13 @@
<dimen name="toolbar_button_horizontal_padding">12dip</dimen>
<!-- Apps view -->
<dimen name="apps_container_width">0dp</dimen>
<dimen name="apps_grid_view_start_margin">52dp</dimen>
<dimen name="apps_view_row_height">64dp</dimen>
<dimen name="apps_view_section_text_size">24sp</dimen>
<dimen name="apps_view_fast_scroll_gutter_size">48dp</dimen>
<dimen name="apps_view_fast_scroll_popup_size">64dp</dimen>
<dimen name="apps_view_fast_scroll_text_size">48dp</dimen>
<!-- AllApps/Customize/AppsCustomize -->
<!-- The height of the tab bar - if this changes, we should update the
+4
View File
@@ -71,6 +71,10 @@
drop if there are multiple choices. [CHAR_LIMIT=35] -->
<string name="external_drop_widget_pick_title">Choose widget to create</string>
<!-- Apps view -->
<!-- Search bar text in the apps view. [CHAR_LIMIT=50] -->
<string name="apps_view_search_bar_hint">Search Apps</string>
<!-- Folders -->
<skip />
<!-- Label of Folder name field in Rename folder dialog box -->
@@ -0,0 +1,242 @@
package com.android.launcher3;
import android.content.ComponentName;
import android.content.Context;
import android.support.v7.widget.RecyclerView;
import com.android.launcher3.compat.AlphabeticIndexCompat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* The alphabetically sorted list of applications.
*/
public class AlphabeticalAppsList {
/**
* Info about a section in the alphabetic list
*/
public class SectionInfo {
public String sectionName;
public int numAppsInSection;
}
/**
* A filter interface to limit the set of applications in the apps list.
*/
public interface Filter {
public boolean retainApp(AppInfo info);
}
// Hack to force RecyclerView to break sections
public static final AppInfo SECTION_BREAK_INFO = null;
private List<AppInfo> mApps = new ArrayList<>();
private List<AppInfo> mFilteredApps = new ArrayList<>();
private List<AppInfo> mSectionedFilteredApps = new ArrayList<>();
private List<SectionInfo> mSections = new ArrayList<>();
private RecyclerView.Adapter mAdapter;
private Filter mFilter;
private AlphabeticIndexCompat mIndexer;
public AlphabeticalAppsList(Context context) {
mIndexer = new AlphabeticIndexCompat(context);
}
/**
* Sets the adapter to notify when this dataset changes.
*/
public void setAdapter(RecyclerView.Adapter adapter) {
mAdapter = adapter;
}
/**
* Returns sections of all the current filtered applications.
*/
public List<SectionInfo> getSections() {
return mSections;
}
/**
* Returns the current filtered list of applications broken down into their sections.
*/
public List<AppInfo> getApps() {
return mSectionedFilteredApps;
}
/**
* Returns the current filtered list of applications.
*/
public List<AppInfo> getAppsWithoutSectionBreaks() {
return mFilteredApps;
}
/**
* Returns the section name for the application.
*/
public String getSectionNameForApp(AppInfo info) {
String title = info.title.toString();
String sectionName = mIndexer.getBucketLabel(mIndexer.getBucketIndex(title));
return sectionName;
}
/**
* Returns the indexer for this locale.
*/
public AlphabeticIndexCompat getIndexer() {
return mIndexer;
}
/**
* Sets the current filter for this list of apps.
*/
public void setFilter(Filter f) {
mFilter = f;
onAppsUpdated();
mAdapter.notifyDataSetChanged();
}
/**
* Sets the current set of apps.
*/
public void setApps(List<AppInfo> apps) {
Collections.sort(apps, LauncherModel.getAppNameComparator());
mApps.clear();
mApps.addAll(apps);
onAppsUpdated();
mAdapter.notifyDataSetChanged();
}
/**
* Adds new apps to the list.
*/
public void addApps(List<AppInfo> apps) {
// We add it in place, in alphabetical order
for (AppInfo info : apps) {
addApp(info);
}
}
/**
* Updates existing apps in the list
*/
public void updateApps(List<AppInfo> apps) {
for (AppInfo info : apps) {
int index = mApps.indexOf(info);
if (index != -1) {
mApps.set(index, info);
onAppsUpdated();
mAdapter.notifyItemChanged(index);
} else {
addApp(info);
}
}
}
/**
* Removes some apps from the list.
*/
public void removeApps(List<AppInfo> apps) {
for (AppInfo info : apps) {
int removeIndex = findAppByComponent(mApps, info);
if (removeIndex != -1) {
int sectionedIndex = mSectionedFilteredApps.indexOf(info);
int numAppsInSection = numAppsInSection(info);
mApps.remove(removeIndex);
onAppsUpdated();
if (numAppsInSection == 1) {
// Remove the section and the icon
mAdapter.notifyItemRemoved(sectionedIndex - 1);
mAdapter.notifyItemRemoved(sectionedIndex - 1);
} else {
mAdapter.notifyItemRemoved(sectionedIndex);
}
}
}
}
/**
* 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;
}
/**
* Implementation to actually add an app to the alphabetic list
*/
private void addApp(AppInfo info) {
Comparator<AppInfo> appNameComparator = LauncherModel.getAppNameComparator();
int index = Collections.binarySearch(mApps, info, appNameComparator);
if (index < 0) {
mApps.add(-(index + 1), info);
onAppsUpdated();
int sectionedIndex = mSectionedFilteredApps.indexOf(info);
int numAppsInSection = numAppsInSection(info);
if (numAppsInSection == 1) {
// New section added along with icon
mAdapter.notifyItemInserted(sectionedIndex - 1);
mAdapter.notifyItemInserted(sectionedIndex - 1);
} else {
mAdapter.notifyItemInserted(sectionedIndex);
}
}
}
/**
* Returns the number of apps in the section that the given info is in.
*/
private int numAppsInSection(AppInfo info) {
int appIndex = mFilteredApps.indexOf(info);
int appCount = 0;
for (SectionInfo section : mSections) {
if (appCount + section.numAppsInSection > appIndex) {
return section.numAppsInSection;
}
appCount += section.numAppsInSection;
}
return 1;
}
/**
* Updates internals when the set of apps are updated.
*/
private void onAppsUpdated() {
// Recreate the filtered apps
mFilteredApps.clear();
for (AppInfo info : mApps) {
if (mFilter == null || mFilter.retainApp(info)) {
mFilteredApps.add(info);
}
}
// Section the apps (for convenience for the grid layout)
mSections.clear();
mSectionedFilteredApps.clear();
SectionInfo lastSectionInfo = null;
for (AppInfo info : mFilteredApps) {
String sectionName = getSectionNameForApp(info);
if (lastSectionInfo == null || !lastSectionInfo.sectionName.equals(sectionName)) {
lastSectionInfo = new SectionInfo();
lastSectionInfo.sectionName = sectionName;
mSectionedFilteredApps.add(SECTION_BREAK_INFO);
mSections.add(lastSectionInfo);
}
lastSectionInfo.numAppsInSection++;
mSectionedFilteredApps.add(info);
}
}
}
@@ -0,0 +1,271 @@
/*
* 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.animation.ObjectAnimator;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.support.v7.widget.RecyclerView;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.ViewConfiguration;
import java.util.List;
/**
* A RecyclerView with custom fastscroll support. This is the main container for the all apps
* icons.
*/
public class AppsContainerRecyclerView extends RecyclerView
implements RecyclerView.OnItemTouchListener {
private AlphabeticalAppsList mApps;
private int mNumAppsPerRow;
private Drawable mFastScrollerBg;
private boolean mDraggingFastScroller;
private String mFastScrollSectionName;
private Paint mFastScrollTextPaint;
private Rect mFastScrollTextBounds = new Rect();
private float mFastScrollAlpha;
private int mDownX;
private int mDownY;
private int mLastX;
private int mLastY;
private int mGutterSize;
public AppsContainerRecyclerView(Context context) {
this(context, null);
}
public AppsContainerRecyclerView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr) {
this(context, attrs, defStyleAttr, 0);
}
public AppsContainerRecyclerView(Context context, AttributeSet attrs, int defStyleAttr,
int defStyleRes) {
super(context, attrs, defStyleAttr);
Resources res = context.getResources();
int fastScrollerSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_popup_size);
mFastScrollerBg = res.getDrawable(R.drawable.apps_list_fastscroll_bg);
mFastScrollerBg.setBounds(0, 0, fastScrollerSize, fastScrollerSize);
mFastScrollTextPaint = new Paint();
mFastScrollTextPaint.setColor(Color.WHITE);
mFastScrollTextPaint.setAntiAlias(true);
mFastScrollTextPaint.setTextSize(res.getDimensionPixelSize(
R.dimen.apps_view_fast_scroll_text_size));
mGutterSize = res.getDimensionPixelSize(R.dimen.apps_view_fast_scroll_gutter_size);
setFastScrollerAlpha(getFastScrollerAlpha());
}
/**
* Sets the list of apps in this view, used to determine the fastscroll position.
*/
public void setApps(AlphabeticalAppsList apps) {
mApps = apps;
}
/**
* Sets the number of apps per row in this recycler view.
*/
public void setNumAppsPerRow(int rowSize) {
mNumAppsPerRow = rowSize;
}
/**
* Sets the fast scroller alpha.
*/
public void setFastScrollerAlpha(float alpha) {
mFastScrollAlpha = alpha;
invalidateFastScroller();
}
/**
* Gets the fast scroller alpha.
*/
public float getFastScrollerAlpha() {
return mFastScrollAlpha;
}
@Override
protected void onFinishInflate() {
addOnItemTouchListener(this);
}
@Override
protected void dispatchDraw(Canvas canvas) {
super.dispatchDraw(canvas);
if (mFastScrollAlpha > 0f) {
boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL);
Rect bgBounds = mFastScrollerBg.getBounds();
int restoreCount = canvas.save(Canvas.MATRIX_SAVE_FLAG);
int x;
if (isRtl) {
x = getPaddingLeft() + getScrollBarSize();
} else {
x = getWidth() - getPaddingRight() - getScrollBarSize() - bgBounds.width();
}
int y = mLastY - bgBounds.height() / 2;
y = Math.max(getPaddingTop(), Math.min(y, getHeight() - getPaddingBottom() -
bgBounds.height()));
canvas.translate(x, y);
mFastScrollerBg.setAlpha((int) (mFastScrollAlpha * 255));
mFastScrollerBg.draw(canvas);
mFastScrollTextPaint.setAlpha((int) (mFastScrollAlpha * 255));
mFastScrollTextPaint.getTextBounds(mFastScrollSectionName, 0,
mFastScrollSectionName.length(), mFastScrollTextBounds);
canvas.drawText(mFastScrollSectionName,
(bgBounds.width() - mFastScrollTextBounds.width()) / 2,
bgBounds.height() - (bgBounds.height() - mFastScrollTextBounds.height()) / 2,
mFastScrollTextPaint);
canvas.restoreToCount(restoreCount);
}
}
/**
* We intercept the touch handling only to support fast scrolling when initiated from the
* gutter. Otherwise, we fall back to the default RecyclerView touch handling.
*/
@Override
public boolean onInterceptTouchEvent(RecyclerView rv, MotionEvent ev) {
return handleTouchEvent(ev);
}
@Override
public void onTouchEvent(RecyclerView rv, MotionEvent ev) {
handleTouchEvent(ev);
}
/**
* Handles the touch event and determines whether to show the fast scroller (or updates it if
* it is already showing).
*/
private boolean handleTouchEvent(MotionEvent ev) {
ViewConfiguration config = ViewConfiguration.get(getContext());
int action = ev.getAction();
int x = (int) ev.getX();
int y = (int) ev.getY();
switch (action) {
case MotionEvent.ACTION_DOWN:
// Keep track of the down positions
mDownX = mLastX = x;
mDownY = mLastY = y;
stopScroll();
break;
case MotionEvent.ACTION_MOVE:
// Check if we are scrolling
boolean isRtl = (getResources().getConfiguration().getLayoutDirection() ==
LAYOUT_DIRECTION_RTL);
boolean isInGutter;
if (isRtl) {
isInGutter = mDownX < mGutterSize;
} else {
isInGutter = mDownX >= (getWidth() - mGutterSize);
}
if (!mDraggingFastScroller && isInGutter &&
Math.abs(y - mDownY) > config.getScaledTouchSlop()) {
getParent().requestDisallowInterceptTouchEvent(true);
mDraggingFastScroller = true;
animateFastScrollerVisibility(true);
}
if (mDraggingFastScroller) {
mLastX = x;
mLastY = y;
// Scroll to the right position, and update the section name
int top = getPaddingTop() + (mFastScrollerBg.getBounds().height() / 2);
int bottom = getHeight() - getPaddingBottom() -
(mFastScrollerBg.getBounds().height() / 2);
float boundedY = (float) Math.max(top, Math.min(bottom, y));
mFastScrollSectionName = scrollToPositionAtProgress((boundedY - top) /
(bottom - top));
invalidateFastScroller();
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL:
mDraggingFastScroller = false;
animateFastScrollerVisibility(false);
break;
}
return mDraggingFastScroller;
}
/**
* Animates the visibility of the fast scroller popup.
*/
private void animateFastScrollerVisibility(boolean visible) {
ObjectAnimator anim = ObjectAnimator.ofFloat(this, "fastScrollerAlpha", visible ? 1f : 0f);
anim.setDuration(visible ? 200 : 150);
anim.start();
}
/**
* Invalidates the fast scroller popup.
*/
private void invalidateFastScroller() {
invalidate(getWidth() - getPaddingRight() - getScrollBarSize() -
mFastScrollerBg.getIntrinsicWidth(), 0, getWidth(), getHeight());
}
/**
* Maps the progress (from 0..1) to the position that should be visible
*/
private String scrollToPositionAtProgress(float progress) {
List<AlphabeticalAppsList.SectionInfo> sections = mApps.getSections();
// Get the total number of rows
int rowCount = 0;
for (AlphabeticalAppsList.SectionInfo info : sections) {
int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
rowCount += numRowsInSection;
}
// Find the index of the first app in that row and scroll to that position
int rowAtProgress = (int) (progress * rowCount);
int appIndex = 0;
rowCount = 0;
for (AlphabeticalAppsList.SectionInfo info : sections) {
int numRowsInSection = (int) Math.ceil((float) info.numAppsInSection / mNumAppsPerRow);
if (rowCount + numRowsInSection > rowAtProgress) {
appIndex += (rowAtProgress - rowCount) * mNumAppsPerRow;
break;
}
rowCount += numRowsInSection;
appIndex += info.numAppsInSection;
}
appIndex = Math.max(0, Math.min(mApps.getAppsWithoutSectionBreaks().size() - 1, appIndex));
AppInfo appInfo = mApps.getAppsWithoutSectionBreaks().get(appIndex);
int sectionedAppIndex = mApps.getApps().indexOf(appInfo);
scrollToPosition(sectionedAppIndex);
// Returns the section name of the row
return mApps.getSectionNameForApp(appInfo);
}
}
+160 -404
View File
@@ -1,409 +1,64 @@
/*
* 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.ComponentName;
import android.content.Context;
import android.graphics.Point;
import android.graphics.Rect;
import android.support.v7.widget.RecyclerView;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.AttributeSet;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;
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.ListView;
import android.widget.SectionIndexer;
import android.widget.TextView;
import com.android.launcher3.compat.AlphabeticIndexCompat;
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 {
View.OnLongClickListener, Insettable, TextWatcher, TextView.OnEditorActionListener,
LauncherTransitionable {
static final int GRID_LAYOUT = 0;
static final int LIST_LAYOUT = 1;
static final int USE_LAYOUT = LIST_LAYOUT;
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;
private Launcher mLauncher;
private AppRowAlgorithm mAppRowsAlgorithm;
private AppsListAdapter mAdapter;
private AlphabeticalAppList mApps;
private ListView mList;
private int mAppsRowSize;
private AlphabeticalAppsList mApps;
private RecyclerView.Adapter mAdapter;
private RecyclerView.LayoutManager mLayoutManager;
private RecyclerView.ItemDecoration mItemDecoration;
private AppsContainerRecyclerView mAppsListView;
private EditText mSearchBar;
private int mNumAppsPerRow;
private Point mLastTouchDownPos = new Point();
private Rect mPadding = new Rect();
private int mContentMarginStart;
public AppsContainerView(Context context) {
this(context, null);
@@ -423,15 +78,22 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
DeviceProfile grid = app.getDynamicGrid().getDeviceProfile();
mLauncher = (Launcher) context;
mApps = new AlphabeticalAppsList(context);
if (USE_LAYOUT == GRID_LAYOUT) {
mAppRowsAlgorithm = new SectionedAppsAlgorithm();
mAppsRowSize = grid.allAppsRowsSize;
mNumAppsPerRow = grid.appsViewNumCols;
AppsGridAdapter adapter = new AppsGridAdapter(context, mApps, mNumAppsPerRow, this,
mLauncher, this);
mLayoutManager = adapter.getLayoutManager(context);
mItemDecoration = adapter.getItemDecoration();
mAdapter = adapter;
mContentMarginStart = adapter.getContentMarginStart();
} else if (USE_LAYOUT == LIST_LAYOUT) {
mAppRowsAlgorithm = new ListedAppsAlgorithm();
mAppsRowSize = 1;
mNumAppsPerRow = 1;
AppsListAdapter adapter = new AppsListAdapter(context, mApps, this, mLauncher, this);
mLayoutManager = adapter.getLayoutManager(context);
mAdapter = adapter;
}
mAdapter = new AppsListAdapter(context, this, mLauncher, this);
mApps = new AlphabeticalAppList(this);
mApps.setAdapter(mAdapter);
}
/**
@@ -466,7 +128,7 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
* Scrolls this list view to the top.
*/
public void scrollToTop() {
mList.scrollTo(0, 0);
mAppsListView.scrollToPosition(0);
}
/**
@@ -480,23 +142,37 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
* 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);
return findViewById(R.id.apps_view_transition_overlay);
}
@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());
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);
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 (isRtl) {
mAppsListView.setPadding(mAppsListView.getPaddingLeft(), mAppsListView.getPaddingTop(),
mAppsListView.getPaddingRight() + mContentMarginStart, mAppsListView.getPaddingBottom());
} else {
mAppsListView.setPadding(mAppsListView.getPaddingLeft() + mContentMarginStart, mAppsListView.getPaddingTop(),
mAppsListView.getPaddingRight(), mAppsListView.getPaddingBottom());
}
if (mItemDecoration != null) {
mAppsListView.addItemDecoration(mItemDecoration);
}
mPadding.set(getPaddingLeft(), getPaddingTop(), getPaddingRight(),
getPaddingBottom());
}
@Override
@@ -574,7 +250,8 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
}
@Override
public void onDropCompleted(View target, DropTarget.DragObject d, boolean isFlingToDelete, boolean success) {
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
@@ -606,4 +283,83 @@ public class AppsContainerView extends FrameLayout implements DragSource, View.O
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 {
final AlphabeticIndexCompat indexer = mApps.getIndexer();
final String filterText = s.toString().toLowerCase().replaceAll("\\s+", "");
mApps.setFilter(new AlphabeticalAppsList.Filter() {
@Override
public boolean retainApp(AppInfo info) {
String title = info.title.toString();
String sectionName = mApps.getSectionNameForApp(info);
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) {
List<AppInfo> appsWithoutSections = mApps.getAppsWithoutSectionBreaks();
List<AppInfo> apps = mApps.getApps();
if (appsWithoutSections.size() == 1) {
mSearchBar.clearFocus();
mAppsListView.getChildAt(apps.indexOf(appsWithoutSections.get(0))).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
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 (toWorkspace) {
// Clear the search bar
mSearchBar.setText("");
} else {
mSearchBar.setFocusableInTouchMode(true);
}
}
}
@@ -0,0 +1,206 @@
package com.android.launcher3;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.android.launcher3.compat.AlphabeticIndexCompat;
/**
* The grid view adapter of all the apps.
*/
class AppsGridAdapter extends RecyclerView.Adapter<AppsGridAdapter.ViewHolder> {
public static final String TAG = "AppsGridAdapter";
private static final int SECTION_BREAK_VIEW_TYPE = 0;
private static final int ICON_VIEW_TYPE = 1;
/**
* ViewHolder for each icon.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public View mContent;
public boolean mIsSectionRow;
public ViewHolder(View v, boolean isSectionRow) {
super(v);
mContent = v;
mIsSectionRow = isSectionRow;
}
}
/**
* Helper class to size the grid items.
*/
public class GridSpanSizer extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
AppInfo info = mApps.getApps().get(position);
if (info == AlphabeticalAppsList.SECTION_BREAK_INFO) {
return mAppsPerRow;
} else {
return 1;
}
}
}
/**
* Helper class to draw the section headers
*/
public class GridItemDecoration extends RecyclerView.ItemDecoration {
@Override
public void onDraw(Canvas c, RecyclerView parent, RecyclerView.State state) {
AlphabeticIndexCompat indexer = mApps.getIndexer();
for (int i = 0; i < parent.getChildCount(); i++) {
View child = parent.getChildAt(i);
ViewHolder holder = (ViewHolder) parent.getChildViewHolder(child);
if (holder != null) {
GridLayoutManager.LayoutParams lp = (GridLayoutManager.LayoutParams)
child.getLayoutParams();
if (!holder.mIsSectionRow && !lp.isItemRemoved()) {
if (mApps.getApps().get(holder.getPosition() - 1) ==
AlphabeticalAppsList.SECTION_BREAK_INFO) {
// Draw at the parent
AppInfo info = mApps.getApps().get(holder.getPosition());
String section = mApps.getSectionNameForApp(info);
mSectionTextPaint.getTextBounds(section, 0, section.length(),
mTmpBounds);
if (mIsRtl) {
c.drawText(section, parent.getWidth() - mStartMargin +
(mStartMargin - mTmpBounds.width()) / 2,
child.getTop() + (2 * child.getPaddingTop()) +
mTmpBounds.height(), mSectionTextPaint);
} else {
c.drawText(section, (mStartMargin - mTmpBounds.width()) / 2,
child.getTop() + (2 * child.getPaddingTop()) +
mTmpBounds.height(), mSectionTextPaint);
}
}
}
}
}
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent,
RecyclerView.State state) {
// Do nothing
}
}
private LayoutInflater mLayoutInflater;
private AlphabeticalAppsList mApps;
private GridSpanSizer mGridSizer;
private GridItemDecoration mItemDecoration;
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
private int mAppsPerRow;
private boolean mIsRtl;
// Section drawing
private int mStartMargin;
private Paint mSectionTextPaint;
private Rect mTmpBounds = new Rect();
public AppsGridAdapter(Context context, AlphabeticalAppsList apps, int appsPerRow,
View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
View.OnLongClickListener iconLongClickListener) {
Resources res = context.getResources();
mApps = apps;
mAppsPerRow = appsPerRow;
mGridSizer = new GridSpanSizer();
mItemDecoration = new GridItemDecoration();
mLayoutInflater = LayoutInflater.from(context);
mTouchListener = touchListener;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
mStartMargin = res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
mSectionTextPaint = new Paint();
mSectionTextPaint.setTextSize(res.getDimensionPixelSize(
R.dimen.apps_view_section_text_size));
mSectionTextPaint.setColor(res.getColor(R.color.apps_view_section_text_color));
mSectionTextPaint.setAntiAlias(true);
}
/**
* Sets whether we are in RTL mode.
*/
public void setRtl(boolean rtl) {
mIsRtl = rtl;
}
/**
* Returns the grid layout manager.
*/
public GridLayoutManager getLayoutManager(Context context) {
GridLayoutManager layoutMgr = new GridLayoutManager(context, mAppsPerRow,
GridLayoutManager.VERTICAL, false);
layoutMgr.setSpanSizeLookup(mGridSizer);
return layoutMgr;
}
/**
* Returns the item decoration for the recycler view.
*/
public RecyclerView.ItemDecoration getItemDecoration() {
return mItemDecoration;
}
/**
* Returns the left padding for the recycler view.
*/
public int getContentMarginStart() {
return mStartMargin;
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()), true);
case ICON_VIEW_TYPE:
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.apps_grid_row_icon_view, parent, false);
icon.setOnTouchListener(mTouchListener);
icon.setOnClickListener(mIconClickListener);
icon.setOnLongClickListener(mIconLongClickListener);
icon.setFocusable(true);
return new ViewHolder(icon, false);
default:
throw new RuntimeException("Unexpected view type");
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
AppInfo info = mApps.getApps().get(position);
if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
BubbleTextView icon = (BubbleTextView) holder.mContent;
icon.applyFromApplicationInfo(info);
}
}
@Override
public int getItemCount() {
return mApps.getApps().size();
}
@Override
public int getItemViewType(int position) {
if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
return SECTION_BREAK_VIEW_TYPE;
}
return ICON_VIEW_TYPE;
}
}
@@ -0,0 +1,119 @@
package com.android.launcher3;
import android.content.Context;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.android.launcher3.compat.AlphabeticIndexCompat;
/**
* The linear list view adapter for all the apps.
*/
class AppsListAdapter extends RecyclerView.Adapter<AppsListAdapter.ViewHolder> {
/**
* ViewHolder for each row.
*/
public static class ViewHolder extends RecyclerView.ViewHolder {
public View mContent;
public ViewHolder(View v) {
super(v);
mContent = v;
}
}
private static final int SECTION_BREAK_VIEW_TYPE = 0;
private static final int ICON_VIEW_TYPE = 1;
private LayoutInflater mLayoutInflater;
private AlphabeticalAppsList mApps;
private View.OnTouchListener mTouchListener;
private View.OnClickListener mIconClickListener;
private View.OnLongClickListener mIconLongClickListener;
public AppsListAdapter(Context context, AlphabeticalAppsList apps,
View.OnTouchListener touchListener, View.OnClickListener iconClickListener,
View.OnLongClickListener iconLongClickListener) {
mApps = apps;
mLayoutInflater = LayoutInflater.from(context);
mTouchListener = touchListener;
mIconClickListener = iconClickListener;
mIconLongClickListener = iconLongClickListener;
}
public RecyclerView.LayoutManager getLayoutManager(Context context) {
return new LinearLayoutManager(context);
}
@Override
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
switch (viewType) {
case SECTION_BREAK_VIEW_TYPE:
return new ViewHolder(new View(parent.getContext()));
case ICON_VIEW_TYPE:
// Inflate the row and all the icon children necessary
ViewGroup row = (ViewGroup) mLayoutInflater.inflate(R.layout.apps_list_row_view,
parent, false);
BubbleTextView icon = (BubbleTextView) mLayoutInflater.inflate(
R.layout.apps_list_row_icon_view, 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);
return new ViewHolder(row);
default:
throw new RuntimeException("Unexpected view type");
}
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
AppInfo info = mApps.getApps().get(position);
if (info != AlphabeticalAppsList.SECTION_BREAK_INFO) {
ViewGroup content = (ViewGroup) holder.mContent;
String sectionDescription = mApps.getSectionNameForApp(info);
// Bind the section header
boolean showSectionHeader = true;
if (position > 0) {
AppInfo prevInfo = mApps.getApps().get(position - 1);
showSectionHeader = (prevInfo == AlphabeticalAppsList.SECTION_BREAK_INFO);
}
TextView tv = (TextView) content.findViewById(R.id.section);
if (showSectionHeader) {
tv.setText(sectionDescription);
tv.setVisibility(View.VISIBLE);
} else {
tv.setVisibility(View.INVISIBLE);
}
// Bind the icon
BubbleTextView icon = (BubbleTextView) content.getChildAt(1);
icon.applyFromApplicationInfo(info);
}
}
@Override
public int getItemCount() {
return mApps.getApps().size();
}
@Override
public int getItemViewType(int position) {
if (mApps.getApps().get(position) == AlphabeticalAppsList.SECTION_BREAK_INFO) {
return SECTION_BREAK_VIEW_TYPE;
}
return ICON_VIEW_TYPE;
}
}
@@ -63,6 +63,7 @@ public class BubbleTextView extends TextView {
private float mSlop;
private final boolean mDeferShadowGenerationOnTouch;
private final boolean mCustomShadowsEnabled;
private final boolean mLayoutHorizontal;
private final int mIconSize;
@@ -96,6 +97,8 @@ public class BubbleTextView extends TextView {
grid.iconDrawablePaddingPx);
mTextSize = a.getDimensionPixelSize(R.styleable.BubbleTextView_textSizeOverride,
grid.allAppsIconTextSizePx);
mDeferShadowGenerationOnTouch =
a.getBoolean(R.styleable.BubbleTextView_deferShadowGeneration, false);
a.recycle();
if (mCustomShadowsEnabled) {
@@ -218,7 +221,7 @@ public class BubbleTextView extends TextView {
// So that the pressed outline is visible immediately on setStayPressed(),
// we pre-create it on ACTION_DOWN (it takes a small but perceptible amount of time
// to create it)
if (mPressedBackground == null) {
if (!mDeferShadowGenerationOnTouch && mPressedBackground == null) {
mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
}
@@ -247,6 +250,10 @@ public class BubbleTextView extends TextView {
mStayPressed = stayPressed;
if (!stayPressed) {
mPressedBackground = null;
} else {
if (mPressedBackground == null) {
mPressedBackground = mOutlineHelper.createMediumDropShadow(this);
}
}
// Only show the shadow effect when persistent pressed state is set.
+17 -11
View File
@@ -122,8 +122,7 @@ public class DeviceProfile {
int hotseatAllAppsRank;
int allAppsNumRows;
int allAppsNumCols;
// TODO(winsonc): to be used with the grid layout
int allAppsRowsSize;
int appsViewNumCols;
int searchBarSpaceWidthPx;
int searchBarSpaceHeightPx;
int pageIndicatorHeightPx;
@@ -365,7 +364,7 @@ public class DeviceProfile {
}
}
private void updateIconSize(float scale, int drawablePadding, Resources resources,
private void updateIconSize(float scale, int drawablePadding, Resources res,
DisplayMetrics dm) {
iconSizePx = (int) (DynamicGrid.pxFromDp(iconSize, dm) * scale);
iconTextSizePx = (int) (DynamicGrid.pxFromSp(iconTextSize, dm) * scale);
@@ -374,9 +373,9 @@ public class DeviceProfile {
// Search Bar
searchBarSpaceWidthPx = Math.min(widthPx,
resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_max_width));
searchBarSpaceHeightPx = getSearchBarTopOffset()
+ resources.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
+ res.getDimensionPixelSize(R.dimen.dynamic_grid_search_bar_height);
// Calculate the actual text height
Paint textPaint = new Paint();
@@ -384,7 +383,7 @@ public class DeviceProfile {
FontMetrics fm = textPaint.getFontMetrics();
cellWidthPx = iconSizePx;
cellHeightPx = iconSizePx + iconDrawablePaddingPx + (int) Math.ceil(fm.bottom - fm.top);
final float scaleDps = resources.getDimensionPixelSize(R.dimen.dragViewScale);
final float scaleDps = res.getDimensionPixelSize(R.dimen.dragViewScale);
dragViewScale = (iconSizePx + scaleDps) / iconSizePx;
// Hotseat
@@ -402,11 +401,11 @@ public class DeviceProfile {
allAppsCellWidthPx = allAppsIconSizePx;
allAppsCellHeightPx = allAppsIconSizePx + drawablePadding + iconTextSizePx;
int maxLongEdgeCellCount =
resources.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
res.getInteger(R.integer.config_dynamic_grid_max_long_edge_cell_count);
int maxShortEdgeCellCount =
resources.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
res.getInteger(R.integer.config_dynamic_grid_max_short_edge_cell_count);
int minEdgeCellCount =
resources.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
res.getInteger(R.integer.config_dynamic_grid_min_edge_cell_count);
int maxRows = (isLandscape ? maxShortEdgeCellCount : maxLongEdgeCellCount);
int maxCols = (isLandscape ? maxLongEdgeCellCount : maxShortEdgeCellCount);
@@ -417,10 +416,17 @@ public class DeviceProfile {
allAppsNumRows = (availableHeightPx - pageIndicatorHeightPx) /
(allAppsCellHeightPx + allAppsCellPaddingPx);
allAppsNumRows = Math.max(minEdgeCellCount, Math.min(maxRows, allAppsNumRows));
allAppsNumCols = (availableWidthPx) /
(allAppsCellWidthPx + allAppsCellPaddingPx);
allAppsNumCols = (availableWidthPx) / (allAppsCellWidthPx + allAppsCellPaddingPx);
allAppsNumCols = Math.max(minEdgeCellCount, Math.min(maxCols, allAppsNumCols));
}
int appsContainerViewPx = res.getDimensionPixelSize(R.dimen.apps_container_width);
int appsViewLeftMarginPx =
res.getDimensionPixelSize(R.dimen.apps_grid_view_start_margin);
int availableAppsWidthPx = (appsContainerViewPx > 0) ? appsContainerViewPx :
availableWidthPx;
appsViewNumCols = (availableAppsWidthPx - appsViewLeftMarginPx) /
(allAppsCellWidthPx + allAppsCellPaddingPx);
}
void updateFromConfiguration(Context context, Resources resources, int wPx, int hPx,
@@ -0,0 +1,131 @@
package com.android.launcher3.compat;
import android.content.Context;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.Locale;
/**
* Fallback class to support Alphabetic indexing if not supported by the framework.
* TODO(winsonc): disable for non-english locales
*/
class BaseAlphabeticIndex {
private static final String BUCKETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ-";
private static final int UNKNOWN_BUCKET_INDEX = BUCKETS.length() - 1;
public BaseAlphabeticIndex() {}
/**
* Sets the max number of the label buckets in this index.
*/
public void setMaxLabelCount(int count) {
// Not currently supported
}
/**
* Returns the index of the bucket in which the given string should appear.
*/
public int getBucketIndex(String s) {
if (s.isEmpty()) {
return UNKNOWN_BUCKET_INDEX;
}
int index = BUCKETS.indexOf(s.substring(0, 1).toUpperCase());
if (index != -1) {
return index;
}
return UNKNOWN_BUCKET_INDEX;
}
/**
* Returns the label for the bucket at the given index (as returned by getBucketIndex).
*/
public String getBucketLabel(int index) {
return BUCKETS.substring(index, index + 1);
}
}
/**
* Reflected libcore.icu.AlphabeticIndex implementation, falls back to the base alphabetic index.
*/
public class AlphabeticIndexCompat extends BaseAlphabeticIndex {
private Object mAlphabeticIndex;
private Method mAddLabelsMethod;
private Method mSetMaxLabelCountMethod;
private Method mGetBucketIndexMethod;
private Method mGetBucketLabelMethod;
private boolean mHasValidAlphabeticIndex;
public AlphabeticIndexCompat(Context context) {
super();
try {
Locale curLocale = context.getResources().getConfiguration().locale;
Class clazz = Class.forName("libcore.icu.AlphabeticIndex");
Constructor ctor = clazz.getConstructor(Locale.class);
mAddLabelsMethod = clazz.getDeclaredMethod("addLabels", Locale.class);
mSetMaxLabelCountMethod = clazz.getDeclaredMethod("setMaxLabelCount", int.class);
mGetBucketIndexMethod = clazz.getDeclaredMethod("getBucketIndex", String.class);
mGetBucketLabelMethod = clazz.getDeclaredMethod("getBucketLabel", int.class);
mAlphabeticIndex = ctor.newInstance(curLocale);
try {
// Ensure we always have some base English locale buckets
if (!curLocale.getLanguage().equals(new Locale("en").getLanguage())) {
mAddLabelsMethod.invoke(mAlphabeticIndex, Locale.ENGLISH);
}
} catch (Exception e) {
e.printStackTrace();
}
mHasValidAlphabeticIndex = true;
} catch (Exception e) {
mHasValidAlphabeticIndex = false;
}
}
/**
* Sets the max number of the label buckets in this index.
* (ICU 51 default is 99)
*/
public void setMaxLabelCount(int count) {
if (mHasValidAlphabeticIndex) {
try {
mSetMaxLabelCountMethod.invoke(mAlphabeticIndex, count);
} catch (Exception e) {
e.printStackTrace();
}
} else {
super.setMaxLabelCount(count);
}
}
/**
* Returns the index of the bucket in which {@param s} should appear.
* Function is synchronized because underlying routine walks an iterator
* whose state is maintained inside the index object.
*/
public int getBucketIndex(String s) {
if (mHasValidAlphabeticIndex) {
try {
return (Integer) mGetBucketIndexMethod.invoke(mAlphabeticIndex, s);
} catch (Exception e) {
e.printStackTrace();
}
}
return super.getBucketIndex(s);
}
/**
* Returns the label for the bucket at the given index (as returned by getBucketIndex).
*/
public String getBucketLabel(int index) {
if (mHasValidAlphabeticIndex) {
try {
return (String) mGetBucketLabelMethod.invoke(mAlphabeticIndex, index);
} catch (Exception e) {
e.printStackTrace();
}
}
return super.getBucketLabel(index);
}
}