SystemUI的Plugin - 安卓R

Plugin介绍

Plugin是一个在SystemUI中的接口,通过其代码注释可以了解其用途(frameworks/base/packages/SystemUI/plugin_core/src/com/android/systemui/plugins/Plugin.java):

/**
 * Plugins are separate APKs that
 * are expected to implement interfaces provided by SystemUI.  Their
 * code is dynamically loaded into the SysUI process which can allow
 * for multiple prototypes to be created and run on a single android
 * build.
 *
 * PluginLifecycle:
 * <pre class="prettyprint">
 *
 * plugin.onCreate(Context sysuiContext, Context pluginContext);
 * --- This is always called before any other calls
 *
 * pluginListener.onPluginConnected(Plugin p);
 * --- This lets the plugin hook know that a plugin is now connected.
 *
 * ** Any other calls back and forth between sysui/plugin **
 *
 * pluginListener.onPluginDisconnected(Plugin p);
 * --- Lets the plugin hook know that it should stop interacting with
 *     this plugin and drop all references to it.
 *
 * plugin.onDestroy();
 * --- Finally the plugin can perform any cleanup to ensure that its not
 *     leaking into the SysUI process.
 *
 * Any time a plugin APK is updated the plugin is destroyed and recreated
 * to load the new code/resources.
 *
 * </pre>
 *
 * Creating plugin hooks:
 *
 * To create a plugin hook, first create an interface in
 * frameworks/base/packages/SystemUI/plugin that extends Plugin.
 * Include in it any hooks you want to be able to call into from
 * sysui and create callback interfaces for anything you need to
 * pass through into the plugin.
 *
 * Then to attach to any plugins simply add a plugin listener and
 * onPluginConnected will get called whenever new plugins are installed,
 * updated, or enabled.  Like this example from SystemUIApplication:
 *
 * <pre class="prettyprint">
 * {@literal
 * PluginManager.getInstance(this).addPluginListener(OverlayPlugin.COMPONENT,
 *        new PluginListener<OverlayPlugin>() {
 *        @Override
 *        public void onPluginConnected(OverlayPlugin plugin) {
 *            StatusBar phoneStatusBar = getComponent(StatusBar.class);
 *            if (phoneStatusBar != null) {
 *                plugin.setup(phoneStatusBar.getStatusBarWindow(),
 *                phoneStatusBar.getNavigationBarView());
 *            }
 *        }
 * }, OverlayPlugin.VERSION, true /* Allow multiple plugins *\/);
 * }
 * </pre>
 * Note the VERSION included here.  Any time incompatible changes in the
 * interface are made, this version should be changed to ensure old plugins
 * aren't accidentally loaded.  Since the plugin library is provided by
 * SystemUI, default implementations can be added for new methods to avoid
 * version changes when possible.
 *
 * Implementing a Plugin:
 *
 * See the ExamplePlugin for an example Android.mk on how to compile
 * a plugin.  Note that SystemUILib is not static for plugins, its classes
 * are provided by SystemUI.
 *
 * Plugin security is based around a signature permission, so plugins must
 * hold the following permission in their manifest.
 *
 * <pre class="prettyprint">
 * {@literal
 * <uses-permission android:name="com.android.systemui.permission.PLUGIN" />
 * }
 * </pre>
 *
 * A plugin is found through a querying for services, so to let SysUI know
 * about it, create a service with a name that points at your implementation
 * of the plugin interface with the action accompanying it:
 *
 * <pre class="prettyprint">
 * {@literal
 * <service android:name=".TestOverlayPlugin">
 *    <intent-filter>
 *        <action android:name="com.android.systemui.action.PLUGIN_COMPONENT" />
 *    </intent-filter>
 * </service>
 * }
 * </pre>
 */
public interface Plugin {
   

    /**
     * @deprecated
     * @see Requires
     */
    default int getVersion() {
   
        // Default of -1 indicates the plugin supports the new Requires model.
        return -1;
    }

    default void onCreate(Context sysuiContext, Context pluginContext) {
   
    }

    default void onDestroy() {
   
    }
}

使用示例

NotificationMenuRowPlugin是一个继承Plugin的接口(frameworks/base/packages/SystemUI/plugin/src/com/android/systemui/plugins/statusbar/NotificationMenuRowPlugin.java):

@ProvidesInterface(action = NotificationMenuRowPlugin.ACTION,
        version = NotificationMenuRowPlugin.VERSION)
@DependsOn(target = OnMenuEventListener.class)
@DependsOn(target = MenuItem.class)
@DependsOn(target = NotificationSwipeActionHelper.class)
@DependsOn(target = SnoozeOption.class)
public interface NotificationMenuRowPlugin extends Plugin {
   

    public static final String ACTION = "com.android.systemui.action.PLUGIN_NOTIFICATION_MENU_ROW";
    public static final int VERSION = 5;

    @ProvidesInterface(version = OnMenuEventListener.VERSION)
    public interface OnMenuEventListener {
   
        public static final int VERSION = 1;

        public void onMenuClicked(View row, int x, int y, MenuItem menu);

        public void onMenuReset(View row);

        public void onMenuShown(View row);
    }

    @ProvidesInterface(version = MenuItem.VERSION)
    public interface MenuItem {
   
        public static final int VERSION = 1;

        public View getMenuView();

        public View getGutsView();

        public String getContentDescription();
    }

    /**
     * @return a list of items to populate the menu 'behind' a notification.
     */
    public ArrayList<MenuItem> getMenuItems(Context context);

    /**
     * @return the {@link MenuItem} to display when a notification is long pressed.
     */
    public MenuItem getLongpressMenuItem(Context context);

    /**
     * @return the {@link MenuItem} to display when app ops icons are pressed.
     */
    public MenuItem getAppOpsMenuItem(Context context);

    /**
     * @return the {@link MenuItem} to display when snooze item is pressed.
     */
    public MenuItem getSnoozeMenuItem(Context context);

    public void setMenuItems(ArrayList<MenuItem> items);

    /**
     * If this returns {@code true}, then the menu row will bind and fade in the notification guts
     * view for the menu item it holds.
     *
     * @see #menuItemToExposeOnSnap()
     * @return whether or not to immediately expose the notification guts
     */
    default boolean shouldShowGutsOnSnapOpen() {
   
        return false;
    }

    /**
     * When #shouldShowGutsOnExpose is true, this method must return the menu item to expose on
     * #onSnapOpen. Otherwise we will fall back to the default behavior of fading in the menu row
     *
     * @return the {@link MenuItem} containing the NotificationGuts that should be exposed
     */
    @Nullable
    default MenuItem menuItemToExposeOnSnap() {
   
        return null;
    }

    /**
     * Get the origin for the circular reveal animation when expanding the notification guts. Only
     * used when #shouldShowGutsOnSnapOpen is true
     * @return the x,y coordinates for the start of the animation
     */
    @Nullable
    default Point getRevealAnimationOrigin() {
   
        return new Point(0, 0);
    }

    public void setMenuClickListener(OnMenuEventListener listener);

    public void setAppName(String appName);

    public void createMenu(ViewGroup parent, StatusBarNotification sbn);

    public void resetMenu();

    public View getMenuView();

    /**
     * Get the target position that a notification row should be snapped open to in order to reveal
     * the menu. This is generally determined by the number of icons in the notification menu and the
     * size of each icon. This method accounts for whether the menu appears on the left or ride side
     * of the parent notification row.
     *

     * @return an int representing the x-offset in pixels that the notification should snap open to.
     * Positive values imply that the notification should be offset to the right to reveal the menu,
     * and negative alues imply that the notification should be offset to the right.
     */
    public int getMenuSnapTarget();

    /**
     * Determines whether or not the menu should be shown in response to user input.
     * @return true if the menu should be shown, false otherwise.
     */
    public boolean shouldShowMenu();

    /**
     * Determines whether the menu is currently visible.
     * @return true if the menu is visible, false otherwise.
     */
    public boolean isMenuVisible();

    /**
     * Determines whether a given movement is towards or away from the current location of the menu.
     * @param movement
     * @return true if the movement is towards the menu, false otherwise.
     */
    public boolean isTowardsMenu(float movement);

    /**
     * Determines whether the menu should snap closed instead of dismissing the
     * parent notification, as a function of its current state.
     *
     * @return true if the menu should snap closed, false otherwise.
     */
    public boolean shouldSnapBack();

    /**
     * Determines whether the menu was previously snapped open to the same side that it is currently
     * being shown on.
     * @return true if the menu is snapped open to the same side on which it currently appears,
     * false otherwise.
     */
    public boolean isSnappedAndOnSameSide();

    /**
     * Determines whether the notification the menu is attached to is able to be dismissed.
     * @return true if the menu's parent notification is dismissable, false otherwise.
     */
    public boolean canBeDismissed();

    /**
     * Informs the menu whether dismiss gestures are left-to-right or right-to-left.
     */
    default void setDismissRtl(boolean dismissRtl) {
   
    }

    /**
     * Determines whether the menu should remain open given its current state, or snap closed.
     * @return true if the menu should remain open, false otherwise.
     */
    public boolean isWithinSnapMenuThreshold();

    /**
     * Determines whether the menu has been swiped far enough to snap open.
     * @return true if the menu has been swiped far enough to open, false otherwise.
     */
    public boolean isSwipedEnoughToShowMenu();

    public default boolean onInterceptTouchEvent(View view, MotionEvent ev) {
   
        return false;
    }

    public default boolean shouldUseDefaultMenuItems() {
   
        return false;
    }

    /**
     * Callback used to signal the menu that its parent's translation has changed.
     * @param translation The new x-translation of the menu as a position (not an offset).
     */
    public void onParentTranslationUpdate(float translation);

    /**
     * Callback used to signal the menu that its parent's height has changed.
     */
    public void onParentHeightUpdate();

    /**
     * Callback used to signal the menu that its parent notification has been updated.
     * @param sbn
     */
    public void onNotificationUpdated(StatusBarNotification sbn);

    /**
     * Callback used to signal the menu that a user is moving the parent notification.
     * @param delta The change in the parent notification's position.
     */
    public void onTouchMove(float delta);

    /**
     * Callback used to signal the menu that a user has begun touching its parent notification.
     */
    public void onTouchStart();

    /**
     * Callback used to signal the menu that a user has finished touching its parent notification.
     */
    public void onTouchEnd();

    /**
     * Callback used to signal the menu that it has been snapped closed.
     */
    public void onSnapClosed();

    /**
     * Callback used to signal the menu that it has been snapped open.
     */
    public void onSnapOpen();

    /**
     * Callback used to signal the menu that its parent notification has been dismissed.
     */
    public void onDismiss();

    public default void onConfigurationChanged() {
    }

}

NotificationMenuRow实现了NotificationMenuRowPlugin接口(frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/NotificationMenuRow.java):

public class NotificationMenuRow implements NotificationMenuRowPlugin, View.OnClickListener,
        ExpandableNotificationRow.LayoutListener {
   

    private static final boolean DEBUG = false;
    private static final String TAG = "swipe";

    // Notification must be swiped at least this fraction of a single menu item to show menu
    private static final float SWIPED_FAR_ENOUGH_MENU_FRACTION = 0.25f;
    private static final float SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION = 0.15f;

    // When the menu is displayed, the notification must be swiped within this fraction of a single
    // menu item to snap back to menu (else it will cover the menu or it'll be dismissed)
    private static final float SWIPED_BACK_ENOUGH_TO_COVER_FRACTION = 0.2f;

    private static final int ICON_ALPHA_ANIM_DURATION = 200;
    private static final long SHOW_MENU_DELAY = 60;

    private ExpandableNotificationRow mParent;

    private Context mContext;
    private FrameLayout mMenuContainer;
    private NotificationMenuItem mInfoItem;
    private MenuItem mAppOpsItem;
    private MenuItem mSnoozeItem;
    private ArrayList<MenuItem> mLeftMenuItems;
    private ArrayList<MenuItem> mRightMenuItems;
    private final Map<View, MenuItem> mMenuItemsByView = new ArrayMap<>();
    private OnMenuEventListener mMenuListener;
    private boolean mDismissRtl;
    private boolean mIsForeground;

    private ValueAnimator mFadeAnimator;
    private boolean mAnimating;
    private boolean mMenuFadedIn;

    private boolean mOnLeft;
    private boolean mIconsPlaced;

    private boolean mDismissing;
    private boolean mSnapping;
    private float mTranslation;

    private int[] mIconLocation = new int[2];
    private int[] mParentLocation = new int[2];

    private int mHorizSpaceForIcon = -1;
    private int mVertSpaceForIcons = -1;
    private int mIconPadding = -1;
    private int mSidePadding;

    private float mAlpha = 0f;

    private CheckForDrag mCheckForDrag;
    private Handler mHandler;

    private boolean mMenuSnapped;
    private boolean mMenuSnappedOnLeft;
    private boolean mShouldShowMenu;

    private boolean mIsUserTouching;

    private final PeopleNotificationIdentifier mPeopleNotificationIdentifier;

    public NotificationMenuRow(Context context,
            PeopleNotificationIdentifier peopleNotificationIdentifier) {
   
        mContext = context;
        mShouldShowMenu = context.getResources().getBoolean(R.bool.config_showNotificationGear);
        mHandler = new Handler(Looper.getMainLooper());
        mLeftMenuItems = new ArrayList<>();
        mRightMenuItems = new ArrayList<>();
        mPeopleNotificationIdentifier = peopleNotificationIdentifier;
    }

    @Override
    public ArrayList<MenuItem> getMenuItems(Context context) {
   
        return mOnLeft ? mLeftMenuItems : mRightMenuItems;
    }

    @Override
    public MenuItem getLongpressMenuItem(Context context) {
   
        return mInfoItem;
    }

    @Override
    public MenuItem getAppOpsMenuItem(Context context) {
   
        return mAppOpsItem;
    }

    @Override
    public MenuItem getSnoozeMenuItem(Context context) {
   
        return mSnoozeItem;
    }

    @VisibleForTesting
    protected ExpandableNotificationRow getParent() {
   
        return mParent;
    }

    @VisibleForTesting
    protected boolean isMenuOnLeft() {
   
        return mOnLeft;
    }

    @VisibleForTesting
    protected boolean isMenuSnappedOnLeft() {
   
        return mMenuSnappedOnLeft;
    }

    @VisibleForTesting
    protected boolean isMenuSnapped() {
   
        return mMenuSnapped;
    }

    @VisibleForTesting
    protected boolean isDismissing() {
   
        return mDismissing;
    }

    @VisibleForTesting
    protected boolean isSnapping() {
   
        return mSnapping;
    }

    @Override
    public void setMenuClickListener(OnMenuEventListener listener) {
   
        mMenuListener = listener;
    }

    @Override
    public void createMenu(ViewGroup parent, StatusBarNotification sbn) {
   
        mParent = (ExpandableNotificationRow) parent;
        createMenuViews(true /* resetState */,
                sbn != null && (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE)
                        != 0);
    }

    @Override
    public boolean isMenuVisible() {
   
        return mAlpha > 0;
    }

    @VisibleForTesting
    protected boolean isUserTouching() {
   
        return mIsUserTouching;
    }

    @Override
    public boolean shouldShowMenu() {
   
        return mShouldShowMenu;
    }

    @Override
    public View getMenuView() {
   
        return mMenuContainer;
    }

    @VisibleForTesting
    protected float getTranslation() {
   
        return mTranslation;
    }

    @Override
    public void resetMenu() {
   
        resetState(true);
    }

    @Override
    public void onTouchEnd() {
   
        mIsUserTouching = false;
    }

    @Override
    public void onNotificationUpdated(StatusBarNotification sbn) {
   
        if (mMenuContainer == null) {
   
            // Menu hasn't been created yet, no need to do anything.
            return;
        }
        createMenuViews(!isMenuVisible() /* resetState */,
                (sbn.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0);
    }

    @Override
    public void onConfigurationChanged() {
   
        mParent.setLayoutListener(this);
    }

    @Override
    public void onLayout() {
   
        mIconsPlaced = false; // Force icons to be re-placed
        setMenuLocation();
        mParent.removeListener();
    }

    private void createMenuViews(boolean resetState, final boolean isForeground) {
   
        mIsForeground = isForeground;

        final Resources res = mContext.getResources();
        mHorizSpaceForIcon = res.getDimensionPixelSize(R.dimen.notification_menu_icon_size);
        mVertSpaceForIcons = res.getDimensionPixelSize(R.dimen.notification_min_height);
        mLeftMenuItems.clear();
        mRightMenuItems.clear();

        boolean showSnooze = Settings.Secure.getInt(mContext.getContentResolver(),
                SHOW_NOTIFICATION_SNOOZE, 0) == 1;

        // Construct the menu items based on the notification
        if (!isForeground && showSnooze) {
   
            // Only show snooze for non-foreground notifications, and if the setting is on
            mSnoozeItem = createSnoozeItem(mContext);
        }
        mAppOpsItem = createAppOpsItem(mContext);
        NotificationEntry entry = mParent.getEntry();
        int personNotifType = mPeopleNotificationIdentifier
                .getPeopleNotificationType(entry.getSbn(), entry.getRanking());
        if (personNotifType == PeopleNotificationIdentifier.TYPE_PERSON) {
   
            mInfoItem = createPartialConversationItem(mContext);
        } else if (personNotifType >= PeopleNotificationIdentifier.TYPE_FULL_PERSON) {
   
            mInfoItem = createConversationItem(mContext);
        } else {
   
            mInfoItem = createInfoItem(mContext);
        }

        if (!isForeground && showSnooze) {
   
            mRightMenuItems.add(mSnoozeItem);
        }
        mRightMenuItems.add(mInfoItem);
        mRightMenuItems.add(mAppOpsItem);
        mLeftMenuItems.addAll(mRightMenuItems);

        populateMenuViews();
        if (resetState) {
   
            resetState(false /* notify */);
        } else {
   
            mIconsPlaced = false;
            setMenuLocation();
            if (!mIsUserTouching) {
   
                onSnapOpen();
            }
        }
    }

    private void populateMenuViews() {
   
        if (mMenuContainer != null) {
   
            mMenuContainer.removeAllViews();
            mMenuItemsByView.clear();
        } else {
   
            mMenuContainer = new FrameLayout(mContext);
        }
        List<MenuItem> menuItems = mOnLeft ? mLeftMenuItems : mRightMenuItems;
        for (int i = 0; i < menuItems.size(); i++) {
   
            addMenuView(menuItems.get(i), mMenuContainer);
        }
    }

    private void resetState(boolean notify) {
   
        setMenuAlpha(0f);
        mIconsPlaced = false;
        mMenuFadedIn = false;
        mAnimating = false;
        mSnapping = false;
        mDismissing = false;
        mMenuSnapped = false;
        setMenuLocation();
        if (mMenuListener != null && notify) {
   
            mMenuListener.onMenuReset(mParent);
        }
    }

    @Override
    public void onTouchMove(float delta) {
   
        mSnapping = false;

        if (!isTowardsMenu(delta) && isMenuLocationChange()) {
   
            // Don't consider it "snapped" if location has changed.
            mMenuSnapped = false;

            // Changed directions, make sure we check to fade in icon again.
            if (!mHandler.hasCallbacks(mCheckForDrag)) {
   
                // No check scheduled, set null to schedule a new one.
                mCheckForDrag = null;
            } else {
   
                // Check scheduled, reset alpha and update location; check will fade it in
                setMenuAlpha(0f);
                setMenuLocation();
            }
        }
        if (mShouldShowMenu
                && !NotificationStackScrollLayout.isPinnedHeadsUp(getParent())
                && !mParent.areGutsExposed()
                && !mParent.showingPulsing()
                && (mCheckForDrag == null || !mHandler.hasCallbacks(mCheckForDrag))) {
   
            // Only show the menu if we're not a heads up view and guts aren't exposed.
            mCheckForDrag = new CheckForDrag();
            mHandler.postDelayed(mCheckForDrag, SHOW_MENU_DELAY);
        }
    }

    @VisibleForTesting
    protected void beginDrag() {
   
        mSnapping = false;
        if (mFadeAnimator != null) {
   
            mFadeAnimator.cancel();
        }
        mHandler.removeCallbacks(mCheckForDrag);
        mCheckForDrag = null;
        mIsUserTouching = true;
    }

    @Override
    public void onTouchStart() {
   
       beginDrag();
    }

    @Override
    public void onSnapOpen() {
   
        mMenuSnapped = true;
        mMenuSnappedOnLeft = isMenuOnLeft();
        if (mAlpha == 0f && mParent != null) {
   
            fadeInMenu(mParent.getWidth());
        }
        if (mMenuListener != null) {
   
            mMenuListener.onMenuShown(getParent());
        }
    }

    @Override
    public void onSnapClosed() {
   
        cancelDrag();
        mMenuSnapped = false;
        mSnapping = true;
    }

    @Override
    public void onDismiss() {
   
        cancelDrag();
        mMenuSnapped = false;
        mDismissing = true;
    }

    @VisibleForTesting
    protected void cancelDrag() {
   
        if (mFadeAnimator != null) {
   
            mFadeAnimator.cancel();
        }
        mHandler.removeCallbacks(mCheckForDrag);
    }

    @VisibleForTesting
    protected float getMinimumSwipeDistance() {
   
        final float multiplier = getParent().canViewBeDismissed()
                ? SWIPED_FAR_ENOUGH_MENU_FRACTION
                : SWIPED_FAR_ENOUGH_MENU_UNCLEARABLE_FRACTION;
        return mHorizSpaceForIcon * multiplier;
    }

    @VisibleForTesting
    protected float getMaximumSwipeDistance() {
   
        return mHorizSpaceForIcon * SWIPED_BACK_ENOUGH_TO_COVER_FRACTION;
    }

    /**
     * Returns whether the gesture is towards the menu location or not.
     */
    @Override
    public boolean isTowardsMenu(float movement) {
   
        return isMenuVisible()
                && ((isMenuOnLeft() && movement <= 0)
                        || (!isMenuOnLeft() && movement >= 0));
    }

    @Override
    public void setAppName(String appName) {
   
        if (appName == null) {
   
            return;
        }
        setAppName(appName, mLeftMenuItems);
        setAppName(appName, mRightMenuItems);
    }

    private void setAppName(String appName,
            ArrayList<MenuItem> menuItems) {
   
        Resources res = mContext.getResources();
        final int count = menuItems.size();
        for (int i = 0; i < count; i++) {
   
            MenuItem item = menuItems.get(i);
            String description = String.format(
                    res.getString(R.string.notification_menu_accessibility),
                    appName, item.getContentDescription());
            View menuView = item.getMenuView();
            if (menuView != null) {
   
                menuView.setContentDescription(description);
            }
        }
    }

    @Override
    public void onParentHeightUpdate() {
   
        if (mParent == null
                || (mLeftMenuItems.isEmpty() && mRightMenuItems.isEmpty())
                || mMenuContainer == null) {
   
            return;
        }
        int parentHeight = mParent.getActualHeight();
        float translationY;
        if (parentHeight < mVertSpaceForIcons) {
   
            translationY = (parentHeight / 2) - (mHorizSpaceForIcon / 2);
        } else {
   
            translationY = (mVertSpaceForIcons - mHorizSpaceForIcon) / 2;
        }
        mMenuContainer.setTranslationY(translationY);
    }

    @Override
    public void onParentTranslationUpdate(float translation) {
   
        mTranslation = translation;
        if (mAnimating || !mMenuFadedIn) {
   
            // Don't adjust when animating, or if the menu hasn't been shown yet.
            return;
        }
        final float fadeThreshold = mParent.getWidth() * 0.3f;
        final float absTrans = Math.abs(translation);
        float desiredAlpha = 0;
        if (absTrans == 0) {
   
            desiredAlpha = 0;
        } else if (absTrans <= fadeThreshold) {
   
            desiredAlpha = 1;
        } else {
   
            desiredAlpha = 1 - ((absTrans - fadeThreshold) / (mParent.getWidth() - fadeThreshold));
        }
        setMenuAlpha(desiredAlpha);
    }

    @Override
    public void onClick(View v) {
   
        if (mMenuListener == null) {
   
            // Nothing to do
            return;
        }
        v.getLocationOnScreen(mIconLocation);
        mParent.getLocationOnScreen(mParentLocation);
        final int centerX = mHorizSpaceForIcon / 2;
        final int centerY = v.getHeight() / 2;
        final int x = mIconLocation[0] - mParentLocation[0] + centerX;
        final int y = mIconLocation[1] - mParentLocation[1] + centerY;
        if (mMenuItemsByView.containsKey(v)) {
   
            mMenuListener.onMenuClicked(mParent, x, y, mMenuItemsByView.get(v));
        }
    }

    private boolean isMenuLocationChange() {
   
        boolean onLeft = mTranslation > mIconPadding;
        boolean onRight = mTranslation < -mIconPadding;
        if ((isMenuOnLeft() && onRight) || (!isMenuOnLeft() && onLeft)) {
   
            return true;
        }
        return false;
    }

    private void setMenuLocation() {
   
        boolean showOnLeft = mTranslation > 0;
        if ((mIconsPlaced && showOnLeft == isMenuOnLeft()) || isSnapping() || mMenuContainer == null
                || !mMenuContainer.isAttachedToWindow()) {
   
            // Do nothing
            return;
        }
        boolean wasOnLeft = mOnLeft;
        mOnLeft = showOnLeft;
        if (wasOnLeft != showOnLeft) {
   
            populateMenuViews();
        }
        final int count = mMenuContainer.getChildCount();
        for (int i = 0; i < count; i++) {
   
            final View v = mMenuContainer.getChildAt(i);
            final float left = i * mHorizSpaceForIcon;
            final float right = mParent.getWidth() - (mHorizSpaceForIcon * (i + 1));
            v.setX(showOnLeft ? left : right);
        }
        mIconsPlaced = true;
    }

    @VisibleForTesting
    protected void setMenuAlpha(float alpha) {
   
        mAlpha = alpha;
        if (mMenuContainer == null) {
   
            return;
        }
        if (alpha == 0) {
   
            mMenuFadedIn = false; // Can fade in again once it's gone.
            mMenuContainer.setVisibility(View.INVISIBLE);
        } else {
   
            mMenuContainer.setVisibility(View.VISIBLE);
        }
        final int count = mMenuContainer.getChildCount();
        for (int i = 0; i < count; i++) {
   
            mMenuContainer.getChildAt(i).setAlpha(mAlpha);
        }
    }

    /**
     * Returns the horizontal space in pixels required to display the menu.
     */
    @VisibleForTesting
    protected int getSpaceForMenu() {
   
        return mHorizSpaceForIcon * mMenuContainer.getChildCount();
    }

    private final class CheckForDrag implements Runnable {
   
        @Override
        public void run() {
   
            final float absTransX = Math.abs(mTranslation);
            final float bounceBackToMenuWidth = getSpaceForMenu();
            final float notiThreshold = mParent.getWidth() * 0.4f;
            if ((!isMenuVisible() || isMenuLocationChange())
                    && absTransX >= bounceBackToMenuWidth * 0.4
                    && absTransX < notiThreshold) {
   
                fadeInMenu(notiThreshold);
            }
        }
    }

    private void fadeInMenu(final float notiThreshold) {
   
        if (mDismissing || mAnimating) {
   
            return;
        }
        if (isMenuLocationChange()) {
   
            setMenuAlpha(0f);
        }
        final float transX = mTranslation;
        final boolean fromLeft = mTranslation > 0;
        setMenuLocation();
        mFadeAnimator = ValueAnimator.ofFloat(mAlpha, 1);
        mFadeAnimator.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
   
            @Override
            public void onAnimationUpdate(ValueAnimator animation) {
   
                final float absTrans = Math.abs(transX);

                boolean pastMenu = (fromLeft && transX <= notiThreshold)
                        || (!fromLeft && absTrans <= notiThreshold);
                if (pastMenu && !mMenuFadedIn) {
   
                    setMenuAlpha((float) animation.getAnimatedValue());
                }
            }
        });
        mFadeAnimator.addListener(new AnimatorListenerAdapter() {
   
            @Override
            public void onAnimationStart(Animator animation) {
   
                mAnimating = true;
            }

            @Override
            public void onAnimationCancel(Animator animation) {
   
                // TODO should animate back to 0f from current alpha
                setMenuAlpha(0f);
            }

            @Override
            public void onAnimationEnd(Animator animation) {
   
                mAnimating = false;
                mMenuFadedIn = mAlpha == 1;
            }
        });
        mFadeAnimator.setInterpolator(Interpolators.ALPHA_IN);
        mFadeAnimator.setDuration(ICON_ALPHA_ANIM_DURATION);
        mFadeAnimator.start();
    }

    @Override
    public void setMenuItems(ArrayList<MenuItem> items) {
   
        // Do nothing we use our own for now.
        // TODO -- handle / allow custom menu items!
    }

    @Override
    public boolean shouldShowGutsOnSnapOpen() {
   
        return false;
    }

    @Override
    public MenuItem menuItemToExposeOnSnap() {
   
        return null;
    }

    @Override
    public Point getRevealAnimationOrigin() {
   
        View v = mInfoItem.getMenuView();
        int menuX = v.getLeft() + v.getPaddingLeft() + (v.getWidth() / 2);
        int menuY = v.getTop() + v.getPaddingTop() + (v.getHeight() / 2);
        if (isMenuOnLeft()) {
   
            return new Point(menuX, menuY);
        } else {
   
            menuX = mParent.getRight() - menuX;
            return new Point(menuX, menuY);
        }
    }

    static MenuItem createSnoozeItem(Context context) {
   
        Resources res = context.getResources();
        NotificationSnooze content = (NotificationSnooze) LayoutInflater.from(context)
                .inflate(R.layout.notification_snooze, null, false);
        String snoozeDescription = res.getString(R.string.notification_menu_snooze_description);
        MenuItem snooze = new NotificationMenuItem(context, snoozeDescription, content,
                R.drawable.ic_snooze);
        return snooze;
    }

    static NotificationMenuItem createConversationItem(Context context) {
   
        Resources res = context.getResources();
        String infoDescription = res.getString(R.string.notification_menu_gear_description);
        NotificationConversationInfo infoContent =
                (NotificationConversationInfo) LayoutInflater.from(context).inflate(
                        R.layout.notification_conversation_info, null, false);
        return new NotificationMenuItem(context, infoDescription, infoContent,
                R.drawable.ic_settings);
    }

    static NotificationMenuItem createPartialConversationItem(Context context) {
   
        Resources res = context.getResources();
        String infoDescription = res.getString(R.string.notification_menu_gear_description);
        PartialConversationInfo infoContent =
                (PartialConversationInfo) LayoutInflater.from(context).inflate(
                        R.layout.partial_conversation_info, null, false);
        return new NotificationMenuItem(context, infoDescription, infoContent,
                R.drawable.ic_settings);
    }

    static NotificationMenuItem createInfoItem(Context context) {
   
        Resources res = context.getResources();
        String infoDescription = res.getString(R.string.notification_menu_gear_description);
        NotificationInfo infoContent = (NotificationInfo) LayoutInflater.from(context).inflate(
                R.layout.notification_info, null, false);
        return new NotificationMenuItem(context, infoDescription, infoContent,
                R.drawable.ic_settings);
    }

    static MenuItem createAppOpsItem(Context context) {
   
        AppOpsInfo appOpsContent = (AppOpsInfo) LayoutInflater.from(context).inflate(
                R.layout.app_ops_info, null, false);
        MenuItem info = new NotificationMenuItem(context, null, appOpsContent,
                -1 /*don't show in slow swipe menu */);
        return info;
    }

    private void addMenuView(MenuItem item, ViewGroup parent) {
   
        View menuView = item.getMenuView();
        if (menuView != null) {
   
            menuView.setAlpha(mAlpha);
            parent.addView(menuView);
            menuView.setOnClickListener(this);
            FrameLayout.LayoutParams lp = (LayoutParams) menuView.getLayoutParams();
            lp.width = mHorizSpaceForIcon;
            lp.height = mHorizSpaceForIcon;
            menuView.setLayoutParams(lp);
        }
        mMenuItemsByView.put(menuView, item);
    }

    @VisibleForTesting
    /**
     * Determine the minimum offset below which the menu should snap back closed.
     */
    protected float getSnapBackThreshold() {
   
        return getSpaceForMenu() - getMaximumSwipeDistance();
    }

    /**
     * Determine the maximum offset above which the parent notification should be dismissed.
     * @return
     */
    @VisibleForTesting
    protected float getDismissThreshold() {
   
        return getParent().getWidth() * SWIPED_FAR_ENOUGH_SIZE_FRACTION;
    }

    @Override
    public boolean isWithinSnapMenuThreshold() {
   
        float translation = getTranslation();
        float snapBackThreshold = getSnapBackThreshold();
        float targetRight = getDismissThreshold();
        return isMenuOnLeft()
                ? translation > snapBackThreshold && translation < targetRight
                : translation < -snapBackThreshold && translation > -targetRight;
    }

    @Override
    public boolean isSwipedEnoughToShowMenu() {
   
        final float minimumSwipeDistance = getMinimumSwipeDistance();
        final float translation = getTranslation();
        return isMenuVisible() && (isMenuOnLeft() ?
                translation > minimumSwipeDistance
                : translation < -minimumSwipeDistance);
    }

    @Override
    public int getMenuSnapTarget() {
   
        return isMenuOnLeft() ? getSpaceForMenu() : -getSpaceForMenu();
    }

    @Override
    public boolean shouldSnapBack() {
   
        float translation = getTranslation();
        float targetLeft = getSnapBackThreshold();
        return isMenuOnLeft() ? translation < targetLeft : translation > -targetLeft;
    }

    @Override
    public boolean isSnappedAndOnSameSide() {
   
        return isMenuSnapped() && isMenuVisible()
                && isMenuSnappedOnLeft() == isMenuOnLeft();
    }

    @Override
    public boolean canBeDismissed() {
   
        return getParent().canViewBeDismissed();
    }

    @Override
    public void setDismissRtl(boolean dismissRtl) {
   
        mDismissRtl = dismissRtl;
        if (mMenuContainer != null) {
   
            createMenuViews(true, mIsForeground);
        }
    }

    public static class NotificationMenuItem implements MenuItem {
   
        View mMenuView;
        GutsContent mGutsContent;
        String mContentDescription;

        /**
         * Add a new 'guts' panel. If iconResId < 0 it will not appear in the slow swipe menu
         * but can still be exposed via other affordances.
         */
        public NotificationMenuItem(Context context, String contentDescription, GutsContent content,
                int iconResId) {
   
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值