Merge "Initial implementation of NotificationStackScroller"
This commit is contained in:
@@ -17,5 +17,5 @@
|
|||||||
|
|
||||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<solid android:color="#ffffffff" />
|
<solid android:color="#ffffffff" />
|
||||||
<corners android:radius="2dp" />
|
<corners android:radius="@dimen/notification_quantum_rounded_rect_radius" />
|
||||||
</shape>
|
</shape>
|
||||||
@@ -366,4 +366,7 @@
|
|||||||
<!-- width of ImmersiveModeConfirmation (-1 for match_parent) -->
|
<!-- width of ImmersiveModeConfirmation (-1 for match_parent) -->
|
||||||
<dimen name="immersive_mode_cling_width">-1px</dimen>
|
<dimen name="immersive_mode_cling_width">-1px</dimen>
|
||||||
|
|
||||||
|
<!-- radius of the corners of the quantum rounded rect background -->
|
||||||
|
<dimen name="notification_quantum_rounded_rect_radius">2dp</dimen>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -346,6 +346,7 @@
|
|||||||
<java-symbol type="dimen" name="notification_title_text_size" />
|
<java-symbol type="dimen" name="notification_title_text_size" />
|
||||||
<java-symbol type="dimen" name="notification_subtext_size" />
|
<java-symbol type="dimen" name="notification_subtext_size" />
|
||||||
<java-symbol type="dimen" name="immersive_mode_cling_width" />
|
<java-symbol type="dimen" name="immersive_mode_cling_width" />
|
||||||
|
<java-symbol type="dimen" name="notification_quantum_rounded_rect_radius" />
|
||||||
|
|
||||||
<java-symbol type="string" name="add_account_button_label" />
|
<java-symbol type="string" name="add_account_button_label" />
|
||||||
<java-symbol type="string" name="addToDictionary" />
|
<java-symbol type="string" name="addToDictionary" />
|
||||||
|
|||||||
@@ -92,6 +92,12 @@
|
|||||||
systemui:rowHeight="@dimen/notification_row_min_height"
|
systemui:rowHeight="@dimen/notification_row_min_height"
|
||||||
/>
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
|
|
||||||
|
<com.android.systemui.statusbar.stack.NotificationStackScrollLayout
|
||||||
|
android:id="@+id/notification_stack_scroller"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
/>
|
||||||
</FrameLayout>
|
</FrameLayout>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
</com.android.systemui.statusbar.phone.NotificationPanelView><!-- end of sliding panel -->
|
</com.android.systemui.statusbar.phone.NotificationPanelView><!-- end of sliding panel -->
|
||||||
|
|||||||
@@ -233,6 +233,21 @@
|
|||||||
<!-- The size of the icon in the recents task view. -->
|
<!-- The size of the icon in the recents task view. -->
|
||||||
<dimen name="recents_task_view_icon_size">60dp</dimen>
|
<dimen name="recents_task_view_icon_size">60dp</dimen>
|
||||||
|
|
||||||
|
<!-- Space below the notification stack -->
|
||||||
|
<dimen name="notification_stack_margin_bottom">0dp</dimen>
|
||||||
|
|
||||||
|
<!-- Space reserved for the cards behind the top card in the top stack -->
|
||||||
|
<dimen name="top_stack_peek_amount">24dp</dimen>
|
||||||
|
|
||||||
|
<!-- Space reserved for the cards behind the top card in the bottom stack -->
|
||||||
|
<dimen name="bottom_stack_peek_amount">18dp</dimen>
|
||||||
|
|
||||||
|
<!-- The side padding of the notifications-->
|
||||||
|
<dimen name="notification_side_padding">8dp</dimen>
|
||||||
|
|
||||||
|
<!-- Z distance between notifications if they are in the stack -->
|
||||||
|
<dimen name="z_distance_between_notifications">2dp</dimen>
|
||||||
|
|
||||||
<!-- Width of the zen mode interstitial dialog. Defaults to MATCH_PARENT. -->
|
<!-- Width of the zen mode interstitial dialog. Defaults to MATCH_PARENT. -->
|
||||||
<dimen name="zen_mode_dialog_width">-1px</dimen>
|
<dimen name="zen_mode_dialog_width">-1px</dimen>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import android.os.Message;
|
|||||||
import android.os.PowerManager;
|
import android.os.PowerManager;
|
||||||
import android.os.RemoteException;
|
import android.os.RemoteException;
|
||||||
import android.os.ServiceManager;
|
import android.os.ServiceManager;
|
||||||
|
import android.os.SystemProperties;
|
||||||
import android.os.UserHandle;
|
import android.os.UserHandle;
|
||||||
import android.os.UserManager;
|
import android.os.UserManager;
|
||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
@@ -77,7 +78,6 @@ import com.android.systemui.RecentsComponent;
|
|||||||
import com.android.systemui.SearchPanelView;
|
import com.android.systemui.SearchPanelView;
|
||||||
import com.android.systemui.SystemUI;
|
import com.android.systemui.SystemUI;
|
||||||
import com.android.systemui.statusbar.phone.KeyguardTouchDelegate;
|
import com.android.systemui.statusbar.phone.KeyguardTouchDelegate;
|
||||||
import com.android.systemui.statusbar.policy.NotificationRowLayout;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -98,6 +98,8 @@ public abstract class BaseStatusBar extends SystemUI implements
|
|||||||
protected static final int MSG_HIDE_HEADS_UP = 1027;
|
protected static final int MSG_HIDE_HEADS_UP = 1027;
|
||||||
protected static final int MSG_ESCALATE_HEADS_UP = 1028;
|
protected static final int MSG_ESCALATE_HEADS_UP = 1028;
|
||||||
|
|
||||||
|
public static final boolean ENABLE_NOTIFICATION_STACK = SystemProperties
|
||||||
|
.getBoolean("persist.notifications.use_stack", false);
|
||||||
protected static final boolean ENABLE_HEADS_UP = true;
|
protected static final boolean ENABLE_HEADS_UP = true;
|
||||||
// scores above this threshold should be displayed in heads up mode.
|
// scores above this threshold should be displayed in heads up mode.
|
||||||
protected static final int INTERRUPTION_THRESHOLD = 10;
|
protected static final int INTERRUPTION_THRESHOLD = 10;
|
||||||
@@ -118,7 +120,7 @@ public abstract class BaseStatusBar extends SystemUI implements
|
|||||||
|
|
||||||
// all notifications
|
// all notifications
|
||||||
protected NotificationData mNotificationData = new NotificationData();
|
protected NotificationData mNotificationData = new NotificationData();
|
||||||
protected NotificationRowLayout mPile;
|
protected ViewGroup mPile;
|
||||||
|
|
||||||
protected NotificationData.Entry mInterruptingNotificationEntry;
|
protected NotificationData.Entry mInterruptingNotificationEntry;
|
||||||
protected long mInterruptingNotificationTime;
|
protected long mInterruptingNotificationTime;
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ import com.android.internal.statusbar.StatusBarIcon;
|
|||||||
import com.android.systemui.DemoMode;
|
import com.android.systemui.DemoMode;
|
||||||
import com.android.systemui.EventLogTags;
|
import com.android.systemui.EventLogTags;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.SwipeHelper;
|
||||||
import com.android.systemui.statusbar.BaseStatusBar;
|
import com.android.systemui.statusbar.BaseStatusBar;
|
||||||
import com.android.systemui.statusbar.CommandQueue;
|
import com.android.systemui.statusbar.CommandQueue;
|
||||||
import com.android.systemui.statusbar.GestureRecorder;
|
import com.android.systemui.statusbar.GestureRecorder;
|
||||||
@@ -94,6 +95,7 @@ import com.android.systemui.statusbar.NotificationData;
|
|||||||
import com.android.systemui.statusbar.NotificationData.Entry;
|
import com.android.systemui.statusbar.NotificationData.Entry;
|
||||||
import com.android.systemui.statusbar.SignalClusterView;
|
import com.android.systemui.statusbar.SignalClusterView;
|
||||||
import com.android.systemui.statusbar.StatusBarIconView;
|
import com.android.systemui.statusbar.StatusBarIconView;
|
||||||
|
|
||||||
import com.android.systemui.statusbar.policy.BatteryController;
|
import com.android.systemui.statusbar.policy.BatteryController;
|
||||||
import com.android.systemui.statusbar.policy.BluetoothController;
|
import com.android.systemui.statusbar.policy.BluetoothController;
|
||||||
import com.android.systemui.statusbar.policy.DateView;
|
import com.android.systemui.statusbar.policy.DateView;
|
||||||
@@ -104,6 +106,8 @@ import com.android.systemui.statusbar.policy.NotificationRowLayout;
|
|||||||
import com.android.systemui.statusbar.policy.OnSizeChangedListener;
|
import com.android.systemui.statusbar.policy.OnSizeChangedListener;
|
||||||
import com.android.systemui.statusbar.policy.RotationLockController;
|
import com.android.systemui.statusbar.policy.RotationLockController;
|
||||||
|
|
||||||
|
import com.android.systemui.statusbar.stack.NotificationStackScrollLayout;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.PrintWriter;
|
import java.io.PrintWriter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -479,11 +483,25 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents);
|
mStatusBarContents = (LinearLayout)mStatusBarView.findViewById(R.id.status_bar_contents);
|
||||||
mTickerView = mStatusBarView.findViewById(R.id.ticker);
|
mTickerView = mStatusBarView.findViewById(R.id.ticker);
|
||||||
|
|
||||||
mPile = (NotificationRowLayout)mStatusBarWindow.findViewById(R.id.latestItems);
|
NotificationRowLayout rowLayout
|
||||||
mPile.setLayoutTransitionsEnabled(false);
|
= (NotificationRowLayout) mStatusBarWindow.findViewById(R.id.latestItems);
|
||||||
mPile.setLongPressListener(getNotificationLongClicker());
|
NotificationStackScrollLayout notificationStack
|
||||||
|
= (NotificationStackScrollLayout) mStatusBarWindow
|
||||||
|
.findViewById(R.id.notification_stack_scroller);
|
||||||
|
if (ENABLE_NOTIFICATION_STACK) {
|
||||||
|
notificationStack.setLongPressListener(getNotificationLongClicker());
|
||||||
|
mPile = notificationStack;
|
||||||
|
} else {
|
||||||
|
rowLayout.setLayoutTransitionsEnabled(false);
|
||||||
|
rowLayout.setLongPressListener(getNotificationLongClicker());
|
||||||
|
mPile = rowLayout;
|
||||||
|
notificationStack.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout);
|
mExpandedContents = mPile; // was: expanded.findViewById(R.id.notificationLinearLayout);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header);
|
mNotificationPanelHeader = mStatusBarWindow.findViewById(R.id.header);
|
||||||
|
|
||||||
mClearButton = mStatusBarWindow.findViewById(R.id.clear_all_button);
|
mClearButton = mStatusBarWindow.findViewById(R.id.clear_all_button);
|
||||||
@@ -597,7 +615,8 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set up the dynamic hide/show of the label
|
// set up the dynamic hide/show of the label
|
||||||
mPile.setOnSizeChangedListener(new OnSizeChangedListener() {
|
if(!ENABLE_NOTIFICATION_STACK)
|
||||||
|
((NotificationRowLayout) mPile).setOnSizeChangedListener(new OnSizeChangedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onSizeChanged(View view, int w, int h, int oldw, int oldh) {
|
public void onSizeChanged(View view, int w, int h, int oldw, int oldh) {
|
||||||
updateCarrierLabelVisibility(false);
|
updateCarrierLabelVisibility(false);
|
||||||
@@ -1457,7 +1476,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mExpandedVisible = true;
|
mExpandedVisible = true;
|
||||||
mPile.setLayoutTransitionsEnabled(true);
|
if(!ENABLE_NOTIFICATION_STACK) {
|
||||||
|
((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(true);
|
||||||
|
}
|
||||||
if (mNavigationBarView != null)
|
if (mNavigationBarView != null)
|
||||||
mNavigationBarView.setSlippery(true);
|
mNavigationBarView.setSlippery(true);
|
||||||
|
|
||||||
@@ -1749,7 +1770,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mExpandedVisible = false;
|
mExpandedVisible = false;
|
||||||
mPile.setLayoutTransitionsEnabled(false);
|
if(!ENABLE_NOTIFICATION_STACK) {
|
||||||
|
((NotificationRowLayout) mPile).setLayoutTransitionsEnabled(false);
|
||||||
|
}
|
||||||
if (mNavigationBarView != null)
|
if (mNavigationBarView != null)
|
||||||
mNavigationBarView.setSlippery(false);
|
mNavigationBarView.setSlippery(false);
|
||||||
visibilityChanged(false);
|
visibilityChanged(false);
|
||||||
@@ -2406,7 +2429,7 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
final ArrayList<View> snapshot = new ArrayList<View>(numChildren);
|
final ArrayList<View> snapshot = new ArrayList<View>(numChildren);
|
||||||
for (int i=0; i<numChildren; i++) {
|
for (int i=0; i<numChildren; i++) {
|
||||||
final View child = mPile.getChildAt(i);
|
final View child = mPile.getChildAt(i);
|
||||||
if (mPile.canChildBeDismissed(child) && child.getBottom() > scrollTop &&
|
if (((SwipeHelper.Callback) mPile).canChildBeDismissed(child) && child.getBottom() > scrollTop &&
|
||||||
child.getTop() < scrollBottom) {
|
child.getTop() < scrollBottom) {
|
||||||
snapshot.add(child);
|
snapshot.add(child);
|
||||||
}
|
}
|
||||||
@@ -2424,10 +2447,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
int currentDelay = 140;
|
int currentDelay = 140;
|
||||||
int totalDelay = 0;
|
int totalDelay = 0;
|
||||||
|
|
||||||
// Set the shade-animating state to avoid doing other work during
|
|
||||||
// all of these animations. In particular, avoid layout and
|
if(!ENABLE_NOTIFICATION_STACK) {
|
||||||
// redrawing when collapsing the shade.
|
// Set the shade-animating state to avoid doing other work during
|
||||||
mPile.setViewRemoval(false);
|
// all of these animations. In particular, avoid layout and
|
||||||
|
// redrawing when collapsing the shade.
|
||||||
|
((NotificationRowLayout) mPile).setViewRemoval(false);
|
||||||
|
}
|
||||||
|
|
||||||
mPostCollapseCleanup = new Runnable() {
|
mPostCollapseCleanup = new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
@@ -2436,7 +2462,9 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
Log.v(TAG, "running post-collapse cleanup");
|
Log.v(TAG, "running post-collapse cleanup");
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
mPile.setViewRemoval(true);
|
if (!ENABLE_NOTIFICATION_STACK) {
|
||||||
|
((NotificationRowLayout) mPile).setViewRemoval(true);
|
||||||
|
}
|
||||||
mBarService.onClearAllNotifications(mCurrentUserId);
|
mBarService.onClearAllNotifications(mCurrentUserId);
|
||||||
} catch (Exception ex) { }
|
} catch (Exception ex) { }
|
||||||
}
|
}
|
||||||
@@ -2450,7 +2478,13 @@ public class PhoneStatusBar extends BaseStatusBar implements DemoMode {
|
|||||||
mHandler.postDelayed(new Runnable() {
|
mHandler.postDelayed(new Runnable() {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
mPile.dismissRowAnimated(_v, velocity);
|
if (!ENABLE_NOTIFICATION_STACK) {
|
||||||
|
((NotificationRowLayout) mPile).dismissRowAnimated(
|
||||||
|
_v, velocity);
|
||||||
|
} else {
|
||||||
|
((NotificationStackScrollLayout) mPile).dismissRowAnimated(
|
||||||
|
_v, velocity);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, totalDelay);
|
}, totalDelay);
|
||||||
currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT);
|
currentDelay = Math.max(50, currentDelay - ROW_DELAY_DECREMENT);
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import android.util.AttributeSet;
|
|||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import android.view.ViewRootImpl;
|
import android.view.ViewRootImpl;
|
||||||
import android.widget.FrameLayout;
|
import android.widget.FrameLayout;
|
||||||
import android.widget.ScrollView;
|
import android.widget.ScrollView;
|
||||||
@@ -31,7 +32,6 @@ import android.widget.ScrollView;
|
|||||||
import com.android.systemui.ExpandHelper;
|
import com.android.systemui.ExpandHelper;
|
||||||
import com.android.systemui.R;
|
import com.android.systemui.R;
|
||||||
import com.android.systemui.statusbar.BaseStatusBar;
|
import com.android.systemui.statusbar.BaseStatusBar;
|
||||||
import com.android.systemui.statusbar.policy.NotificationRowLayout;
|
|
||||||
|
|
||||||
|
|
||||||
public class StatusBarWindowView extends FrameLayout
|
public class StatusBarWindowView extends FrameLayout
|
||||||
@@ -40,7 +40,7 @@ public class StatusBarWindowView extends FrameLayout
|
|||||||
public static final boolean DEBUG = BaseStatusBar.DEBUG;
|
public static final boolean DEBUG = BaseStatusBar.DEBUG;
|
||||||
|
|
||||||
private ExpandHelper mExpandHelper;
|
private ExpandHelper mExpandHelper;
|
||||||
private NotificationRowLayout latestItems;
|
private ViewGroup latestItems;
|
||||||
private NotificationPanelView mNotificationPanel;
|
private NotificationPanelView mNotificationPanel;
|
||||||
private ScrollView mScrollView;
|
private ScrollView mScrollView;
|
||||||
|
|
||||||
@@ -55,12 +55,18 @@ public class StatusBarWindowView extends FrameLayout
|
|||||||
@Override
|
@Override
|
||||||
protected void onAttachedToWindow () {
|
protected void onAttachedToWindow () {
|
||||||
super.onAttachedToWindow();
|
super.onAttachedToWindow();
|
||||||
latestItems = (NotificationRowLayout) findViewById(R.id.latestItems);
|
|
||||||
|
if (BaseStatusBar.ENABLE_NOTIFICATION_STACK) {
|
||||||
|
latestItems = (ViewGroup) findViewById(R.id.notification_stack_scroller);
|
||||||
|
} else {
|
||||||
|
latestItems = (ViewGroup) findViewById(R.id.latestItems);
|
||||||
|
}
|
||||||
mScrollView = (ScrollView) findViewById(R.id.scroll);
|
mScrollView = (ScrollView) findViewById(R.id.scroll);
|
||||||
mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
|
mNotificationPanel = (NotificationPanelView) findViewById(R.id.notification_panel);
|
||||||
int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
|
int minHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_min_height);
|
||||||
int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
|
int maxHeight = getResources().getDimensionPixelSize(R.dimen.notification_row_max_height);
|
||||||
mExpandHelper = new ExpandHelper(getContext(), latestItems, minHeight, maxHeight);
|
mExpandHelper = new ExpandHelper(getContext(), (ExpandHelper.Callback) latestItems,
|
||||||
|
minHeight, maxHeight);
|
||||||
mExpandHelper.setEventSource(this);
|
mExpandHelper.setEventSource(this);
|
||||||
mExpandHelper.setScrollView(mScrollView);
|
mExpandHelper.setScrollView(mScrollView);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,816 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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.systemui.statusbar.stack;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.res.Configuration;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Outline;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import android.view.MotionEvent;
|
||||||
|
import android.view.VelocityTracker;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewConfiguration;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.view.ViewParent;
|
||||||
|
import android.widget.OverScroller;
|
||||||
|
|
||||||
|
import com.android.systemui.ExpandHelper;
|
||||||
|
import com.android.systemui.R;
|
||||||
|
import com.android.systemui.SwipeHelper;
|
||||||
|
import com.android.systemui.statusbar.ExpandableNotificationRow;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A layout which handles a dynamic amount of notifications and presents them in a scrollable stack.
|
||||||
|
*/
|
||||||
|
public class NotificationStackScrollLayout extends ViewGroup
|
||||||
|
implements SwipeHelper.Callback, ExpandHelper.Callback {
|
||||||
|
|
||||||
|
private static final String TAG = "NotificationStackScrollLayout";
|
||||||
|
private static final boolean DEBUG = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sentinel value for no current active pointer. Used by {@link #mActivePointerId}.
|
||||||
|
*/
|
||||||
|
private static final int INVALID_POINTER = -1;
|
||||||
|
|
||||||
|
private SwipeHelper mSwipeHelper;
|
||||||
|
private boolean mAllowScrolling = true;
|
||||||
|
private int mCurrentStackHeight = Integer.MAX_VALUE;
|
||||||
|
private int mOwnScrollY;
|
||||||
|
private int mMaxLayoutHeight;
|
||||||
|
|
||||||
|
private VelocityTracker mVelocityTracker;
|
||||||
|
private OverScroller mScroller;
|
||||||
|
private int mTouchSlop;
|
||||||
|
private int mMinimumVelocity;
|
||||||
|
private int mMaximumVelocity;
|
||||||
|
private int mOverscrollDistance;
|
||||||
|
private int mOverflingDistance;
|
||||||
|
private boolean mIsBeingDragged;
|
||||||
|
private int mLastMotionY;
|
||||||
|
private int mActivePointerId;
|
||||||
|
|
||||||
|
private int mSidePaddings;
|
||||||
|
private Paint mDebugPaint;
|
||||||
|
private int mBackgroundRoundedRectCornerRadius;
|
||||||
|
private int mContentHeight;
|
||||||
|
private int mCollapsedSize;
|
||||||
|
private int mBottomStackPeekSize;
|
||||||
|
private int mEmptyMarginBottom;
|
||||||
|
private int mPaddingBetweenElements;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The algorithm which calculates the properties for our children
|
||||||
|
*/
|
||||||
|
private StackScrollAlgorithm mStackScrollAlgorithm;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current State this Layout is in
|
||||||
|
*/
|
||||||
|
private StackScrollState mCurrentStackScrollState;
|
||||||
|
|
||||||
|
public NotificationStackScrollLayout(Context context) {
|
||||||
|
this(context, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationStackScrollLayout(Context context, AttributeSet attrs) {
|
||||||
|
this(context, attrs, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr) {
|
||||||
|
this(context, attrs, defStyleAttr, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public NotificationStackScrollLayout(Context context, AttributeSet attrs, int defStyleAttr,
|
||||||
|
int defStyleRes) {
|
||||||
|
super(context, attrs, defStyleAttr, defStyleRes);
|
||||||
|
initView(context);
|
||||||
|
if (DEBUG) {
|
||||||
|
setWillNotDraw(false);
|
||||||
|
mDebugPaint = new Paint();
|
||||||
|
mDebugPaint.setColor(0xffff0000);
|
||||||
|
mDebugPaint.setStrokeWidth(2);
|
||||||
|
mDebugPaint.setStyle(Paint.Style.STROKE);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDraw(Canvas canvas) {
|
||||||
|
if (DEBUG) {
|
||||||
|
int y = mCollapsedSize;
|
||||||
|
canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
|
||||||
|
y = (int) (getLayoutHeight() - mBottomStackPeekSize - mCollapsedSize);
|
||||||
|
canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
|
||||||
|
y = (int) getLayoutHeight();
|
||||||
|
canvas.drawLine(0, y, getWidth(), y, mDebugPaint);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initView(Context context) {
|
||||||
|
mScroller = new OverScroller(getContext());
|
||||||
|
setFocusable(true);
|
||||||
|
setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
|
||||||
|
final ViewConfiguration configuration = ViewConfiguration.get(context);
|
||||||
|
mTouchSlop = configuration.getScaledTouchSlop();
|
||||||
|
mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
|
||||||
|
mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
|
||||||
|
mOverscrollDistance = configuration.getScaledOverscrollDistance();
|
||||||
|
mOverflingDistance = configuration.getScaledOverflingDistance();
|
||||||
|
float densityScale = getResources().getDisplayMetrics().density;
|
||||||
|
float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
|
||||||
|
mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, pagingTouchSlop);
|
||||||
|
|
||||||
|
mSidePaddings = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.notification_side_padding);
|
||||||
|
mBackgroundRoundedRectCornerRadius = context.getResources()
|
||||||
|
.getDimensionPixelSize(
|
||||||
|
com.android.internal.R.dimen.notification_quantum_rounded_rect_radius);
|
||||||
|
mCollapsedSize = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.notification_row_min_height);
|
||||||
|
mBottomStackPeekSize = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
|
||||||
|
mEmptyMarginBottom = context.getResources().getDimensionPixelSize(
|
||||||
|
R.dimen.notification_stack_margin_bottom);
|
||||||
|
// currently the padding is in the elements themself
|
||||||
|
mPaddingBetweenElements = 0;
|
||||||
|
mStackScrollAlgorithm = new StackScrollAlgorithm(context);
|
||||||
|
mCurrentStackScrollState = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
|
||||||
|
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
|
||||||
|
int mode = MeasureSpec.getMode(widthMeasureSpec);
|
||||||
|
int size = MeasureSpec.getSize(widthMeasureSpec);
|
||||||
|
int childMeasureSpec = MeasureSpec.makeMeasureSpec(size - 2 * mSidePaddings, mode);
|
||||||
|
measureChildren(childMeasureSpec, heightMeasureSpec);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onLayout(boolean changed, int l, int t, int r, int b) {
|
||||||
|
|
||||||
|
// we layout all our children centered on the top
|
||||||
|
float centerX = getWidth() / 2.0f;
|
||||||
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
|
View child = getChildAt(i);
|
||||||
|
float width = child.getMeasuredWidth();
|
||||||
|
float height = child.getMeasuredHeight();
|
||||||
|
int oldWidth = child.getWidth();
|
||||||
|
int oldHeight = child.getHeight();
|
||||||
|
child.layout((int) (centerX - width / 2.0f),
|
||||||
|
0,
|
||||||
|
(int) (centerX + width / 2.0f),
|
||||||
|
(int) height);
|
||||||
|
updateChildOutline(child, width, height, oldWidth, oldHeight);
|
||||||
|
}
|
||||||
|
setMaxLayoutHeight(getHeight() - mEmptyMarginBottom);
|
||||||
|
updateScrollPositionIfNecessary();
|
||||||
|
updateChildren();
|
||||||
|
updateContentHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setMaxLayoutHeight(int maxLayoutHeight) {
|
||||||
|
mMaxLayoutHeight = maxLayoutHeight;
|
||||||
|
updateAlgorithmHeight();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateAlgorithmHeight() {
|
||||||
|
mStackScrollAlgorithm.setLayoutHeight(getLayoutHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the children views according to the stack scroll algorithm. Call this whenever
|
||||||
|
* modifications to {@link #mOwnScrollY} are performed to reflect it in the view layout.
|
||||||
|
*/
|
||||||
|
private void updateChildren() {
|
||||||
|
if (!isCurrentlyAnimating()) {
|
||||||
|
if (mCurrentStackScrollState == null) {
|
||||||
|
mCurrentStackScrollState = new StackScrollState(this);
|
||||||
|
}
|
||||||
|
mCurrentStackScrollState.setScrollY(mOwnScrollY);
|
||||||
|
mStackScrollAlgorithm.getStackScrollState(mCurrentStackScrollState);
|
||||||
|
mCurrentStackScrollState.apply();
|
||||||
|
mOwnScrollY = mCurrentStackScrollState.getScrollY();
|
||||||
|
} else {
|
||||||
|
// TODO: handle animation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isCurrentlyAnimating() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateChildOutline(View child,
|
||||||
|
float width,
|
||||||
|
float height,
|
||||||
|
int oldWidth,
|
||||||
|
int oldHeight) {
|
||||||
|
// The children currently have paddings inside themselfs because of the expansion
|
||||||
|
// visualization. In order for the shadows to work correctly we have to set the correct
|
||||||
|
// outline.
|
||||||
|
View container = child.findViewById(R.id.container);
|
||||||
|
if (container != null && (oldWidth != width || oldHeight != height)) {
|
||||||
|
Outline outline = getOutlineForSize(container.getLeft(),
|
||||||
|
container.getTop(),
|
||||||
|
container.getWidth(),
|
||||||
|
container.getHeight());
|
||||||
|
child.setOutline(outline);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Outline getOutlineForSize(int leftInset, int topInset, int width, int height) {
|
||||||
|
Outline result = new Outline();
|
||||||
|
result.setRoundRect(leftInset, topInset, leftInset + width, topInset + height,
|
||||||
|
mBackgroundRoundedRectCornerRadius);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateScrollPositionIfNecessary() {
|
||||||
|
int scrollRange = getScrollRange();
|
||||||
|
if (scrollRange < mOwnScrollY) {
|
||||||
|
mOwnScrollY = scrollRange;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCurrentStackHeight(int currentStackHeight) {
|
||||||
|
this.mCurrentStackHeight = currentStackHeight;
|
||||||
|
updateAlgorithmHeight();
|
||||||
|
updateChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current height of the view. This is at most the size of the view given by a the
|
||||||
|
* layout but it can also be made smaller by setting {@link #mCurrentStackHeight}
|
||||||
|
*
|
||||||
|
* @return either the layout height or the externally defined height, whichever is smaller
|
||||||
|
*/
|
||||||
|
private float getLayoutHeight() {
|
||||||
|
return Math.min(mMaxLayoutHeight, mCurrentStackHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLongPressListener(View.OnLongClickListener listener) {
|
||||||
|
mSwipeHelper.setLongPressListener(listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onChildDismissed(View v) {
|
||||||
|
if (DEBUG) Log.v(TAG, "onChildDismissed: " + v);
|
||||||
|
final View veto = v.findViewById(R.id.veto);
|
||||||
|
if (veto != null && veto.getVisibility() != View.GONE) {
|
||||||
|
veto.performClick();
|
||||||
|
}
|
||||||
|
allowScrolling(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onBeginDrag(View v) {
|
||||||
|
allowScrolling(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onDragCancelled(View v) {
|
||||||
|
allowScrolling(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getChildAtPosition(MotionEvent ev) {
|
||||||
|
return getChildAtPosition(ev.getX(), ev.getY());
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getChildAtRawPosition(float touchX, float touchY) {
|
||||||
|
int[] location = new int[2];
|
||||||
|
getLocationOnScreen(location);
|
||||||
|
return getChildAtPosition(touchX - location[0],touchY - location[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getChildAtPosition(float touchX, float touchY) {
|
||||||
|
// find the view under the pointer, accounting for GONE views
|
||||||
|
final int count = getChildCount();
|
||||||
|
for (int childIdx = 0; childIdx < count; childIdx++) {
|
||||||
|
View slidingChild = getChildAt(childIdx);
|
||||||
|
if (slidingChild.getVisibility() == GONE) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
float top = slidingChild.getTranslationY();
|
||||||
|
float bottom = top + slidingChild.getMeasuredHeight();
|
||||||
|
int left = slidingChild.getLeft();
|
||||||
|
int right = slidingChild.getRight();
|
||||||
|
|
||||||
|
if (touchY >= top && touchY <= bottom && touchX >= left && touchX <= right) {
|
||||||
|
return slidingChild;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canChildBeExpanded(View v) {
|
||||||
|
return v instanceof ExpandableNotificationRow
|
||||||
|
&& ((ExpandableNotificationRow) v).isExpandable();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserExpandedChild(View v, boolean userExpanded) {
|
||||||
|
if (v instanceof ExpandableNotificationRow) {
|
||||||
|
((ExpandableNotificationRow) v).setUserExpanded(userExpanded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUserLockedChild(View v, boolean userLocked) {
|
||||||
|
if (v instanceof ExpandableNotificationRow) {
|
||||||
|
((ExpandableNotificationRow) v).setUserLocked(userLocked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public View getChildContentView(View v) {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean canChildBeDismissed(View v) {
|
||||||
|
final View veto = v.findViewById(R.id.veto);
|
||||||
|
return (veto != null && veto.getVisibility() != View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void allowScrolling(boolean allow) {
|
||||||
|
mAllowScrolling = allow;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onConfigurationChanged(Configuration newConfig) {
|
||||||
|
super.onConfigurationChanged(newConfig);
|
||||||
|
float densityScale = getResources().getDisplayMetrics().density;
|
||||||
|
mSwipeHelper.setDensityScale(densityScale);
|
||||||
|
float pagingTouchSlop = ViewConfiguration.get(getContext()).getScaledPagingTouchSlop();
|
||||||
|
mSwipeHelper.setPagingTouchSlop(pagingTouchSlop);
|
||||||
|
initView(getContext());
|
||||||
|
}
|
||||||
|
|
||||||
|
public void dismissRowAnimated(View child, int vel) {
|
||||||
|
mSwipeHelper.dismissChild(child, vel);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onTouchEvent(MotionEvent ev) {
|
||||||
|
boolean scrollerWantsIt = false;
|
||||||
|
if (mAllowScrolling) {
|
||||||
|
scrollerWantsIt = onScrollTouch(ev);
|
||||||
|
}
|
||||||
|
boolean horizontalSwipeWantsIt = false;
|
||||||
|
if (!mIsBeingDragged) {
|
||||||
|
horizontalSwipeWantsIt = mSwipeHelper.onTouchEvent(ev);
|
||||||
|
}
|
||||||
|
return horizontalSwipeWantsIt || scrollerWantsIt || super.onTouchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onScrollTouch(MotionEvent ev) {
|
||||||
|
initVelocityTrackerIfNotExists();
|
||||||
|
mVelocityTracker.addMovement(ev);
|
||||||
|
|
||||||
|
final int action = ev.getAction();
|
||||||
|
|
||||||
|
switch (action & MotionEvent.ACTION_MASK) {
|
||||||
|
case MotionEvent.ACTION_DOWN: {
|
||||||
|
if (getChildCount() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
boolean isBeingDragged = !mScroller.isFinished();
|
||||||
|
setIsBeingDragged(isBeingDragged);
|
||||||
|
if (isBeingDragged) {
|
||||||
|
final ViewParent parent = getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* If being flinged and user touches, stop the fling. isFinished
|
||||||
|
* will be false if being flinged.
|
||||||
|
*/
|
||||||
|
if (!mScroller.isFinished()) {
|
||||||
|
mScroller.abortAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remember where the motion event started
|
||||||
|
mLastMotionY = (int) ev.getY();
|
||||||
|
mActivePointerId = ev.getPointerId(0);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_MOVE:
|
||||||
|
final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
|
||||||
|
if (activePointerIndex == -1) {
|
||||||
|
Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int y = (int) ev.getY(activePointerIndex);
|
||||||
|
int deltaY = mLastMotionY - y;
|
||||||
|
if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
|
||||||
|
final ViewParent parent = getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true);
|
||||||
|
}
|
||||||
|
setIsBeingDragged(true);
|
||||||
|
if (deltaY > 0) {
|
||||||
|
deltaY -= mTouchSlop;
|
||||||
|
} else {
|
||||||
|
deltaY += mTouchSlop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (mIsBeingDragged) {
|
||||||
|
// Scroll to follow the motion event
|
||||||
|
mLastMotionY = y;
|
||||||
|
|
||||||
|
final int oldX = mScrollX;
|
||||||
|
final int oldY = mOwnScrollY;
|
||||||
|
final int range = getScrollRange();
|
||||||
|
final int overscrollMode = getOverScrollMode();
|
||||||
|
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
|
||||||
|
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
|
||||||
|
|
||||||
|
// Calling overScrollBy will call onOverScrolled, which
|
||||||
|
// calls onScrollChanged if applicable.
|
||||||
|
if (overScrollBy(0, deltaY, 0, mOwnScrollY,
|
||||||
|
0, range, 0, mOverscrollDistance, true)) {
|
||||||
|
// Break our velocity if we hit a scroll barrier.
|
||||||
|
mVelocityTracker.clear();
|
||||||
|
}
|
||||||
|
// TODO: Overscroll
|
||||||
|
// if (canOverscroll) {
|
||||||
|
// final int pulledToY = oldY + deltaY;
|
||||||
|
// if (pulledToY < 0) {
|
||||||
|
// mEdgeGlowTop.onPull((float) deltaY / getHeight());
|
||||||
|
// if (!mEdgeGlowBottom.isFinished()) {
|
||||||
|
// mEdgeGlowBottom.onRelease();
|
||||||
|
// }
|
||||||
|
// } else if (pulledToY > range) {
|
||||||
|
// mEdgeGlowBottom.onPull((float) deltaY / getHeight());
|
||||||
|
// if (!mEdgeGlowTop.isFinished()) {
|
||||||
|
// mEdgeGlowTop.onRelease();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (mEdgeGlowTop != null
|
||||||
|
// && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())){
|
||||||
|
// postInvalidateOnAnimation();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
if (mIsBeingDragged) {
|
||||||
|
final VelocityTracker velocityTracker = mVelocityTracker;
|
||||||
|
velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
|
||||||
|
int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
|
||||||
|
|
||||||
|
if (getChildCount() > 0) {
|
||||||
|
if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
|
||||||
|
fling(-initialVelocity);
|
||||||
|
} else {
|
||||||
|
if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0,
|
||||||
|
getScrollRange())) {
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
mActivePointerId = INVALID_POINTER;
|
||||||
|
endDrag();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
if (mIsBeingDragged && getChildCount() > 0) {
|
||||||
|
if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
}
|
||||||
|
mActivePointerId = INVALID_POINTER;
|
||||||
|
endDrag();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_POINTER_DOWN: {
|
||||||
|
final int index = ev.getActionIndex();
|
||||||
|
mLastMotionY = (int) ev.getY(index);
|
||||||
|
mActivePointerId = ev.getPointerId(index);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
|
onSecondaryPointerUp(ev);
|
||||||
|
mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onSecondaryPointerUp(MotionEvent ev) {
|
||||||
|
final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
|
||||||
|
MotionEvent.ACTION_POINTER_INDEX_SHIFT;
|
||||||
|
final int pointerId = ev.getPointerId(pointerIndex);
|
||||||
|
if (pointerId == mActivePointerId) {
|
||||||
|
// This was our active pointer going up. Choose a new
|
||||||
|
// active pointer and adjust accordingly.
|
||||||
|
// TODO: Make this decision more intelligent.
|
||||||
|
final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
|
||||||
|
mLastMotionY = (int) ev.getY(newPointerIndex);
|
||||||
|
mActivePointerId = ev.getPointerId(newPointerIndex);
|
||||||
|
if (mVelocityTracker != null) {
|
||||||
|
mVelocityTracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initVelocityTrackerIfNotExists() {
|
||||||
|
if (mVelocityTracker == null) {
|
||||||
|
mVelocityTracker = VelocityTracker.obtain();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void recycleVelocityTracker() {
|
||||||
|
if (mVelocityTracker != null) {
|
||||||
|
mVelocityTracker.recycle();
|
||||||
|
mVelocityTracker = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initOrResetVelocityTracker() {
|
||||||
|
if (mVelocityTracker == null) {
|
||||||
|
mVelocityTracker = VelocityTracker.obtain();
|
||||||
|
} else {
|
||||||
|
mVelocityTracker.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void computeScroll() {
|
||||||
|
if (mScroller.computeScrollOffset()) {
|
||||||
|
// This is called at drawing time by ViewGroup.
|
||||||
|
int oldX = mScrollX;
|
||||||
|
int oldY = mOwnScrollY;
|
||||||
|
int x = mScroller.getCurrX();
|
||||||
|
int y = mScroller.getCurrY();
|
||||||
|
|
||||||
|
if (oldX != x || oldY != y) {
|
||||||
|
final int range = getScrollRange();
|
||||||
|
final int overscrollMode = getOverScrollMode();
|
||||||
|
final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
|
||||||
|
(overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
|
||||||
|
|
||||||
|
overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
|
||||||
|
0, mOverflingDistance, false);
|
||||||
|
onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
|
||||||
|
|
||||||
|
if (canOverscroll) {
|
||||||
|
// TODO: Overscroll
|
||||||
|
// if (y < 0 && oldY >= 0) {
|
||||||
|
// mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
|
||||||
|
// } else if (y > range && oldY <= range) {
|
||||||
|
// mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
updateChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keep on drawing until the animation has finished.
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customScrollBy(int y) {
|
||||||
|
mOwnScrollY += y;
|
||||||
|
updateChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void customScrollTo(int y) {
|
||||||
|
mOwnScrollY = y;
|
||||||
|
updateChildren();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onOverScrolled(int scrollX, int scrollY,
|
||||||
|
boolean clampedX, boolean clampedY) {
|
||||||
|
// Treat animating scrolls differently; see #computeScroll() for why.
|
||||||
|
if (!mScroller.isFinished()) {
|
||||||
|
final int oldX = mScrollX;
|
||||||
|
final int oldY = mOwnScrollY;
|
||||||
|
mScrollX = scrollX;
|
||||||
|
mOwnScrollY = scrollY;
|
||||||
|
invalidateParentIfNeeded();
|
||||||
|
onScrollChanged(mScrollX, mOwnScrollY, oldX, oldY);
|
||||||
|
if (clampedY) {
|
||||||
|
mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange());
|
||||||
|
}
|
||||||
|
updateChildren();
|
||||||
|
} else {
|
||||||
|
customScrollTo(scrollY);
|
||||||
|
scrollTo(scrollX, mScrollY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getScrollRange() {
|
||||||
|
int scrollRange = 0;
|
||||||
|
if (getChildCount() > 0) {
|
||||||
|
int contentHeight = getContentHeight();
|
||||||
|
scrollRange = Math.max(0,
|
||||||
|
contentHeight - mMaxLayoutHeight + mCollapsedSize);
|
||||||
|
}
|
||||||
|
return scrollRange;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getContentHeight() {
|
||||||
|
return mContentHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateContentHeight() {
|
||||||
|
int height = 0;
|
||||||
|
for (int i = 0; i < getChildCount(); i++) {
|
||||||
|
View child = getChildAt(i);
|
||||||
|
height += child.getHeight();
|
||||||
|
if (i < getChildCount()-1) {
|
||||||
|
height += mPaddingBetweenElements;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mContentHeight = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fling the scroll view
|
||||||
|
*
|
||||||
|
* @param velocityY The initial velocity in the Y direction. Positive
|
||||||
|
* numbers mean that the finger/cursor is moving down the screen,
|
||||||
|
* which means we want to scroll towards the top.
|
||||||
|
*/
|
||||||
|
private void fling(int velocityY) {
|
||||||
|
if (getChildCount() > 0) {
|
||||||
|
int height = (int) getLayoutHeight();
|
||||||
|
int bottom = getContentHeight();
|
||||||
|
|
||||||
|
mScroller.fling(mScrollX, mOwnScrollY, 0, velocityY, 0, 0, 0,
|
||||||
|
Math.max(0, bottom - height), 0, height/2);
|
||||||
|
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void endDrag() {
|
||||||
|
setIsBeingDragged(false);
|
||||||
|
|
||||||
|
recycleVelocityTracker();
|
||||||
|
|
||||||
|
// TODO: Overscroll
|
||||||
|
// if (mEdgeGlowTop != null) {
|
||||||
|
// mEdgeGlowTop.onRelease();
|
||||||
|
// mEdgeGlowBottom.onRelease();
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onInterceptTouchEvent(MotionEvent ev) {
|
||||||
|
boolean scrollWantsIt = false;
|
||||||
|
if (mAllowScrolling) {
|
||||||
|
scrollWantsIt = onInterceptTouchEventScroll(ev);
|
||||||
|
}
|
||||||
|
boolean swipeWantsIt = false;
|
||||||
|
if (!mIsBeingDragged) {
|
||||||
|
swipeWantsIt = mSwipeHelper.onInterceptTouchEvent(ev);
|
||||||
|
}
|
||||||
|
return swipeWantsIt || scrollWantsIt ||
|
||||||
|
super.onInterceptTouchEvent(ev);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean onInterceptTouchEventScroll(MotionEvent ev) {
|
||||||
|
/*
|
||||||
|
* This method JUST determines whether we want to intercept the motion.
|
||||||
|
* If we return true, onMotionEvent will be called and we do the actual
|
||||||
|
* scrolling there.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Shortcut the most recurring case: the user is in the dragging
|
||||||
|
* state and he is moving his finger. We want to intercept this
|
||||||
|
* motion.
|
||||||
|
*/
|
||||||
|
final int action = ev.getAction();
|
||||||
|
if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Don't try to intercept touch if we can't scroll anyway.
|
||||||
|
*/
|
||||||
|
if (mOwnScrollY == 0 && getScrollRange() == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (action & MotionEvent.ACTION_MASK) {
|
||||||
|
case MotionEvent.ACTION_MOVE: {
|
||||||
|
/*
|
||||||
|
* mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
|
||||||
|
* whether the user has moved far enough from his original down touch.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Locally do absolute value. mLastMotionY is set to the y value
|
||||||
|
* of the down event.
|
||||||
|
*/
|
||||||
|
final int activePointerId = mActivePointerId;
|
||||||
|
if (activePointerId == INVALID_POINTER) {
|
||||||
|
// If we don't have a valid id, the touch down wasn't on content.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int pointerIndex = ev.findPointerIndex(activePointerId);
|
||||||
|
if (pointerIndex == -1) {
|
||||||
|
Log.e(TAG, "Invalid pointerId=" + activePointerId
|
||||||
|
+ " in onInterceptTouchEvent");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int y = (int) ev.getY(pointerIndex);
|
||||||
|
final int yDiff = Math.abs(y - mLastMotionY);
|
||||||
|
if (yDiff > mTouchSlop) {
|
||||||
|
setIsBeingDragged(true);
|
||||||
|
mLastMotionY = y;
|
||||||
|
initVelocityTrackerIfNotExists();
|
||||||
|
mVelocityTracker.addMovement(ev);
|
||||||
|
final ViewParent parent = getParent();
|
||||||
|
if (parent != null) {
|
||||||
|
parent.requestDisallowInterceptTouchEvent(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_DOWN: {
|
||||||
|
final int y = (int) ev.getY();
|
||||||
|
if (getChildAtPosition(ev.getX(), y) == null) {
|
||||||
|
setIsBeingDragged(false);
|
||||||
|
recycleVelocityTracker();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Remember location of down touch.
|
||||||
|
* ACTION_DOWN always refers to pointer index 0.
|
||||||
|
*/
|
||||||
|
mLastMotionY = y;
|
||||||
|
mActivePointerId = ev.getPointerId(0);
|
||||||
|
|
||||||
|
initOrResetVelocityTracker();
|
||||||
|
mVelocityTracker.addMovement(ev);
|
||||||
|
/*
|
||||||
|
* If being flinged and user touches the screen, initiate drag;
|
||||||
|
* otherwise don't. mScroller.isFinished should be false when
|
||||||
|
* being flinged.
|
||||||
|
*/
|
||||||
|
boolean isBeingDragged = !mScroller.isFinished();
|
||||||
|
setIsBeingDragged(isBeingDragged);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case MotionEvent.ACTION_CANCEL:
|
||||||
|
case MotionEvent.ACTION_UP:
|
||||||
|
/* Release the drag */
|
||||||
|
setIsBeingDragged(false);
|
||||||
|
mActivePointerId = INVALID_POINTER;
|
||||||
|
recycleVelocityTracker();
|
||||||
|
if (mScroller.springBack(mScrollX, mOwnScrollY, 0, 0, 0, getScrollRange())) {
|
||||||
|
postInvalidateOnAnimation();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case MotionEvent.ACTION_POINTER_UP:
|
||||||
|
onSecondaryPointerUp(ev);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The only time we want to intercept motion events is if we are in the
|
||||||
|
* drag mode.
|
||||||
|
*/
|
||||||
|
return mIsBeingDragged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setIsBeingDragged(boolean isDragged) {
|
||||||
|
mIsBeingDragged = isDragged;
|
||||||
|
if (isDragged) {
|
||||||
|
mSwipeHelper.removeLongPressCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWindowFocusChanged(boolean hasWindowFocus) {
|
||||||
|
super.onWindowFocusChanged(hasWindowFocus);
|
||||||
|
if (!hasWindowFocus) {
|
||||||
|
mSwipeHelper.removeLongPressCallback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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.systemui.statusbar.stack;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Functor which interpolates the stack distance linearly based on base values.
|
||||||
|
* The base values are based on an interpolation between a linear function and a
|
||||||
|
* quadratic function
|
||||||
|
*/
|
||||||
|
public class PiecewiseLinearIndentationFunctor extends StackIndentationFunctor {
|
||||||
|
|
||||||
|
private final ArrayList<Float> mBaseValues;
|
||||||
|
private final float mLinearPart;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param maxItemsInStack The maximum number of items which should be visible at the same time,
|
||||||
|
* i.e the function returns totalTransitionDistance for the element with
|
||||||
|
* index maxItemsInStack
|
||||||
|
* @param peekSize The visual appearance of this is how far the cards in the stack peek
|
||||||
|
* out below the top card and it is measured in real pixels.
|
||||||
|
* Note that the visual appearance does not necessarily always correspond to
|
||||||
|
* the actual visual distance below the top card but is a maximum,
|
||||||
|
* achieved when the next card just starts transitioning into the stack and
|
||||||
|
* the stack is full.
|
||||||
|
* If totalTransitionDistance is equal to this, we directly start at the peek,
|
||||||
|
* otherwise the first element transitions between 0 and
|
||||||
|
* totalTransitionDistance - peekSize.
|
||||||
|
* Visualization:
|
||||||
|
* --------------------------------------------------- ---
|
||||||
|
* | | |
|
||||||
|
* | FIRST ITEM | | <- totalTransitionDistance
|
||||||
|
* | | |
|
||||||
|
* |---------------------------------------------------| | ---
|
||||||
|
* |__________________SECOND ITEM______________________| | | <- peekSize
|
||||||
|
* |===================================================| _|_ _|_
|
||||||
|
*
|
||||||
|
* @param totalTransitionDistance The total transition distance an element has to go through
|
||||||
|
* @param linearPart The interpolation factor between the linear and the quadratic amount taken.
|
||||||
|
* This factor must be somewhere in [0 , 1]
|
||||||
|
*/
|
||||||
|
PiecewiseLinearIndentationFunctor(int maxItemsInStack,
|
||||||
|
int peekSize,
|
||||||
|
int totalTransitionDistance,
|
||||||
|
float linearPart) {
|
||||||
|
super(maxItemsInStack, peekSize, totalTransitionDistance);
|
||||||
|
mBaseValues = new ArrayList<Float>(maxItemsInStack+1);
|
||||||
|
initBaseValues();
|
||||||
|
mLinearPart = linearPart;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initBaseValues() {
|
||||||
|
int sumOfSquares = getSumOfSquares(mMaxItemsInStack-1);
|
||||||
|
int totalWeight = 0;
|
||||||
|
mBaseValues.add(0.0f);
|
||||||
|
for (int i = 0; i < mMaxItemsInStack - 1; i++) {
|
||||||
|
totalWeight += (mMaxItemsInStack - i - 1) * (mMaxItemsInStack - i - 1);
|
||||||
|
mBaseValues.add((float) totalWeight / sumOfSquares);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the sum of squares up to and including n, i.e sum(i * i, 1, n)
|
||||||
|
*
|
||||||
|
* @param n the maximum square to include
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private int getSumOfSquares(int n) {
|
||||||
|
return n * (n + 1) * (2 * n + 1) / 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public float getValue(float itemsBefore) {
|
||||||
|
if (mStackStartsAtPeek) {
|
||||||
|
// We directly start at the stack, so no initial interpolation.
|
||||||
|
itemsBefore++;
|
||||||
|
}
|
||||||
|
if (itemsBefore < 0) {
|
||||||
|
return 0;
|
||||||
|
} else if (itemsBefore >= mMaxItemsInStack) {
|
||||||
|
return mTotalTransitionDistance;
|
||||||
|
}
|
||||||
|
int below = (int) itemsBefore;
|
||||||
|
float partialIn = itemsBefore - below;
|
||||||
|
|
||||||
|
if (below == 0) {
|
||||||
|
return mDistanceToPeekStart * partialIn;
|
||||||
|
} else {
|
||||||
|
float result = mDistanceToPeekStart;
|
||||||
|
float progress = mBaseValues.get(below - 1) * (1 - partialIn)
|
||||||
|
+ mBaseValues.get(below) * partialIn;
|
||||||
|
result += (progress * (1 - mLinearPart)
|
||||||
|
+ (itemsBefore - 1) / (mMaxItemsInStack - 1) * mLinearPart) * mPeekSize;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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.systemui.statusbar.stack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A functor which can be queried for offset given the number of items before it.
|
||||||
|
*/
|
||||||
|
public abstract class StackIndentationFunctor {
|
||||||
|
|
||||||
|
protected final int mTotalTransitionDistance;
|
||||||
|
protected final int mDistanceToPeekStart;
|
||||||
|
protected int mMaxItemsInStack;
|
||||||
|
protected int mPeekSize;
|
||||||
|
protected boolean mStackStartsAtPeek;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param maxItemsInStack The maximum number of items which should be visible at the same time,
|
||||||
|
* i.e the function returns totalTransitionDistance for the element with
|
||||||
|
* index maxItemsInStack
|
||||||
|
* @param peekSize The visual appearance of this is how far the cards in the stack peek
|
||||||
|
* out below the top card and it is measured in real pixels.
|
||||||
|
* Note that the visual appearance does not necessarily always correspond to
|
||||||
|
* the actual visual distance below the top card but is a maximum,
|
||||||
|
* achieved when the next card just starts transitioning into the stack and
|
||||||
|
* the stack is full.
|
||||||
|
* If totalTransitionDistance is equal to this, we directly start at the peek,
|
||||||
|
* otherwise the first element transitions between 0 and
|
||||||
|
* totalTransitionDistance - peekSize.
|
||||||
|
* Visualization:
|
||||||
|
* --------------------------------------------------- ---
|
||||||
|
* | | |
|
||||||
|
* | FIRST ITEM | | <- totalTransitionDistance
|
||||||
|
* | | |
|
||||||
|
* |---------------------------------------------------| | ---
|
||||||
|
* |__________________SECOND ITEM______________________| | | <- peekSize
|
||||||
|
* |===================================================| _|_ _|_
|
||||||
|
*
|
||||||
|
* @param totalTransitionDistance The total transition distance an element has to go through
|
||||||
|
*/
|
||||||
|
StackIndentationFunctor(int maxItemsInStack, int peekSize, int totalTransitionDistance) {
|
||||||
|
mTotalTransitionDistance = totalTransitionDistance;
|
||||||
|
mDistanceToPeekStart = mTotalTransitionDistance - peekSize;
|
||||||
|
mStackStartsAtPeek = mDistanceToPeekStart == 0;
|
||||||
|
mMaxItemsInStack = maxItemsInStack;
|
||||||
|
mPeekSize = peekSize;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPeekSize(int mPeekSize) {
|
||||||
|
this.mPeekSize = mPeekSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the offset of this Functor given a the quantity of items before it
|
||||||
|
*
|
||||||
|
* @param itemsBefore how many items are already in the stack before this element
|
||||||
|
* @return the offset
|
||||||
|
*/
|
||||||
|
public abstract float getValue(float itemsBefore);
|
||||||
|
}
|
||||||
@@ -0,0 +1,399 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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.systemui.statusbar.stack;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import com.android.systemui.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Algorithm of the {@link com.android.systemui.statusbar.stack
|
||||||
|
* .NotificationStackScrollLayout} which can be queried for {@link com.android.systemui.statusbar
|
||||||
|
* .stack.StackScrollState}
|
||||||
|
*/
|
||||||
|
public class StackScrollAlgorithm {
|
||||||
|
|
||||||
|
private static final int MAX_ITEMS_IN_BOTTOM_STACK = 3;
|
||||||
|
private static final int MAX_ITEMS_IN_TOP_STACK = 3;
|
||||||
|
|
||||||
|
private int mPaddingBetweenElements;
|
||||||
|
private int mCollapsedSize;
|
||||||
|
private int mTopStackPeekSize;
|
||||||
|
private int mBottomStackPeekSize;
|
||||||
|
private int mZDistanceBetweenElements;
|
||||||
|
private int mZBasicHeight;
|
||||||
|
|
||||||
|
private StackIndentationFunctor mTopStackIndentationFunctor;
|
||||||
|
private StackIndentationFunctor mBottomStackIndentationFunctor;
|
||||||
|
|
||||||
|
private float mLayoutHeight;
|
||||||
|
private StackScrollAlgorithmState mTempAlgorithmState = new StackScrollAlgorithmState();
|
||||||
|
|
||||||
|
public StackScrollAlgorithm(Context context) {
|
||||||
|
initConstants(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initConstants(Context context) {
|
||||||
|
|
||||||
|
// currently the padding is in the elements themself
|
||||||
|
mPaddingBetweenElements = 0;
|
||||||
|
mCollapsedSize = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.notification_row_min_height);
|
||||||
|
mTopStackPeekSize = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.top_stack_peek_amount);
|
||||||
|
mBottomStackPeekSize = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.bottom_stack_peek_amount);
|
||||||
|
mZDistanceBetweenElements = context.getResources()
|
||||||
|
.getDimensionPixelSize(R.dimen.z_distance_between_notifications);
|
||||||
|
mZBasicHeight = (MAX_ITEMS_IN_BOTTOM_STACK + 1) * mZDistanceBetweenElements;
|
||||||
|
|
||||||
|
mTopStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
|
||||||
|
MAX_ITEMS_IN_TOP_STACK,
|
||||||
|
mTopStackPeekSize,
|
||||||
|
mCollapsedSize + mPaddingBetweenElements,
|
||||||
|
0.5f);
|
||||||
|
mBottomStackIndentationFunctor = new PiecewiseLinearIndentationFunctor(
|
||||||
|
MAX_ITEMS_IN_BOTTOM_STACK,
|
||||||
|
mBottomStackPeekSize,
|
||||||
|
mBottomStackPeekSize,
|
||||||
|
0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void getStackScrollState(StackScrollState resultState) {
|
||||||
|
// The state of the local variables are saved in an algorithmState to easily subdivide it
|
||||||
|
// into multiple phases.
|
||||||
|
StackScrollAlgorithmState algorithmState = mTempAlgorithmState;
|
||||||
|
|
||||||
|
// First we reset the view states to their default values.
|
||||||
|
resultState.resetViewStates();
|
||||||
|
|
||||||
|
// The first element is always in there so it's initialized with 1.0f.
|
||||||
|
algorithmState.itemsInTopStack = 1.0f;
|
||||||
|
algorithmState.partialInTop = 0.0f;
|
||||||
|
algorithmState.lastTopStackIndex = 0;
|
||||||
|
algorithmState.scrollY = resultState.getScrollY();
|
||||||
|
algorithmState.itemsInBottomStack = 0.0f;
|
||||||
|
|
||||||
|
// Phase 1:
|
||||||
|
findNumberOfItemsInTopStackAndUpdateState(resultState, algorithmState);
|
||||||
|
|
||||||
|
// Phase 2:
|
||||||
|
updatePositionsForState(resultState, algorithmState);
|
||||||
|
|
||||||
|
// Phase 3:
|
||||||
|
updateZValuesForState(resultState, algorithmState);
|
||||||
|
|
||||||
|
// Write the algorithm state to the result.
|
||||||
|
resultState.setScrollY(algorithmState.scrollY);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the positions for the views. This is the main part of the algorithm.
|
||||||
|
*
|
||||||
|
* @param resultState The result state to update if a change to the properties of a child occurs
|
||||||
|
* @param algorithmState The state in which the current pass of the algorithm is currently in
|
||||||
|
* and which will be updated
|
||||||
|
*/
|
||||||
|
private void updatePositionsForState(StackScrollState resultState,
|
||||||
|
StackScrollAlgorithmState algorithmState) {
|
||||||
|
float stackHeight = getLayoutHeight();
|
||||||
|
|
||||||
|
// The position where the bottom stack starts.
|
||||||
|
float transitioningPositionStart = stackHeight - mCollapsedSize - mBottomStackPeekSize;
|
||||||
|
|
||||||
|
// The y coordinate of the current child.
|
||||||
|
float currentYPosition = 0.0f;
|
||||||
|
|
||||||
|
// How far in is the element currently transitioning into the bottom stack.
|
||||||
|
float yPositionInScrollView = 0.0f;
|
||||||
|
|
||||||
|
ViewGroup hostView = resultState.getHostView();
|
||||||
|
int childCount = hostView.getChildCount();
|
||||||
|
int numberOfElementsCompletelyIn = (int) algorithmState.itemsInTopStack;
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
View child = hostView.getChildAt(i);
|
||||||
|
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
|
||||||
|
childViewState.yTranslation = currentYPosition;
|
||||||
|
int childHeight = child.getHeight();
|
||||||
|
// The y position after this element
|
||||||
|
float nextYPosition = currentYPosition + childHeight + mPaddingBetweenElements;
|
||||||
|
float yPositionInScrollViewAfterElement = yPositionInScrollView
|
||||||
|
+ childHeight
|
||||||
|
+ mPaddingBetweenElements;
|
||||||
|
float scrollOffset = yPositionInScrollViewAfterElement - algorithmState.scrollY;
|
||||||
|
if (i < algorithmState.lastTopStackIndex) {
|
||||||
|
// Case 1:
|
||||||
|
// We are in the top Stack
|
||||||
|
nextYPosition = updateStateForTopStackChild(algorithmState,
|
||||||
|
numberOfElementsCompletelyIn,
|
||||||
|
i, childViewState);
|
||||||
|
|
||||||
|
} else if (i == algorithmState.lastTopStackIndex) {
|
||||||
|
// Case 2:
|
||||||
|
// First element of regular scrollview comes next, so the position is just the
|
||||||
|
// scrolling position
|
||||||
|
nextYPosition = scrollOffset;
|
||||||
|
} else if (nextYPosition >= transitioningPositionStart) {
|
||||||
|
if (currentYPosition >= transitioningPositionStart) {
|
||||||
|
// Case 3:
|
||||||
|
// According to the regular scroll view we are fully translated out of the
|
||||||
|
// bottom of the screen so we are fully in the bottom stack
|
||||||
|
nextYPosition = updateStateForChildFullyInBottomStack(algorithmState,
|
||||||
|
transitioningPositionStart, childViewState, childHeight);
|
||||||
|
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Case 4:
|
||||||
|
// According to the regular scroll view we are currently translating out of /
|
||||||
|
// into the bottom of the screen
|
||||||
|
nextYPosition = updateStateForChildTransitioningInBottom(
|
||||||
|
algorithmState, stackHeight, transitioningPositionStart,
|
||||||
|
currentYPosition, childViewState,
|
||||||
|
childHeight, nextYPosition);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentYPosition = nextYPosition;
|
||||||
|
yPositionInScrollView = yPositionInScrollViewAfterElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float updateStateForChildTransitioningInBottom(StackScrollAlgorithmState algorithmState,
|
||||||
|
float stackHeight, float transitioningPositionStart, float currentYPosition,
|
||||||
|
StackScrollState.ViewState childViewState, int childHeight, float nextYPosition) {
|
||||||
|
float newSize = transitioningPositionStart + mCollapsedSize - currentYPosition;
|
||||||
|
newSize = Math.min(childHeight, newSize);
|
||||||
|
// Transitioning element on top of bottom stack:
|
||||||
|
algorithmState.partialInBottom = 1.0f - (
|
||||||
|
(stackHeight - mBottomStackPeekSize - nextYPosition) / mCollapsedSize);
|
||||||
|
// Our element can be expanded, so we might even have to scroll further than
|
||||||
|
// mCollapsedSize
|
||||||
|
algorithmState.partialInBottom = Math.min(1.0f, algorithmState.partialInBottom);
|
||||||
|
float offset = mBottomStackIndentationFunctor.getValue(
|
||||||
|
algorithmState.partialInBottom);
|
||||||
|
nextYPosition = transitioningPositionStart + offset;
|
||||||
|
algorithmState.itemsInBottomStack += algorithmState.partialInBottom;
|
||||||
|
// TODO: only temporarily collapse
|
||||||
|
if (childHeight != (int) newSize) {
|
||||||
|
childViewState.height = (int) newSize;
|
||||||
|
}
|
||||||
|
return nextYPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float updateStateForChildFullyInBottomStack(StackScrollAlgorithmState algorithmState,
|
||||||
|
float transitioningPositionStart, StackScrollState.ViewState childViewState,
|
||||||
|
int childHeight) {
|
||||||
|
|
||||||
|
float nextYPosition;
|
||||||
|
algorithmState.itemsInBottomStack += 1.0f;
|
||||||
|
if (algorithmState.itemsInBottomStack < MAX_ITEMS_IN_BOTTOM_STACK) {
|
||||||
|
// We are visually entering the bottom stack
|
||||||
|
nextYPosition = transitioningPositionStart
|
||||||
|
+ mBottomStackIndentationFunctor.getValue(
|
||||||
|
algorithmState.itemsInBottomStack);
|
||||||
|
} else {
|
||||||
|
// we are fully inside the stack
|
||||||
|
if (algorithmState.itemsInBottomStack > MAX_ITEMS_IN_BOTTOM_STACK + 2) {
|
||||||
|
childViewState.alpha = 0.0f;
|
||||||
|
} else if (algorithmState.itemsInBottomStack
|
||||||
|
> MAX_ITEMS_IN_BOTTOM_STACK + 1) {
|
||||||
|
childViewState.alpha = 1.0f - algorithmState.partialInBottom;
|
||||||
|
}
|
||||||
|
nextYPosition = transitioningPositionStart + mBottomStackPeekSize;
|
||||||
|
}
|
||||||
|
// TODO: only temporarily collapse
|
||||||
|
if (childHeight != mCollapsedSize) {
|
||||||
|
childViewState.height = mCollapsedSize;
|
||||||
|
}
|
||||||
|
return nextYPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float updateStateForTopStackChild(StackScrollAlgorithmState algorithmState,
|
||||||
|
int numberOfElementsCompletelyIn, int i, StackScrollState.ViewState childViewState) {
|
||||||
|
|
||||||
|
float nextYPosition = 0;
|
||||||
|
|
||||||
|
// First we calculate the index relative to the current stack window of size at most
|
||||||
|
// {@link #MAX_ITEMS_IN_TOP_STACK}
|
||||||
|
int paddedIndex = i
|
||||||
|
- Math.max(numberOfElementsCompletelyIn - MAX_ITEMS_IN_TOP_STACK, 0);
|
||||||
|
if (paddedIndex >= 0) {
|
||||||
|
// We are currently visually entering the top stack
|
||||||
|
nextYPosition = mCollapsedSize + mPaddingBetweenElements -
|
||||||
|
mTopStackIndentationFunctor.getValue(
|
||||||
|
algorithmState.itemsInTopStack - i - 1);
|
||||||
|
if (paddedIndex == 0 && i != 0) {
|
||||||
|
childViewState.alpha = 1.0f - algorithmState.partialInTop;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are hidden behind the top card and faded out, so we can hide ourselfs
|
||||||
|
if (i != 0) {
|
||||||
|
childViewState.alpha = 0.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nextYPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the number of items in the top stack and update the result state if needed.
|
||||||
|
*
|
||||||
|
* @param resultState The result state to update if a height change of an child occurs
|
||||||
|
* @param algorithmState The state in which the current pass of the algorithm is currently in
|
||||||
|
* and which will be updated
|
||||||
|
*/
|
||||||
|
private void findNumberOfItemsInTopStackAndUpdateState(StackScrollState resultState,
|
||||||
|
StackScrollAlgorithmState algorithmState) {
|
||||||
|
|
||||||
|
// The y Position if the element would be in a regular scrollView
|
||||||
|
float yPositionInScrollView = 0.0f;
|
||||||
|
ViewGroup hostView = resultState.getHostView();
|
||||||
|
int childCount = hostView.getChildCount();
|
||||||
|
|
||||||
|
// find the number of elements in the top stack.
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
View child = hostView.getChildAt(i);
|
||||||
|
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
|
||||||
|
int childHeight = child.getHeight();
|
||||||
|
float yPositionInScrollViewAfterElement = yPositionInScrollView
|
||||||
|
+ childHeight
|
||||||
|
+ mPaddingBetweenElements;
|
||||||
|
if (yPositionInScrollView < algorithmState.scrollY) {
|
||||||
|
if (yPositionInScrollViewAfterElement <= algorithmState.scrollY) {
|
||||||
|
// According to the regular scroll view we are fully off screen
|
||||||
|
algorithmState.itemsInTopStack += 1.0f;
|
||||||
|
if (childHeight != mCollapsedSize) {
|
||||||
|
childViewState.height = mCollapsedSize;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// According to the regular scroll view we are partially off screen
|
||||||
|
// If it is expanded we have to collapse it to a new size
|
||||||
|
float newSize = yPositionInScrollViewAfterElement
|
||||||
|
- mPaddingBetweenElements
|
||||||
|
- algorithmState.scrollY;
|
||||||
|
|
||||||
|
// How much did we scroll into this child
|
||||||
|
algorithmState.partialInTop = (mCollapsedSize - newSize) / (mCollapsedSize
|
||||||
|
+ mPaddingBetweenElements);
|
||||||
|
|
||||||
|
// Our element can be expanded, so this can get negative
|
||||||
|
algorithmState.partialInTop = Math.max(0.0f, algorithmState.partialInTop);
|
||||||
|
algorithmState.itemsInTopStack += algorithmState.partialInTop;
|
||||||
|
// TODO: handle overlapping sizes with end stack
|
||||||
|
newSize = Math.max(mCollapsedSize, newSize);
|
||||||
|
// TODO: only temporarily collapse
|
||||||
|
if (newSize != childHeight) {
|
||||||
|
childViewState.height = (int) newSize;
|
||||||
|
|
||||||
|
// We decrease scrollY by the same amount we made this child smaller.
|
||||||
|
// The new scroll position is therefore the start of the element
|
||||||
|
algorithmState.scrollY = (int) yPositionInScrollView;
|
||||||
|
resultState.setScrollY(algorithmState.scrollY);
|
||||||
|
}
|
||||||
|
if (childHeight > mCollapsedSize) {
|
||||||
|
// If we are just resizing this child, this element is not treated to be
|
||||||
|
// transitioning into the stack and therefore it is the last element in
|
||||||
|
// the stack.
|
||||||
|
algorithmState.lastTopStackIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
algorithmState.lastTopStackIndex = i;
|
||||||
|
|
||||||
|
// We are already past the stack so we can end the loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
yPositionInScrollView = yPositionInScrollViewAfterElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the Z positions for all children based on the number of items in both stacks and
|
||||||
|
* save it in the resultState
|
||||||
|
*
|
||||||
|
* @param resultState The result state to update the zTranslation values
|
||||||
|
* @param algorithmState The state in which the current pass of the algorithm is currently in
|
||||||
|
*/
|
||||||
|
private void updateZValuesForState(StackScrollState resultState,
|
||||||
|
StackScrollAlgorithmState algorithmState) {
|
||||||
|
ViewGroup hostView = resultState.getHostView();
|
||||||
|
int childCount = hostView.getChildCount();
|
||||||
|
for (int i = 0; i < childCount; i++) {
|
||||||
|
View child = hostView.getChildAt(i);
|
||||||
|
StackScrollState.ViewState childViewState = resultState.getViewStateForView(child);
|
||||||
|
if (i < algorithmState.itemsInTopStack) {
|
||||||
|
float stackIndex = algorithmState.itemsInTopStack - i;
|
||||||
|
stackIndex = Math.min(stackIndex, MAX_ITEMS_IN_TOP_STACK + 2);
|
||||||
|
childViewState.zTranslation = mZBasicHeight
|
||||||
|
+ stackIndex * mZDistanceBetweenElements;
|
||||||
|
} else if (i > (childCount - 1 - algorithmState.itemsInBottomStack)) {
|
||||||
|
float numItemsAbove = i - (childCount - 1 - algorithmState.itemsInBottomStack);
|
||||||
|
float translationZ = mZBasicHeight
|
||||||
|
- numItemsAbove * mZDistanceBetweenElements;
|
||||||
|
childViewState.zTranslation = translationZ;
|
||||||
|
} else {
|
||||||
|
childViewState.zTranslation = mZBasicHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public float getLayoutHeight() {
|
||||||
|
return mLayoutHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLayoutHeight(float layoutHeight) {
|
||||||
|
this.mLayoutHeight = layoutHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StackScrollAlgorithmState {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The scroll position of the algorithm
|
||||||
|
*/
|
||||||
|
public int scrollY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quantity of items which are in the top stack.
|
||||||
|
*/
|
||||||
|
public float itemsInTopStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* how far in is the element currently transitioning into the top stack
|
||||||
|
*/
|
||||||
|
public float partialInTop;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The last item index which is in the top stack.
|
||||||
|
* NOTE: In the top stack the item after the transitioning element is also in the stack!
|
||||||
|
* This is needed to ensure a smooth transition between the y position in the regular
|
||||||
|
* scrollview and the one in the stack.
|
||||||
|
*/
|
||||||
|
public int lastTopStackIndex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The quantity of items which are in the bottom stack.
|
||||||
|
*/
|
||||||
|
public float itemsInBottomStack;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* how far in is the element currently transitioning into the bottom stack
|
||||||
|
*/
|
||||||
|
public float partialInBottom;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2014 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.systemui.statusbar.stack;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A state of a {@link com.android.systemui.statusbar.stack.NotificationStackScrollLayout} which
|
||||||
|
* can be applied to a viewGroup.
|
||||||
|
*/
|
||||||
|
public class StackScrollState {
|
||||||
|
|
||||||
|
private static final String CHILD_NOT_FOUND_TAG = "StackScrollStateNoSuchChild";
|
||||||
|
|
||||||
|
private final ViewGroup mHostView;
|
||||||
|
private Map<View, ViewState> mStateMap;
|
||||||
|
private int mScrollY;
|
||||||
|
|
||||||
|
public int getScrollY() {
|
||||||
|
return mScrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setScrollY(int scrollY) {
|
||||||
|
this.mScrollY = scrollY;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StackScrollState(ViewGroup hostView) {
|
||||||
|
mHostView = hostView;
|
||||||
|
mStateMap = new HashMap<View, ViewState>(mHostView.getChildCount());
|
||||||
|
}
|
||||||
|
|
||||||
|
public ViewGroup getHostView() {
|
||||||
|
return mHostView;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetViewStates() {
|
||||||
|
int numChildren = mHostView.getChildCount();
|
||||||
|
for (int i = 0; i < numChildren; i++) {
|
||||||
|
View child = mHostView.getChildAt(i);
|
||||||
|
ViewState viewState = mStateMap.get(child);
|
||||||
|
if (viewState == null) {
|
||||||
|
viewState = new ViewState();
|
||||||
|
mStateMap.put(child, viewState);
|
||||||
|
}
|
||||||
|
// initialize with the default values of the view
|
||||||
|
viewState.height = child.getHeight();
|
||||||
|
viewState.alpha = 1.0f;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public ViewState getViewStateForView(View requestedView) {
|
||||||
|
return mStateMap.get(requestedView);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the properties saved in {@link #mStateMap} to the children of the {@link #mHostView}.
|
||||||
|
* The properties are only applied if they effectively changed.
|
||||||
|
*/
|
||||||
|
public void apply() {
|
||||||
|
int numChildren = mHostView.getChildCount();
|
||||||
|
for (int i = 0; i < numChildren; i++) {
|
||||||
|
View child = mHostView.getChildAt(i);
|
||||||
|
ViewState state = mStateMap.get(child);
|
||||||
|
if (state != null) {
|
||||||
|
float alpha = child.getAlpha();
|
||||||
|
float yTranslation = child.getTranslationY();
|
||||||
|
float zTranslation = child.getTranslationZ();
|
||||||
|
int height = child.getHeight();
|
||||||
|
float newAlpha = state.alpha;
|
||||||
|
float newYTranslation = state.yTranslation;
|
||||||
|
float newZTranslation = state.zTranslation;
|
||||||
|
int newHeight = state.height;
|
||||||
|
boolean becomesInvisible = newAlpha == 0.0f;
|
||||||
|
if (alpha != newAlpha) {
|
||||||
|
// apply layer type
|
||||||
|
boolean becomesFullyVisible = newAlpha == 1.0f;
|
||||||
|
boolean newLayerTypeIsHardware = !becomesInvisible && !becomesFullyVisible;
|
||||||
|
int layerType = child.getLayerType();
|
||||||
|
int newLayerType = newLayerTypeIsHardware
|
||||||
|
? View.LAYER_TYPE_HARDWARE
|
||||||
|
: View.LAYER_TYPE_NONE;
|
||||||
|
if (layerType != newLayerType) {
|
||||||
|
child.setLayerType(newLayerType, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply alpha
|
||||||
|
if (!becomesInvisible) {
|
||||||
|
child.setAlpha(newAlpha);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply visibility
|
||||||
|
int oldVisibility = child.getVisibility();
|
||||||
|
int newVisibility = becomesInvisible ? View.INVISIBLE : View.VISIBLE;
|
||||||
|
if (newVisibility != oldVisibility) {
|
||||||
|
child.setVisibility(newVisibility);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply yTranslation
|
||||||
|
if (yTranslation != newYTranslation) {
|
||||||
|
child.setTranslationY(newYTranslation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply zTranslation
|
||||||
|
if (zTranslation != newZTranslation) {
|
||||||
|
child.setTranslationZ(newZTranslation);
|
||||||
|
}
|
||||||
|
|
||||||
|
// apply height
|
||||||
|
if (height != newHeight) {
|
||||||
|
applyNewHeight(child, newHeight);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Log.wtf(CHILD_NOT_FOUND_TAG, "No child state was found when applying this state " +
|
||||||
|
"to the hostView");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyNewHeight(View child, int newHeight) {
|
||||||
|
ViewGroup.LayoutParams lp = child.getLayoutParams();
|
||||||
|
lp.height = newHeight;
|
||||||
|
child.setLayoutParams(lp);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class ViewState {
|
||||||
|
float alpha;
|
||||||
|
float yTranslation;
|
||||||
|
float zTranslation;
|
||||||
|
int height;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user