diff options
author | Yvonne Wong <ywong@cyngn.com> | 2015-11-20 10:49:59 -0800 |
---|---|---|
committer | Yvonne Wong <ywong@cyngn.com> | 2015-11-30 11:50:09 -0800 |
commit | b3de38c976eeb5a8c4b5c1b0d68d9c84253d3187 (patch) | |
tree | fd5ebd4fecaa73e607d422575a34c474e594c20b /src/com/android/launcher3/list | |
parent | 9a147bf42d2e3b3fb565e4264efe00eb293454b8 (diff) | |
download | android_packages_apps_Trebuchet-b3de38c976eeb5a8c4b5c1b0d68d9c84253d3187.tar.gz android_packages_apps_Trebuchet-b3de38c976eeb5a8c4b5c1b0d68d9c84253d3187.tar.bz2 android_packages_apps_Trebuchet-b3de38c976eeb5a8c4b5c1b0d68d9c84253d3187.zip |
Reimplement CM Settings Overview Panel in the new Launcher Part 1
- Adds vertical sliding panel and animations associated with opening and closing the panel
- Adds the views for settings and animation for the drawer arrow
- Enables hiding workspace icon labels, hiding drawer icon labels, scrolling wallpaper, and larger icons
- Changes how ragged grid custom icon sizes gets defined
Change-Id: I1a82215a09486b4770494e665e598efdbabd1d3e
Diffstat (limited to 'src/com/android/launcher3/list')
5 files changed, 1658 insertions, 0 deletions
diff --git a/src/com/android/launcher3/list/AutoScrollListView.java b/src/com/android/launcher3/list/AutoScrollListView.java new file mode 100644 index 000000000..733a753c1 --- /dev/null +++ b/src/com/android/launcher3/list/AutoScrollListView.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.util.AttributeSet; +import android.widget.ListView; + +/** + * A ListView that can be asked to scroll (smoothly or otherwise) to a specific + * position. This class takes advantage of similar functionality that exists + * in {@link ListView} and enhances it. + */ +public class AutoScrollListView extends ListView { + + /** + * Position the element at about 1/3 of the list height + */ + private static final float PREFERRED_SELECTION_OFFSET_FROM_TOP = 0.33f; + + private int mRequestedScrollPosition = -1; + private boolean mSmoothScrollRequested; + + public AutoScrollListView(Context context) { + super(context); + } + + public AutoScrollListView(Context context, AttributeSet attrs) { + super(context, attrs); + } + + public AutoScrollListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + } + + /** + * Brings the specified position to view by optionally performing a jump-scroll maneuver: + * first it jumps to some position near the one requested and then does a smooth + * scroll to the requested position. This creates an impression of full smooth + * scrolling without actually traversing the entire list. If smooth scrolling is + * not requested, instantly positions the requested item at a preferred offset. + */ + public void requestPositionToScreen(int position, boolean smoothScroll) { + mRequestedScrollPosition = position; + mSmoothScrollRequested = smoothScroll; + requestLayout(); + } + + @Override + protected void layoutChildren() { + super.layoutChildren(); + if (mRequestedScrollPosition == -1) { + return; + } + + final int position = mRequestedScrollPosition; + mRequestedScrollPosition = -1; + + int firstPosition = getFirstVisiblePosition() + 1; + int lastPosition = getLastVisiblePosition(); + if (position >= firstPosition && position <= lastPosition) { + return; // Already on screen + } + + final int offset = (int) (getHeight() * PREFERRED_SELECTION_OFFSET_FROM_TOP); + if (!mSmoothScrollRequested) { + setSelectionFromTop(position, offset); + + // Since we have changed the scrolling position, we need to redo child layout + // Calling "requestLayout" in the middle of a layout pass has no effect, + // so we call layoutChildren explicitly + super.layoutChildren(); + + } else { + // We will first position the list a couple of screens before or after + // the new selection and then scroll smoothly to it. + int twoScreens = (lastPosition - firstPosition) * 2; + int preliminaryPosition; + if (position < firstPosition) { + preliminaryPosition = position + twoScreens; + if (preliminaryPosition >= getCount()) { + preliminaryPosition = getCount() - 1; + } + if (preliminaryPosition < firstPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } else { + preliminaryPosition = position - twoScreens; + if (preliminaryPosition < 0) { + preliminaryPosition = 0; + } + if (preliminaryPosition > lastPosition) { + setSelection(preliminaryPosition); + super.layoutChildren(); + } + } + + + smoothScrollToPositionFromTop(position, offset); + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/CompositeCursorAdapter.java b/src/com/android/launcher3/list/CompositeCursorAdapter.java new file mode 100644 index 000000000..b163c501c --- /dev/null +++ b/src/com/android/launcher3/list/CompositeCursorAdapter.java @@ -0,0 +1,532 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.database.Cursor; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; + +import java.util.ArrayList; + +/** + * A general purpose adapter that is composed of multiple cursors. It just + * appends them in the order they are added. + */ +public abstract class CompositeCursorAdapter extends BaseAdapter { + + private static final int INITIAL_CAPACITY = 2; + + public static class Partition { + boolean showIfEmpty; + boolean hasHeader; + + Cursor cursor; + int idColumnIndex; + int count; + + public Partition(boolean showIfEmpty, boolean hasHeader) { + this.showIfEmpty = showIfEmpty; + this.hasHeader = hasHeader; + } + + /** + * True if the directory should be shown even if no contacts are found. + */ + public boolean getShowIfEmpty() { + return showIfEmpty; + } + + public boolean getHasHeader() { + return hasHeader; + } + } + + private final Context mContext; + private ArrayList<Partition> mPartitions; + private int mCount = 0; + private boolean mCacheValid = true; + private boolean mNotificationsEnabled = true; + private boolean mNotificationNeeded; + + public CompositeCursorAdapter(Context context) { + this(context, INITIAL_CAPACITY); + } + + public CompositeCursorAdapter(Context context, int initialCapacity) { + mContext = context; + mPartitions = new ArrayList<Partition>(); + } + + public Context getContext() { + return mContext; + } + + /** + * Registers a partition. The cursor for that partition can be set later. + * Partitions should be added in the order they are supposed to appear in the + * list. + */ + public void addPartition(boolean showIfEmpty, boolean hasHeader) { + addPartition(new Partition(showIfEmpty, hasHeader)); + } + + public void addPartition(Partition partition) { + mPartitions.add(partition); + invalidate(); + notifyDataSetChanged(); + } + + public void addPartition(int location, Partition partition) { + mPartitions.add(location, partition); + invalidate(); + notifyDataSetChanged(); + } + + public void removePartition(int partitionIndex) { + Cursor cursor = mPartitions.get(partitionIndex).cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + mPartitions.remove(partitionIndex); + invalidate(); + notifyDataSetChanged(); + } + + /** + * Removes cursors for all partitions. + */ + // TODO: Is this really what this is supposed to do? Just remove the cursors? Not close them? + // Not remove the partitions themselves? Isn't this leaking? + + public void clearPartitions() { + for (Partition partition : mPartitions) { + partition.cursor = null; + } + invalidate(); + notifyDataSetChanged(); + } + + /** + * Closes all cursors and removes all partitions. + */ + public void close() { + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + if (cursor != null && !cursor.isClosed()) { + cursor.close(); + } + } + mPartitions.clear(); + invalidate(); + notifyDataSetChanged(); + } + + public void setHasHeader(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).hasHeader = flag; + invalidate(); + } + + public void setShowIfEmpty(int partitionIndex, boolean flag) { + mPartitions.get(partitionIndex).showIfEmpty = flag; + invalidate(); + } + + public Partition getPartition(int partitionIndex) { + return mPartitions.get(partitionIndex); + } + + protected void invalidate() { + mCacheValid = false; + } + + public int getPartitionCount() { + return mPartitions.size(); + } + + protected void ensureCacheValid() { + if (mCacheValid) { + return; + } + + mCount = 0; + for (Partition partition : mPartitions) { + Cursor cursor = partition.cursor; + int count = cursor != null ? cursor.getCount() : 0; + if (partition.hasHeader) { + if (count != 0 || partition.showIfEmpty) { + count++; + } + } + partition.count = count; + mCount += count; + } + + mCacheValid = true; + } + + /** + * Returns true if the specified partition was configured to have a header. + */ + public boolean hasHeader(int partition) { + return mPartitions.get(partition).hasHeader; + } + + /** + * Returns the total number of list items in all partitions. + */ + public int getCount() { + ensureCacheValid(); + return mCount; + } + + /** + * Returns the cursor for the given partition + */ + public Cursor getCursor(int partition) { + return mPartitions.get(partition).cursor; + } + + /** + * Changes the cursor for an individual partition. + */ + public void changeCursor(int partition, Cursor cursor) { + Cursor prevCursor = mPartitions.get(partition).cursor; + if (prevCursor != cursor) { + if (prevCursor != null && !prevCursor.isClosed()) { + prevCursor.close(); + } + mPartitions.get(partition).cursor = cursor; + if (cursor != null) { + mPartitions.get(partition).idColumnIndex = cursor.getColumnIndex("_id"); + } + invalidate(); + notifyDataSetChanged(); + } + } + + /** + * Returns true if the specified partition has no cursor or an empty cursor. + */ + public boolean isPartitionEmpty(int partition) { + Cursor cursor = mPartitions.get(partition).cursor; + return cursor == null || cursor.getCount() == 0; + } + + /** + * Given a list position, returns the index of the corresponding partition. + */ + public int getPartitionForPosition(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + return i; + } + start = end; + } + return -1; + } + + /** + * Given a list position, return the offset of the corresponding item in its + * partition. The header, if any, will have offset -1. + */ + public int getOffsetInPartition(int position) { + ensureCacheValid(); + int start = 0; + for (Partition partition : mPartitions) { + int end = start + partition.count; + if (position >= start && position < end) { + int offset = position - start; + if (partition.hasHeader) { + offset--; + } + return offset; + } + start = end; + } + return -1; + } + + /** + * Returns the first list position for the specified partition. + */ + public int getPositionForPartition(int partition) { + ensureCacheValid(); + int position = 0; + for (int i = 0; i < partition; i++) { + position += mPartitions.get(i).count; + } + return position; + } + + @Override + public int getViewTypeCount() { + return getItemViewTypeCount() + 1; + } + + /** + * Returns the overall number of item view types across all partitions. An + * implementation of this method needs to ensure that the returned count is + * consistent with the values returned by {@link #getItemViewType(int,int)}. + */ + public int getItemViewTypeCount() { + return 1; + } + + /** + * Returns the view type for the list item at the specified position in the + * specified partition. + */ + protected int getItemViewType(int partition, int position) { + return 1; + } + + @Override + public int getItemViewType(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + if (offset == -1) { + return IGNORE_ITEM_VIEW_TYPE; + } else { + return getItemViewType(i, offset); + } + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + public View getView(int position, View convertView, ViewGroup parent) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader) { + offset--; + } + View view; + if (offset == -1) { + view = getHeaderView(i, mPartitions.get(i).cursor, convertView, parent); + } else { + if (!mPartitions.get(i).cursor.moveToPosition(offset)) { + throw new IllegalStateException("Couldn't move cursor to position " + + offset); + } + view = getView(i, mPartitions.get(i).cursor, offset, convertView, parent); + } + if (view == null) { + throw new NullPointerException("View should not be null, partition: " + i + + " position: " + offset); + } + return view; + } + start = end; + } + + throw new ArrayIndexOutOfBoundsException(position); + } + + /** + * Returns the header view for the specified partition, creating one if needed. + */ + protected View getHeaderView(int partition, Cursor cursor, View convertView, + ViewGroup parent) { + View view = convertView != null + ? convertView + : newHeaderView(mContext, partition, cursor, parent); + bindHeaderView(view, partition, cursor); + return view; + } + + /** + * Creates the header view for the specified partition. + */ + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + return null; + } + + /** + * Binds the header view for the specified partition. + */ + protected void bindHeaderView(View view, int partition, Cursor cursor) { + } + + /** + * Returns an item view for the specified partition, creating one if needed. + */ + protected View getView(int partition, Cursor cursor, int position, View convertView, + ViewGroup parent) { + View view; + if (convertView != null) { + view = convertView; + } else { + view = newView(mContext, partition, cursor, position, parent); + } + bindView(view, partition, cursor, position); + return view; + } + + /** + * Creates an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent); + + /** + * Binds an item view for the specified partition and position. Position + * corresponds directly to the current cursor position. + */ + protected abstract void bindView(View v, int partition, Cursor cursor, int position); + + /** + * Returns a pre-positioned cursor for the specified list position. + */ + public Object getItem(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return null; + } + Cursor cursor = mPartition.cursor; + cursor.moveToPosition(offset); + return cursor; + } + start = end; + } + + return null; + } + + /** + * Returns the item ID for the specified list position. + */ + public long getItemId(int position) { + ensureCacheValid(); + int start = 0; + for (Partition mPartition : mPartitions) { + int end = start + mPartition.count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartition.hasHeader) { + offset--; + } + if (offset == -1) { + return 0; + } + if (mPartition.idColumnIndex == -1) { + return 0; + } + + Cursor cursor = mPartition.cursor; + if (cursor == null || cursor.isClosed() || !cursor.moveToPosition(offset)) { + return 0; + } + return cursor.getLong(mPartition.idColumnIndex); + } + start = end; + } + + return 0; + } + + /** + * Returns false if any partition has a header. + */ + @Override + public boolean areAllItemsEnabled() { + for (Partition mPartition : mPartitions) { + if (mPartition.hasHeader) { + return false; + } + } + return true; + } + + /** + * Returns true for all items except headers. + */ + @Override + public boolean isEnabled(int position) { + ensureCacheValid(); + int start = 0; + for (int i = 0, n = mPartitions.size(); i < n; i++) { + int end = start + mPartitions.get(i).count; + if (position >= start && position < end) { + int offset = position - start; + if (mPartitions.get(i).hasHeader && offset == 0) { + return false; + } else { + return isEnabled(i, offset); + } + } + start = end; + } + + return false; + } + + /** + * Returns true if the item at the specified offset of the specified + * partition is selectable and clickable. + */ + protected boolean isEnabled(int partition, int position) { + return true; + } + + /** + * Enable or disable data change notifications. It may be a good idea to + * disable notifications before making changes to several partitions at once. + */ + public void setNotificationsEnabled(boolean flag) { + mNotificationsEnabled = flag; + if (flag && mNotificationNeeded) { + notifyDataSetChanged(); + } + } + + @Override + public void notifyDataSetChanged() { + if (mNotificationsEnabled) { + mNotificationNeeded = false; + super.notifyDataSetChanged(); + } else { + mNotificationNeeded = true; + } + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListAdapter.java b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java new file mode 100644 index 000000000..cc053e18f --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListAdapter.java @@ -0,0 +1,137 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; + +/** + * A subclass of {@link CompositeCursorAdapter} that manages pinned partition headers. + */ +public abstract class PinnedHeaderListAdapter extends CompositeCursorAdapter + implements PinnedHeaderListView.PinnedHeaderAdapter { + + public static final int PARTITION_HEADER_TYPE = 0; + + private boolean mPinnedPartitionHeadersEnabled; + private boolean mHeaderVisibility[]; + + public PinnedHeaderListAdapter(Context context) { + super(context); + } + + public PinnedHeaderListAdapter(Context context, int initialCapacity) { + super(context, initialCapacity); + } + + public boolean getPinnedPartitionHeadersEnabled() { + return mPinnedPartitionHeadersEnabled; + } + + public void setPinnedPartitionHeadersEnabled(boolean flag) { + this.mPinnedPartitionHeadersEnabled = flag; + } + + @Override + public int getPinnedHeaderCount() { + if (mPinnedPartitionHeadersEnabled) { + return getPartitionCount(); + } else { + return 0; + } + } + + protected boolean isPinnedPartitionHeaderVisible(int partition) { + return getPinnedPartitionHeadersEnabled() && hasHeader(partition) + && !isPartitionEmpty(partition); + } + + /** + * The default implementation creates the same type of view as a normal + * partition header. + */ + @Override + public View getPinnedHeaderView(int partition, View convertView, ViewGroup parent) { + if (hasHeader(partition)) { + View view = null; + if (convertView != null) { + Integer headerType = (Integer)convertView.getTag(); + if (headerType != null && headerType == PARTITION_HEADER_TYPE) { + view = convertView; + } + } + if (view == null) { + view = newHeaderView(getContext(), partition, null, parent); + view.setTag(PARTITION_HEADER_TYPE); + view.setFocusable(false); + view.setEnabled(false); + } + bindHeaderView(view, partition, getCursor(partition)); + view.setLayoutDirection(parent.getLayoutDirection()); + return view; + } else { + return null; + } + } + + @Override + public void configurePinnedHeaders(PinnedHeaderListView listView) { + if (!getPinnedPartitionHeadersEnabled()) { + return; + } + + int size = getPartitionCount(); + boolean unCached = false; + // Cache visibility bits, because we will need them several times later on + if (mHeaderVisibility == null || mHeaderVisibility.length != size) { + mHeaderVisibility = new boolean[size]; + unCached = true; + } + for (int i = 0; i < size; i++) { + boolean visible = isPinnedPartitionHeaderVisible(i); + mHeaderVisibility[i] = visible; + if (!visible) { + listView.setHeaderInvisible(i, true); + } + } + + int headerViewsCount = listView.getHeaderViewsCount(); + + // Starting at the top, find and pin headers for partitions preceding the visible one(s) + int topHeaderHeight = 0; + for (int i = 0; i < size; i++) { + if (mHeaderVisibility[i]) { + int position = listView.getPositionAt(topHeaderHeight) - headerViewsCount; + int partition = getPartitionForPosition(position); + if (i > partition) { + break; + } + + if (!unCached){ + listView.setHeaderPinnedAtTop(i, topHeaderHeight, false); + topHeaderHeight += listView.getPinnedHeaderHeight(i); + } + + } + } + } + + @Override + public int getScrollPositionForHeader(int viewIndex) { + return getPositionForPartition(viewIndex); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/PinnedHeaderListView.java b/src/com/android/launcher3/list/PinnedHeaderListView.java new file mode 100644 index 000000000..58e8791ab --- /dev/null +++ b/src/com/android/launcher3/list/PinnedHeaderListView.java @@ -0,0 +1,565 @@ +/* + * Copyright (C) 2010 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.list; + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.graphics.RectF; +import android.util.AttributeSet; +import android.util.Log; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AbsListView; +import android.widget.AbsListView.OnScrollListener; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemSelectedListener; +import android.widget.ListAdapter; + +/** + * A ListView that maintains a header pinned at the top of the list. The + * pinned header can be pushed up and dissolved as needed. + */ +public class PinnedHeaderListView extends AutoScrollListView + implements OnScrollListener, OnItemSelectedListener { + + /** + * Adapter interface. The list adapter must implement this interface. + */ + public interface PinnedHeaderAdapter { + + /** + * Returns the overall number of pinned headers, visible or not. + */ + int getPinnedHeaderCount(); + + /** + * Creates or updates the pinned header view. + */ + View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent); + + /** + * Configures the pinned headers to match the visible list items. The + * adapter should call {@link PinnedHeaderListView#setHeaderPinnedAtTop}, + * {@link PinnedHeaderListView#setHeaderPinnedAtBottom}, + * {@link PinnedHeaderListView#setFadingHeader} or + * {@link PinnedHeaderListView#setHeaderInvisible}, for each header that + * needs to change its position or visibility. + */ + void configurePinnedHeaders(PinnedHeaderListView listView); + + /** + * Returns the list position to scroll to if the pinned header is touched. + * Return -1 if the list does not need to be scrolled. + */ + int getScrollPositionForHeader(int viewIndex); + } + + private static final int MAX_ALPHA = 255; + private static final int TOP = 0; + private static final int BOTTOM = 1; + private static final int FADING = 2; + + private static final int DEFAULT_ANIMATION_DURATION = 20; + + private static final int DEFAULT_SMOOTH_SCROLL_DURATION = 100; + + private static final class PinnedHeader { + View view; + boolean visible; + int y; + int height; + int alpha; + int state; + + boolean animating; + boolean targetVisible; + int sourceY; + int targetY; + long targetTime; + } + + private PinnedHeaderAdapter mAdapter; + private int mSize; + private PinnedHeader[] mHeaders; + private RectF mBounds = new RectF(); + private Rect mClipRect = new Rect(); + private OnScrollListener mOnScrollListener; + private OnItemSelectedListener mOnItemSelectedListener; + private int mScrollState; + + private boolean mScrollToSectionOnHeaderTouch = false; + private boolean mHeaderTouched = false; + + private int mAnimationDuration = DEFAULT_ANIMATION_DURATION; + private boolean mAnimating; + private long mAnimationTargetTime; + private int mHeaderPaddingStart; + private int mHeaderWidth; + + public PinnedHeaderListView(Context context) { + this(context, null, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs) { + this(context, attrs, android.R.attr.listViewStyle); + } + + public PinnedHeaderListView(Context context, AttributeSet attrs, int defStyle) { + super(context, attrs, defStyle); + super.setOnScrollListener(this); + super.setOnItemSelectedListener(this); + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + mHeaderPaddingStart = getPaddingStart(); + mHeaderWidth = r - l - mHeaderPaddingStart - getPaddingEnd(); + } + + public void setPinnedHeaderAnimationDuration(int duration) { + mAnimationDuration = duration; + } + + @Override + public void setAdapter(ListAdapter adapter) { + mAdapter = (PinnedHeaderAdapter)adapter; + super.setAdapter(adapter); + } + + @Override + public void setOnScrollListener(OnScrollListener onScrollListener) { + mOnScrollListener = onScrollListener; + super.setOnScrollListener(this); + } + + @Override + public void setOnItemSelectedListener(OnItemSelectedListener listener) { + mOnItemSelectedListener = listener; + super.setOnItemSelectedListener(this); + } + + public void setScrollToSectionOnHeaderTouch(boolean value) { + mScrollToSectionOnHeaderTouch = value; + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, + int totalItemCount) { + if (mAdapter != null) { + int count = mAdapter.getPinnedHeaderCount(); + if (count != mSize) { + mSize = count; + if (mHeaders == null) { + mHeaders = new PinnedHeader[mSize]; + } else if (mHeaders.length < mSize) { + PinnedHeader[] headers = mHeaders; + mHeaders = new PinnedHeader[mSize]; + System.arraycopy(headers, 0, mHeaders, 0, headers.length); + } + } + + for (int i = 0; i < mSize; i++) { + if (mHeaders[i] == null) { + mHeaders[i] = new PinnedHeader(); + } + mHeaders[i].view = mAdapter.getPinnedHeaderView(i, mHeaders[i].view, this); + } + + mAnimationTargetTime = System.currentTimeMillis() + mAnimationDuration; + mAdapter.configurePinnedHeaders(this); + invalidateIfAnimating(); + + } + if (mOnScrollListener != null) { + mOnScrollListener.onScroll(this, firstVisibleItem, visibleItemCount, totalItemCount); + } + } + + @Override + protected float getTopFadingEdgeStrength() { + // Disable vertical fading at the top when the pinned header is present + return mSize > 0 ? 0 : super.getTopFadingEdgeStrength(); + } + + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + mScrollState = scrollState; + if (mOnScrollListener != null) { + mOnScrollListener.onScrollStateChanged(this, scrollState); + } + } + + /** + * Ensures that the selected item is positioned below the top-pinned headers + * and above the bottom-pinned ones. + */ + @Override + public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { + int height = getHeight(); + + int windowTop = 0; + int windowBottom = height; + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + if (header.state == TOP) { + windowTop = header.y + header.height; + } else if (header.state == BOTTOM) { + windowBottom = header.y; + break; + } + } + } + + View selectedView = getSelectedView(); + if (selectedView != null) { + if (selectedView.getTop() < windowTop) { + setSelectionFromTop(position, windowTop); + } else if (selectedView.getBottom() > windowBottom) { + setSelectionFromTop(position, windowBottom - selectedView.getHeight()); + } + } + + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onItemSelected(parent, view, position, id); + } + } + + @Override + public void onNothingSelected(AdapterView<?> parent) { + if (mOnItemSelectedListener != null) { + mOnItemSelectedListener.onNothingSelected(parent); + } + } + + public int getPinnedHeaderHeight(int viewIndex) { + ensurePinnedHeaderLayout(viewIndex); + return mHeaders[viewIndex].view.getHeight(); + } + + /** + * Set header to be pinned at the top. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtTop(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.y = y; + header.state = TOP; + + // TODO perhaps we should animate at the top as well + header.animating = false; + } + + /** + * Set header to be pinned at the bottom. + * + * @param viewIndex index of the header view + * @param y is position of the header in pixels. + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderPinnedAtBottom(int viewIndex, int y, boolean animate) { + ensurePinnedHeaderLayout(viewIndex); + PinnedHeader header = mHeaders[viewIndex]; + header.state = BOTTOM; + if (header.animating) { + header.targetTime = mAnimationTargetTime; + header.sourceY = header.y; + header.targetY = y; + } else if (animate && (header.y != y || !header.visible)) { + if (header.visible) { + header.sourceY = header.y; + } else { + header.visible = true; + header.sourceY = y + header.height; + } + header.animating = true; + header.targetVisible = true; + header.targetTime = mAnimationTargetTime; + header.targetY = y; + } else { + header.visible = true; + header.y = y; + } + } + + /** + * Set header to be pinned at the top of the first visible item. + * + * @param viewIndex index of the header view + * @param position is position of the header in pixels. + */ + public void setFadingHeader(int viewIndex, int position, boolean fade) { + ensurePinnedHeaderLayout(viewIndex); + + View child = getChildAt(position - getFirstVisiblePosition()); + if (child == null) return; + + PinnedHeader header = mHeaders[viewIndex]; + header.visible = true; + header.state = FADING; + header.alpha = MAX_ALPHA; + header.animating = false; + + int top = getTotalTopPinnedHeaderHeight(); + header.y = top; + if (fade) { + int bottom = child.getBottom() - top; + int headerHeight = header.height; + if (bottom < headerHeight) { + int portion = bottom - headerHeight; + header.alpha = MAX_ALPHA * (headerHeight + portion) / headerHeight; + header.y = top + portion; + } + } + } + + /** + * Makes header invisible. + * + * @param viewIndex index of the header view + * @param animate true if the transition to the new coordinate should be animated + */ + public void setHeaderInvisible(int viewIndex, boolean animate) { + PinnedHeader header = mHeaders[viewIndex]; + if (header.visible && (animate || header.animating) && header.state == BOTTOM) { + header.sourceY = header.y; + if (!header.animating) { + header.visible = true; + header.targetY = getBottom() + header.height; + } + header.animating = true; + header.targetTime = mAnimationTargetTime; + header.targetVisible = false; + } else { + header.visible = false; + } + } + + private void ensurePinnedHeaderLayout(int viewIndex) { + View view = mHeaders[viewIndex].view; + if (view.isLayoutRequested()) { + int widthSpec = View.MeasureSpec.makeMeasureSpec(mHeaderWidth, View.MeasureSpec.EXACTLY); + int heightSpec; + ViewGroup.LayoutParams layoutParams = view.getLayoutParams(); + if (layoutParams != null && layoutParams.height > 0) { + heightSpec = View.MeasureSpec + .makeMeasureSpec(layoutParams.height, View.MeasureSpec.EXACTLY); + } else { + heightSpec = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); + } + view.measure(widthSpec, heightSpec); + int height = view.getMeasuredHeight(); + mHeaders[viewIndex].height = height; + view.layout(0, 0, mHeaderWidth, height); + } + } + + /** + * Returns the sum of heights of headers pinned to the top. + */ + public int getTotalTopPinnedHeaderHeight() { + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == TOP) { + return header.y + header.height; + } + } + return 0; + } + + /** + * Returns the list item position at the specified y coordinate. + */ + public int getPositionAt(int y) { + do { + int position = pointToPosition(getPaddingLeft() + 1, y); + if (position != -1) { + return position; + } + // If position == -1, we must have hit a separator. Let's examine + // a nearby pixel + y--; + } while (y > 0); + return 0; + } + + @Override + public boolean onInterceptTouchEvent(MotionEvent ev) { + mHeaderTouched = false; + if (super.onInterceptTouchEvent(ev)) { + return true; + } + + if (mScrollState == SCROLL_STATE_IDLE) { + final int y = (int)ev.getY(); + final int x = (int)ev.getX(); + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + // For RTL layouts, this also takes into account that the scrollbar is on the left + // side. + final int padding = getPaddingLeft(); + if (header.visible && header.y <= y && header.y + header.height > y && + x >= padding && padding + mHeaderWidth >= x) { + mHeaderTouched = true; + if (mScrollToSectionOnHeaderTouch && + ev.getAction() == MotionEvent.ACTION_DOWN) { + return smoothScrollToPartition(i); + } else { + return true; + } + } + } + } + + return false; + } + + @Override + public boolean onTouchEvent(MotionEvent ev) { + if (mHeaderTouched) { + if (ev.getAction() == MotionEvent.ACTION_UP) { + mHeaderTouched = false; + } + return true; + } + return super.onTouchEvent(ev); + }; + + private boolean smoothScrollToPartition(int partition) { + final int position = mAdapter.getScrollPositionForHeader(partition); + if (position == -1) { + return false; + } + + int offset = 0; + for (int i = 0; i < partition; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + offset += header.height; + } + } + smoothScrollToPositionFromTop(position + getHeaderViewsCount(), offset, + DEFAULT_SMOOTH_SCROLL_DURATION); + return true; + } + + private void invalidateIfAnimating() { + mAnimating = false; + for (int i = 0; i < mSize; i++) { + if (mHeaders[i].animating) { + mAnimating = true; + invalidate(); + return; + } + } + } + + @Override + protected void dispatchDraw(Canvas canvas) { + long currentTime = mAnimating ? System.currentTimeMillis() : 0; + + int top = 0; + int bottom = getBottom(); + boolean hasVisibleHeaders = false; + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible) { + hasVisibleHeaders = true; + if (header.state == BOTTOM && header.y < bottom) { + bottom = header.y; + } else if (header.state == TOP || header.state == FADING) { + int newTop = header.y + header.height; + if (newTop > top) { + top = newTop; + } + } + } + } + + if (hasVisibleHeaders) { + canvas.save(); + mClipRect.set(0, top, getWidth(), bottom); + canvas.clipRect(mClipRect); + } + + super.dispatchDraw(canvas); + + if (hasVisibleHeaders) { + canvas.restore(); + + // First draw top headers, then the bottom ones to handle the Z axis correctly + for (int i = mSize; --i >= 0;) { + PinnedHeader header = mHeaders[i]; + if (header.visible && (header.state == TOP || header.state == FADING)) { + drawHeader(canvas, header, currentTime); + } + } + + for (int i = 0; i < mSize; i++) { + PinnedHeader header = mHeaders[i]; + if (header.visible && header.state == BOTTOM) { + drawHeader(canvas, header, currentTime); + } + } + } + + invalidateIfAnimating(); + } + + private void drawHeader(Canvas canvas, PinnedHeader header, long currentTime) { + if (header.animating) { + int timeLeft = (int)(header.targetTime - currentTime); + if (timeLeft <= 0) { + header.y = header.targetY; + header.visible = header.targetVisible; + header.animating = false; + } else { + header.y = header.targetY + (header.sourceY - header.targetY) * timeLeft + / mAnimationDuration; + } + } + if (header.visible) { + View view = header.view; + int saveCount = canvas.save(); + canvas.translate(isLayoutRtl() ? + getWidth() - mHeaderPaddingStart - mHeaderWidth : mHeaderPaddingStart, + header.y); + if (header.state == FADING) { + mBounds.set(0, 0, mHeaderWidth, view.getHeight()); + canvas.saveLayerAlpha(mBounds, header.alpha, Canvas.ALL_SAVE_FLAG); + } + view.draw(canvas); + canvas.restoreToCount(saveCount); + } + } + + /** + * Note: this is a reimplementation of View.isLayoutRtl() since that is currently hidden api. + */ + public boolean isLayoutRtl() { + return (getLayoutDirection() == LAYOUT_DIRECTION_RTL); + } +}
\ No newline at end of file diff --git a/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java new file mode 100644 index 000000000..aa2f45171 --- /dev/null +++ b/src/com/android/launcher3/list/SettingsPinnedHeaderAdapter.java @@ -0,0 +1,307 @@ +package com.android.launcher3.list; + +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.content.res.Resources; +import android.database.Cursor; +import android.graphics.Typeface; +import android.util.Log; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.widget.TextView; +import com.android.launcher3.Launcher; +import com.android.launcher3.OverviewSettingsPanel; +import com.android.launcher3.R; +import com.android.launcher3.settings.SettingsProvider; + +public class SettingsPinnedHeaderAdapter extends PinnedHeaderListAdapter { + private Launcher mLauncher; + private Context mContext; + + class SettingsPosition { + int partition = 0; + int position = 0; + + SettingsPosition (int partition, int position) { + this.partition = partition; + this.position = position; + } + } + + public SettingsPinnedHeaderAdapter(Context context) { + super(context); + mLauncher = (Launcher) context; + mContext = context; + } + + private String[] mHeaders; + public int mPinnedHeaderCount; + + public void setHeaders(String[] headers) { + this.mHeaders = headers; + } + + @Override + protected View newHeaderView(Context context, int partition, Cursor cursor, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_header, null); + } + + @Override + protected void bindHeaderView(View view, int partition, Cursor cursor) { + TextView textView = (TextView) view.findViewById(R.id.item_name); + textView.setText(mHeaders[partition]); + textView.setTypeface(textView.getTypeface(), Typeface.BOLD); + + textView.setTextSize(TypedValue.COMPLEX_UNIT_SP, 16); + } + + @Override + protected View newView(Context context, int partition, Cursor cursor, int position, + ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(context); + return inflater.inflate(R.layout.settings_pane_list_item, null); + } + + @Override + protected void bindView(View v, int partition, Cursor cursor, int position) { + TextView text = (TextView)v.findViewById(R.id.item_name); + // RTL + Configuration config = mLauncher.getResources().getConfiguration(); + if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) { + text.setGravity(Gravity.RIGHT); + } + + String title = cursor.getString(1); + text.setText(title); + + v.setTag(new SettingsPosition(partition, position)); + + Resources res = mLauncher.getResources(); + + + boolean current = false; + String state = ""; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + /*case 0: + current = mLauncher.isSearchBarEnabled(); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break;*/ + case 1: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + state = current ? res.getString(R.string.icon_labels_hide) + : res.getString(R.string.icon_labels_show); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + case 2: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + /*case 3: + updateDynamicGridSizeSettingsItem(v); + break;*/ + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + state = current ? res.getString(R.string.icon_labels_hide) + : res.getString(R.string.icon_labels_show); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + break; + default: + switch (position) { + case 0: + current = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); + state = current ? res.getString(R.string.setting_state_on) + : res.getString(R.string.setting_state_off); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + break; + default: + ((TextView) v.findViewById(R.id.item_state)).setText(""); + } + } + + v.setOnClickListener(mSettingsItemListener); + } + + @Override + public View getPinnedHeaderView(int viewIndex, View convertView, ViewGroup parent) { + LayoutInflater inflater = LayoutInflater.from(getContext()); + View view = inflater.inflate(R.layout.settings_pane_list_header, parent, false); + view.setFocusable(false); + view.setEnabled(false); + bindHeaderView(view, viewIndex, null); + return view; + } + + @Override + public int getPinnedHeaderCount() { + return mPinnedHeaderCount; + } + + /*public void updateDynamicGridSizeSettingsItem(View v) { + DeviceProfile.GridSize gridSize = DeviceProfile.GridSize.getModeForValue( + SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_DYNAMIC_GRID_SIZE, 0)); + String state = ""; + + switch (gridSize) { + case Comfortable: + state = mLauncher.getResources().getString(R.string.grid_size_comfortable); + break; + case Cozy: + state = mLauncher.getResources().getString(R.string.grid_size_cozy); + break; + case Condensed: + state = mLauncher.getResources().getString(R.string.grid_size_condensed); + break; + case Custom: + int rows = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_ROWS, 0); + int columns = SettingsProvider.getIntCustomDefault(mLauncher, + SettingsProvider.SETTINGS_UI_HOMESCREEN_COLUMNS, 0); + state = rows + " " + "\u00d7" + " " + columns; + break; + } + ((TextView) v.findViewById(R.id.item_state)).setText(state); + }*/ + + OnClickListener mSettingsItemListener = new OnClickListener() { + + @Override + public void onClick(View v) { + int partition = ((SettingsPosition) v.getTag()).partition; + int position = ((SettingsPosition) v.getTag()).position; + + switch (partition) { + case OverviewSettingsPanel.HOME_SETTINGS_POSITION: + switch (position) { + /*case 0: + updateSearchBarVisibility(v); + mLauncher.setUpdateDynamicGrid(false); + break;*/ + case 1: + onIconLabelsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_HIDE_ICON_LABELS, + R.bool.preferences_interface_homescreen_hide_icon_labels_default); + mLauncher.reloadLauncher(); + break; + case 2: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SCROLLING_WALLPAPER_SCROLL, + R.bool.preferences_interface_homescreen_scrolling_wallpaper_scroll_default); + mLauncher.reloadLauncher(); + break; + /*case 3: + mLauncher.onClickDynamicGridSizeButton(); + break;*/ + } + break; + case OverviewSettingsPanel.DRAWER_SETTINGS_POSITION: + switch (position) { + case 0: + onIconLabelsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_DRAWER_HIDE_ICON_LABELS, + R.bool.preferences_interface_drawer_hide_icon_labels_default); + mLauncher.reloadLauncher(); + break; + } + break; + default: + switch (position) { + case 0: + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_GENERAL_ICONS_LARGE, + R.bool.preferences_interface_general_icons_large_default); + mLauncher.reloadLauncher(); + break; + /*case 1: + Intent intent = new Intent(); + intent.setClassName(OverviewSettingsPanel.ANDROID_SETTINGS, + OverviewSettingsPanel.ANDROID_PROTECTED_APPS); + mLauncher.startActivity(intent); + break;*/ + } + } + + View defaultHome = mLauncher.findViewById(R.id.default_home_screen_panel); + defaultHome.setVisibility(getCursor(0).getCount() > 1 ? View.VISIBLE : View.GONE); + } + }; + + /*private void updateSearchBarVisibility(View v) { + boolean isSearchEnabled = SettingsProvider.getBoolean(mContext, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + + if (!isSearchEnabled) { + if (!Utilities.searchActivityExists(mContext)) { + Toast.makeText(mContext, mContext.getString(R.string.search_activity_not_found), + Toast.LENGTH_SHORT).show(); + return; + } + } + + onSettingsBooleanChanged(v, + SettingsProvider.SETTINGS_UI_HOMESCREEN_SEARCH, + R.bool.preferences_interface_homescreen_search_default); + }*/ + + private void onSettingsBooleanChanged(View v, String key, int res) { + boolean current = SettingsProvider.getBoolean( + mContext, key, res); + + // Set new state + SettingsProvider.putBoolean(mContext, key, !current); + SettingsProvider.putBoolean(mContext, SettingsProvider.SETTINGS_CHANGED, true); + + String state = current ? mLauncher.getResources().getString( + R.string.setting_state_off) : mLauncher.getResources().getString( + R.string.setting_state_on); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } + + private void onIconLabelsBooleanChanged(View v, String key, int res) { + boolean current = SettingsProvider.getBoolean( + mContext, key, res); + + // Set new state + SettingsProvider.putBoolean(mContext, key, !current); + SettingsProvider.putBoolean(mContext, SettingsProvider.SETTINGS_CHANGED, true); + + String state = current ? mLauncher.getResources().getString( + R.string.icon_labels_show) : mLauncher.getResources().getString( + R.string.icon_labels_hide); + ((TextView) v.findViewById(R.id.item_state)).setText(state); + } +}
\ No newline at end of file |